Skip to main content

tpcp_core/
identity.rs

1use alloc::{string::String, vec::Vec};
2use base64::{Engine as _, engine::general_purpose};
3use ed25519_dalek::{Signer, Verifier, SigningKey, VerifyingKey, Signature};
4use serde_json::Value;
5
6/// Signs a JSON payload with an Ed25519 private key.
7/// Returns standard base64-encoded signature (matches Python's base64.b64encode()).
8pub fn sign(key: &SigningKey, payload: &[u8]) -> String {
9    let sig: Signature = key.sign(payload);
10    general_purpose::STANDARD.encode(sig.to_bytes())
11}
12
13/// Verifies a standard base64-encoded Ed25519 signature.
14pub fn verify(pub_key_b64: &str, payload: &[u8], sig_b64: &str) -> bool {
15    let pub_bytes = match general_purpose::STANDARD.decode(pub_key_b64) {
16        Ok(b) => b,
17        Err(_) => return false,
18    };
19    let sig_bytes = match general_purpose::STANDARD.decode(sig_b64) {
20        Ok(b) => b,
21        Err(_) => return false,
22    };
23    let pub_arr: [u8; 32] = match pub_bytes.try_into() {
24        Ok(a) => a,
25        Err(_) => return false,
26    };
27    let sig_arr: [u8; 64] = match sig_bytes.try_into() {
28        Ok(a) => a,
29        Err(_) => return false,
30    };
31    let vk = match VerifyingKey::from_bytes(&pub_arr) {
32        Ok(k) => k,
33        Err(_) => return false,
34    };
35    let sig = Signature::from_bytes(&sig_arr);
36    vk.verify(payload, &sig).is_ok()
37}
38
39/// Serializes a serde_json Value to canonical JSON with sorted keys.
40/// Matches Python's json.dumps(sort_keys=True, separators=(',',':')).
41pub fn canonical_json(value: &Value) -> Vec<u8> {
42    canonical_json_bytes(value)
43}
44
45fn canonical_json_bytes(value: &Value) -> Vec<u8> {
46    match value {
47        Value::Object(map) => {
48            let mut keys: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
49            keys.sort_unstable();
50            let mut out = Vec::from(b"{" as &[u8]);
51            for (i, k) in keys.iter().enumerate() {
52                let key_json = serde_json::to_vec(&Value::String((*k).into())).unwrap_or_default();
53                out.extend_from_slice(&key_json);
54                out.push(b':');
55                out.extend_from_slice(&canonical_json_bytes(&map[*k]));
56                if i + 1 < keys.len() {
57                    out.push(b',');
58                }
59            }
60            out.push(b'}');
61            out
62        }
63        Value::Array(arr) => {
64            let mut out = Vec::from(b"[" as &[u8]);
65            for (i, item) in arr.iter().enumerate() {
66                out.extend_from_slice(&canonical_json_bytes(item));
67                if i + 1 < arr.len() {
68                    out.push(b',');
69                }
70            }
71            out.push(b']');
72            out
73        }
74        other => serde_json::to_vec(other).unwrap_or_default(),
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    /// Build a deterministic SigningKey from a fixed 32-byte seed.
83    fn test_signing_key() -> SigningKey {
84        let seed: [u8; 32] = [
85            1, 2, 3, 4, 5, 6, 7, 8,
86            9, 10, 11, 12, 13, 14, 15, 16,
87            17, 18, 19, 20, 21, 22, 23, 24,
88            25, 26, 27, 28, 29, 30, 31, 32,
89        ];
90        SigningKey::from_bytes(&seed)
91    }
92
93    /// Encode the verifying (public) key as standard base64.
94    fn pub_key_b64(sk: &SigningKey) -> String {
95        general_purpose::STANDARD.encode(sk.verifying_key().to_bytes())
96    }
97
98    #[test]
99    fn test_sign_verify_roundtrip() {
100        let sk = test_signing_key();
101        let pk_b64 = pub_key_b64(&sk);
102        let payload = b"hello tpcp";
103
104        let sig = sign(&sk, payload);
105        assert!(
106            verify(&pk_b64, payload, &sig),
107            "valid signature should verify successfully"
108        );
109    }
110
111    #[test]
112    fn test_verify_rejects_tampered() {
113        let sk = test_signing_key();
114        let pk_b64 = pub_key_b64(&sk);
115        let payload = b"original payload";
116        let tampered = b"tampered payload";
117
118        let sig = sign(&sk, payload);
119        assert!(
120            !verify(&pk_b64, tampered, &sig),
121            "signature over different payload must not verify"
122        );
123    }
124
125    #[test]
126    fn test_verify_rejects_malformed_public_key() {
127        // Public key must be exactly 32 bytes when decoded. This is 16 bytes.
128        let short_key = general_purpose::STANDARD.encode([0u8; 16]);
129        let payload = b"some payload";
130        let sk = test_signing_key();
131        let sig = sign(&sk, payload);
132        assert!(
133            !verify(&short_key, payload, &sig),
134            "malformed (wrong length) public key must fail verification"
135        );
136    }
137
138    #[test]
139    fn test_verify_rejects_invalid_base64_signature() {
140        let sk = test_signing_key();
141        let pk_b64 = pub_key_b64(&sk);
142        let payload = b"test";
143        assert!(
144            !verify(&pk_b64, payload, "not!!!valid===base64"),
145            "invalid base64 signature must fail verification"
146        );
147    }
148
149    #[test]
150    fn test_verify_rejects_invalid_base64_public_key() {
151        let payload = b"test";
152        assert!(
153            !verify("also-not-base64!!!", payload, "AAAA"),
154            "invalid base64 public key must fail verification"
155        );
156    }
157
158    #[test]
159    fn test_canonical_json_empty_object() {
160        let empty: serde_json::Value = serde_json::json!({});
161        let result = canonical_json(&empty);
162        assert_eq!(core::str::from_utf8(&result).unwrap(), "{}");
163    }
164
165    #[test]
166    fn test_canonical_json_nested_objects() {
167        let nested: serde_json::Value = serde_json::json!({
168            "outer": {"z": 1, "a": 2},
169            "arr": [3, 2, 1]
170        });
171        let result = canonical_json(&nested);
172        let s = core::str::from_utf8(&result).unwrap();
173        // "arr" < "outer" alphabetically
174        let pos_arr = s.find("\"arr\"").unwrap();
175        let pos_outer = s.find("\"outer\"").unwrap();
176        assert!(pos_arr < pos_outer, "keys must be sorted: arr before outer");
177        // Nested keys should also be sorted: "a" < "z"
178        let pos_a = s.find("\"a\"").unwrap();
179        let pos_z = s.find("\"z\"").unwrap();
180        assert!(pos_a < pos_z, "nested keys must be sorted: a before z");
181    }
182
183    #[test]
184    fn test_canonical_json_deterministic() {
185        // Two JSON objects with the same keys in different insertion order.
186        let a: Value = serde_json::json!({"z": 1, "a": 2, "m": 3});
187        let b: Value = serde_json::json!({"m": 3, "z": 1, "a": 2});
188
189        let bytes_a = canonical_json(&a);
190        let bytes_b = canonical_json(&b);
191
192        assert_eq!(
193            bytes_a, bytes_b,
194            "canonical_json must produce identical output regardless of key insertion order"
195        );
196        // Keys must appear sorted: "a" < "m" < "z"
197        let s = core::str::from_utf8(&bytes_a).expect("valid UTF-8");
198        let pos_a = s.find("\"a\"").unwrap();
199        let pos_m = s.find("\"m\"").unwrap();
200        let pos_z = s.find("\"z\"").unwrap();
201        assert!(pos_a < pos_m && pos_m < pos_z, "keys must be sorted alphabetically");
202    }
203}