baseid_crypto/
multikey.rs

1//! Multicodec + multibase encoding/decoding for `did:key` support.
2
3use baseid_core::error::CryptoError;
4use baseid_core::types::KeyType;
5
6use crate::key::PublicKey;
7
8// Multicodec prefixes for each key type.
9const ED25519_PREFIX: [u8; 2] = [0xed, 0x01];
10const P256_PREFIX: [u8; 2] = [0x80, 0x24];
11const P384_PREFIX: [u8; 2] = [0x81, 0x24];
12const SECP256K1_PREFIX: [u8; 2] = [0xe7, 0x01];
13
14impl PublicKey {
15    /// Encode this public key as a multibase base58btc string with multicodec prefix.
16    ///
17    /// The result is suitable for use in `did:key` identifiers (e.g., `z6Mk...`).
18    pub fn to_multibase(&self) -> String {
19        let prefix = multicodec_prefix(self.key_type);
20        let mut data = Vec::with_capacity(prefix.len() + self.bytes.len());
21        data.extend_from_slice(&prefix);
22        data.extend_from_slice(&self.bytes);
23        multibase::encode(multibase::Base::Base58Btc, &data)
24    }
25
26    /// Decode a multibase string into a `PublicKey`.
27    ///
28    /// Reads the multicodec prefix to determine the key type, then validates
29    /// the remaining bytes as a public key.
30    pub fn from_multibase(encoded: &str) -> baseid_core::Result<Self> {
31        let (_base, data) =
32            multibase::decode(encoded).map_err(|_| CryptoError::InvalidKeyMaterial)?;
33
34        if data.len() < 2 {
35            return Err(CryptoError::InvalidKeyMaterial.into());
36        }
37
38        let (key_type, prefix_len) = match (data[0], data[1]) {
39            (0xed, 0x01) => (KeyType::Ed25519, 2),
40            (0x80, 0x24) => (KeyType::P256, 2),
41            (0x81, 0x24) => (KeyType::P384, 2),
42            (0xe7, 0x01) => (KeyType::Secp256k1, 2),
43            _ => return Err(CryptoError::UnsupportedAlgorithm.into()),
44        };
45
46        let key_bytes = &data[prefix_len..];
47        PublicKey::from_bytes(key_type, key_bytes)
48    }
49}
50
51// BLS12-381 G2 multicodec prefix (0xeb 0x01)
52const BLS12381G2_PREFIX: [u8; 2] = [0xeb, 0x01];
53
54fn multicodec_prefix(key_type: KeyType) -> [u8; 2] {
55    match key_type {
56        KeyType::Ed25519 => ED25519_PREFIX,
57        KeyType::P256 => P256_PREFIX,
58        KeyType::P384 => P384_PREFIX,
59        KeyType::Secp256k1 => SECP256K1_PREFIX,
60        KeyType::Bls12381G2 => BLS12381G2_PREFIX,
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use crate::key::KeyPair;
68
69    fn roundtrip(key_type: KeyType) {
70        let kp = KeyPair::generate(key_type).unwrap();
71        let encoded = kp.public.to_multibase();
72        // All base58btc multibase strings start with 'z'
73        assert!(encoded.starts_with('z'));
74        let decoded = PublicKey::from_multibase(&encoded).unwrap();
75        assert_eq!(decoded.key_type, kp.public.key_type);
76        assert_eq!(decoded.bytes, kp.public.bytes);
77    }
78
79    #[test]
80    fn roundtrip_ed25519() {
81        roundtrip(KeyType::Ed25519);
82    }
83
84    #[test]
85    fn roundtrip_p256() {
86        roundtrip(KeyType::P256);
87    }
88
89    #[test]
90    fn roundtrip_p384() {
91        roundtrip(KeyType::P384);
92    }
93
94    #[test]
95    fn roundtrip_secp256k1() {
96        roundtrip(KeyType::Secp256k1);
97    }
98
99    #[test]
100    fn ed25519_prefix_format() {
101        let kp = KeyPair::generate(KeyType::Ed25519).unwrap();
102        let encoded = kp.public.to_multibase();
103        // Ed25519 did:key identifiers conventionally start with z6Mk
104        assert!(encoded.starts_with("z6Mk"), "got: {encoded}");
105    }
106
107    #[test]
108    fn reject_invalid_multibase() {
109        assert!(PublicKey::from_multibase("not-valid").is_err());
110    }
111
112    #[test]
113    fn reject_unknown_prefix() {
114        // Encode with a bogus multicodec prefix
115        let mut data = vec![0xFF, 0xFF];
116        data.extend_from_slice(&[0u8; 32]);
117        let encoded = multibase::encode(multibase::Base::Base58Btc, &data);
118        assert!(PublicKey::from_multibase(&encoded).is_err());
119    }
120
121    #[test]
122    fn reject_too_short() {
123        let data = vec![0xed]; // only 1 byte, need at least 2 for prefix
124        let encoded = multibase::encode(multibase::Base::Base58Btc, &data);
125        assert!(PublicKey::from_multibase(&encoded).is_err());
126    }
127}