Skip to main content

solid_pod_rs_didkey/
did.rs

1//! `did:key` identifier encoding / decoding.
2//!
3//! Format: `did:key:z<base58btc(varint(codec) || pubkey)>`
4//!
5//! The 'z' prefix is the multibase tag for base58btc (Bitcoin
6//! alphabet). W3C `did-method-key` pins this encoding — other
7//! multibases are rejected.
8
9use multibase::Base;
10
11use crate::error::DidKeyError;
12use crate::pubkey::DidKeyPubkey;
13
14/// Prefix of every W3C `did:key` identifier.
15pub const DID_KEY_PREFIX: &str = "did:key:";
16
17/// Encode a [`DidKeyPubkey`] to its canonical `did:key:z…` string.
18pub fn encode(pubkey: &DidKeyPubkey) -> String {
19    let payload = pubkey.to_multicodec_bytes();
20    let mb = multibase::encode(Base::Base58Btc, payload);
21    format!("{DID_KEY_PREFIX}{mb}")
22}
23
24/// Decode a `did:key:z…` string back into a [`DidKeyPubkey`].
25///
26/// The method fragment (e.g. `did:key:z…#z…`) is stripped before
27/// decoding so callers that pass a verification-method URL don't need
28/// to preprocess.
29pub fn decode(did: &str) -> Result<DidKeyPubkey, DidKeyError> {
30    let stripped = did
31        .strip_prefix(DID_KEY_PREFIX)
32        .ok_or_else(|| DidKeyError::NotDidKey(did.to_string()))?;
33    let body = stripped.split('#').next().unwrap_or(stripped);
34    if !body.starts_with('z') {
35        return Err(DidKeyError::InvalidMultibase(format!(
36            "did:key requires base58btc 'z' prefix, got '{}'",
37            body.chars().next().unwrap_or(' ')
38        )));
39    }
40    let (base, bytes) = multibase::decode(body)
41        .map_err(|e| DidKeyError::InvalidMultibase(format!("decode: {e}")))?;
42    if base != Base::Base58Btc {
43        return Err(DidKeyError::InvalidMultibase(format!(
44            "did:key multibase must be base58btc, got {base:?}"
45        )));
46    }
47    DidKeyPubkey::from_multicodec_bytes(&bytes)
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::pubkey::{ED25519_LEN, SEC1_COMPRESSED_LEN};
54
55    #[test]
56    fn encode_rejects_method_fragment() {
57        // The decoder strips `#…` so an encoder-decoder mismatch does
58        // not occur. Sanity test.
59        let k = DidKeyPubkey::Ed25519([9u8; ED25519_LEN]);
60        let did = encode(&k);
61        let with_frag = format!("{did}#keys-0");
62        let decoded = decode(&with_frag).unwrap();
63        assert_eq!(decoded, k);
64    }
65
66    #[test]
67    fn ed25519_roundtrip() {
68        let k = DidKeyPubkey::Ed25519([42u8; ED25519_LEN]);
69        let did = encode(&k);
70        assert!(did.starts_with("did:key:z"));
71        let back = decode(&did).unwrap();
72        assert_eq!(back, k);
73    }
74
75    #[test]
76    fn p256_roundtrip() {
77        let mut sec1 = vec![0u8; SEC1_COMPRESSED_LEN];
78        sec1[0] = 0x02;
79        for (i, byte) in sec1.iter_mut().enumerate().skip(1) {
80            *byte = i as u8;
81        }
82        let k = DidKeyPubkey::P256(sec1);
83        let did = encode(&k);
84        let back = decode(&did).unwrap();
85        assert_eq!(back, k);
86    }
87
88    #[test]
89    fn secp256k1_roundtrip() {
90        let mut sec1 = vec![0u8; SEC1_COMPRESSED_LEN];
91        sec1[0] = 0x03;
92        for (i, byte) in sec1.iter_mut().enumerate().skip(1) {
93            *byte = (i as u8).wrapping_mul(3);
94        }
95        let k = DidKeyPubkey::Secp256k1(sec1);
96        let did = encode(&k);
97        let back = decode(&did).unwrap();
98        assert_eq!(back, k);
99    }
100
101    #[test]
102    fn rejects_wrong_prefix() {
103        let err = decode("did:example:123").unwrap_err();
104        assert!(matches!(err, DidKeyError::NotDidKey(_)));
105    }
106
107    #[test]
108    fn rejects_non_base58btc_multibase() {
109        // `u` = base64url. We only accept base58btc.
110        let err = decode("did:key:uAQIDBAUGBw").unwrap_err();
111        assert!(matches!(err, DidKeyError::InvalidMultibase(_)));
112    }
113
114    #[test]
115    fn rejects_malformed_multibase_body() {
116        // Valid 'z' prefix but non-base58 characters ('0', 'O', 'I', 'l'
117        // are forbidden in Bitcoin alphabet).
118        let err = decode("did:key:z0OIl").unwrap_err();
119        assert!(matches!(err, DidKeyError::InvalidMultibase(_)));
120    }
121}