Skip to main content

macula_rust/ucan/
did_key.rs

1//! did:key for a node key as carried: `did:key:z` and the base58btc of the
2//! key type's unsigned LEB128 varint, then the key. The key type is
3//! mldsa-87-pub (0x1212) in pq_pure, and Macula's own, private-use
4//! (0x300087), for pq_hybrid's composite, which has no multicodec yet.
5
6use super::Refusal;
7use crate::node_key::carried_key_well_formed;
8use crate::profile::Profile;
9
10const CODEC_MLDSA87: u64 = 0x1212;
11const CODEC_MLDSA87_RSA4096: u64 = 0x30_0087;
12
13const PREFIX: &str = "did:key:z";
14
15/// The longest base58btc text a did:key for a node key can have: a pq_hybrid
16/// key, the longest, is a 3-byte codec varint, the 2,592-byte ML-DSA-87 key
17/// and a DER RSA-4096 public key of about 526 bytes, some 4,270 characters.
18/// Anything longer is malformed, refused before it is decoded: base58 decodes
19/// in time quadratic in its length, ahead of the signature check. macula pins
20/// it in ucan_v1.json (`did_key_length`, macula#87).
21pub const MAX_DID_KEY_ENCODED: usize = 4_400;
22const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
23
24/// The did:key for a key as carried in `profile`.
25pub fn did_key(carried: &[u8], profile: Profile) -> String {
26    let mut bytes = varint(codec(profile));
27    bytes.extend_from_slice(carried);
28    format!("{PREFIX}{}", base58btc_encode(&bytes))
29}
30
31/// The key a did:key carries, when it is a key in its one carried form for
32/// `profile` (D13); [`Refusal::Malformed`] otherwise.
33pub fn carried_key(did: &str, profile: Profile) -> Result<Vec<u8>, Refusal> {
34    let encoded = did.strip_prefix(PREFIX).ok_or(Refusal::Malformed)?;
35    if encoded.len() > MAX_DID_KEY_ENCODED {
36        return Err(Refusal::Malformed);
37    }
38    let prefix = varint(codec(profile));
39    let decoded = base58btc_decode(encoded).ok_or(Refusal::Malformed)?;
40    match decoded.strip_prefix(prefix.as_slice()) {
41        Some(carried) if !carried.is_empty() && carried_key_well_formed(carried, profile) => {
42            Ok(carried.to_vec())
43        }
44        _ => Err(Refusal::Malformed),
45    }
46}
47
48fn codec(profile: Profile) -> u64 {
49    match profile {
50        Profile::PqPure => CODEC_MLDSA87,
51        Profile::PqHybrid => CODEC_MLDSA87_RSA4096,
52    }
53}
54
55/// An unsigned LEB128 varint, as multicodec prefixes are written.
56fn varint(mut n: u64) -> Vec<u8> {
57    let mut out = Vec::new();
58    while n >= 0x80 {
59        out.push((n & 0x7f) as u8 | 0x80);
60        n >>= 7;
61    }
62    out.push(n as u8);
63    out
64}
65
66/// base58 with the Bitcoin alphabet, as multibase's base58btc: each leading
67/// zero byte is a leading `1`.
68fn base58btc_encode(bytes: &[u8]) -> String {
69    let zeros = bytes.iter().take_while(|&&b| b == 0).count();
70    let mut digits: Vec<u8> = Vec::new();
71    for &byte in &bytes[zeros..] {
72        let mut carry = byte as u32;
73        for digit in digits.iter_mut() {
74            carry += (*digit as u32) << 8;
75            *digit = (carry % 58) as u8;
76            carry /= 58;
77        }
78        while carry > 0 {
79            digits.push((carry % 58) as u8);
80            carry /= 58;
81        }
82    }
83    let mut out = String::with_capacity(zeros + digits.len());
84    out.extend(std::iter::repeat_n('1', zeros));
85    out.extend(digits.iter().rev().map(|&d| ALPHABET[d as usize] as char));
86    out
87}
88
89/// The bytes a base58btc text encodes, or `None` for a text with a
90/// character outside the alphabet.
91fn base58btc_decode(text: &str) -> Option<Vec<u8>> {
92    let ones = text.bytes().take_while(|&b| b == b'1').count();
93    let mut value: Vec<u8> = Vec::new();
94    for c in text[ones..].bytes() {
95        let mut carry = ALPHABET.iter().position(|&a| a == c)? as u32;
96        for byte in value.iter_mut() {
97            carry += (*byte as u32) * 58;
98            *byte = (carry & 0xff) as u8;
99            carry >>= 8;
100        }
101        while carry > 0 {
102            value.push((carry & 0xff) as u8);
103            carry >>= 8;
104        }
105    }
106    let mut out = vec![0u8; ones];
107    out.extend(value.iter().rev());
108    Some(out)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn base58btc_round_trips_and_keeps_leading_zeros() {
117        for bytes in [
118            vec![],
119            vec![0],
120            vec![0, 0, 1],
121            vec![0xff; 40],
122            (0u8..=255).collect::<Vec<_>>(),
123        ] {
124            let text = base58btc_encode(&bytes);
125            assert_eq!(base58btc_decode(&text), Some(bytes.clone()), "{text}");
126        }
127        assert_eq!(base58btc_encode(&[0, 0, 0x3a]), "1121");
128        assert_eq!(base58btc_decode("0OIl"), None);
129    }
130
131    #[test]
132    fn the_key_types_are_leb128_varints() {
133        assert_eq!(varint(CODEC_MLDSA87), vec![0x92, 0x24]);
134        assert_eq!(varint(CODEC_MLDSA87_RSA4096), vec![0x87, 0x81, 0xc0, 0x01]);
135    }
136
137    #[test]
138    fn a_did_key_of_another_shape_carries_no_key() {
139        assert_eq!(
140            carried_key("did:key:x", Profile::PqPure),
141            Err(Refusal::Malformed)
142        );
143        assert_eq!(
144            carried_key("did:key:z", Profile::PqPure),
145            Err(Refusal::Malformed)
146        );
147        let short = did_key(&[1, 2, 3], Profile::PqPure);
148        assert_eq!(
149            carried_key(&short, Profile::PqPure),
150            Err(Refusal::Malformed)
151        );
152    }
153}