Skip to main content

aex_core/
signature.rs

1use serde::{Deserialize, Serialize};
2
3/// The signature algorithm used to produce a [`Signature`].
4///
5/// New variants are added as we onboard identity schemes. Do NOT repurpose
6/// existing variants — audit entries reference these values forever.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "kebab-case")]
9pub enum SignatureAlgorithm {
10    /// Ed25519 — used by the native Spize identity provider.
11    Ed25519,
12    /// ECDSA over secp256k1 — used by DID-ethr / EtereCitizen identities
13    /// (Ethereum-compatible wallet signatures).
14    EcdsaSecp256k1,
15}
16
17/// A cryptographic signature over an opaque byte string.
18///
19/// The interpretation of `bytes` depends on `algorithm`:
20/// - `Ed25519`: 64-byte signature as per RFC 8032.
21/// - `EcdsaSecp256k1`: 65-byte (r || s || v) Ethereum signature format.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct Signature {
24    pub algorithm: SignatureAlgorithm,
25    #[serde(with = "hex_bytes")]
26    pub bytes: Vec<u8>,
27}
28
29/// Serialize `Vec<u8>` as hex string (avoid bloating JSON with base64-looking blobs).
30mod hex_bytes {
31    use serde::{Deserialize, Deserializer, Serializer};
32
33    pub fn serialize<S: Serializer>(bytes: &[u8], ser: S) -> Result<S::Ok, S::Error> {
34        ser.serialize_str(&hex_encode(bytes))
35    }
36
37    pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Vec<u8>, D::Error> {
38        let s = String::deserialize(de)?;
39        hex_decode(&s).map_err(serde::de::Error::custom)
40    }
41
42    fn hex_encode(bytes: &[u8]) -> String {
43        let mut s = String::with_capacity(bytes.len() * 2);
44        for b in bytes {
45            s.push_str(&format!("{:02x}", b));
46        }
47        s
48    }
49
50    fn hex_decode(s: &str) -> Result<Vec<u8>, String> {
51        if !s.len().is_multiple_of(2) {
52            return Err(format!("odd hex length: {}", s.len()));
53        }
54        let mut out = Vec::with_capacity(s.len() / 2);
55        for i in (0..s.len()).step_by(2) {
56            let byte = u8::from_str_radix(&s[i..i + 2], 16)
57                .map_err(|e| format!("invalid hex at {}: {}", i, e))?;
58            out.push(byte);
59        }
60        Ok(out)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn serde_roundtrip_ed25519() {
70        let sig = Signature {
71            algorithm: SignatureAlgorithm::Ed25519,
72            bytes: vec![0xde, 0xad, 0xbe, 0xef],
73        };
74        let json = serde_json::to_string(&sig).unwrap();
75        assert!(json.contains("deadbeef"));
76        assert!(json.contains("ed25519"));
77        let back: Signature = serde_json::from_str(&json).unwrap();
78        assert_eq!(sig, back);
79    }
80
81    #[test]
82    fn serde_roundtrip_ecdsa() {
83        let sig = Signature {
84            algorithm: SignatureAlgorithm::EcdsaSecp256k1,
85            bytes: vec![0x01, 0x02, 0x03],
86        };
87        let json = serde_json::to_string(&sig).unwrap();
88        assert!(json.contains("ecdsa-secp256k1"));
89        let back: Signature = serde_json::from_str(&json).unwrap();
90        assert_eq!(sig, back);
91    }
92}