ndn_security/did/
encoding.rs

1//! Encoding and decoding between NDN [`Name`]s and `did:ndn` DID strings.
2//!
3//! # Encoding
4//!
5//! A `did:ndn` DID is the base64url (no padding) encoding of the complete NDN
6//! Name TLV wire format, including the outer `Name-Type` and `TLV-Length` octets.
7//!
8//! ```text
9//! did:ndn:<base64url(Name TLV)>
10//! ```
11//!
12//! Every NDN name maps to exactly one DID, and every valid `did:ndn` DID maps to
13//! exactly one NDN name. The encoding is lossless across all component types
14//! (GenericNameComponent, BLAKE3_DIGEST, ImplicitSha256Digest, versioned
15//! components, etc.) without any type-specific special cases.
16//!
17//! # Backward compatibility
18//!
19//! Earlier drafts of the spec used two forms that are now deprecated:
20//!
21//! - **Simple form** — colon-joined ASCII component values:
22//!   `/com/acme/alice` → `did:ndn:com:acme:alice`
23//! - **`v1:` binary form** — `did:ndn:v1:<base64url(Name TLV)>`
24//!
25//! Both forms are still accepted by [`did_to_name`] for backward compatibility.
26//! [`name_to_did`] no longer produces either deprecated form.
27//!
28//! # Ambiguity in the deprecated scheme
29//!
30//! The `v1:` prefix occupied the same position as the first name-component in the
31//! simple form. A name whose first component is literally `v1` would produce the
32//! same `did:ndn:v1:...` string as a binary-encoded name, making round-trip
33//! decoding impossible without external context. The unified binary form has no
34//! such ambiguity.
35
36use ndn_packet::Name;
37use ndn_packet::name::NameComponent;
38
39use crate::did::resolver::DidError;
40
41/// TLV type for Name container.
42const NAME_TLV_TYPE: u64 = 7;
43
44/// Encode an NDN [`Name`] as a `did:ndn` DID string.
45///
46/// The method-specific identifier is the base64url (no padding) encoding of the
47/// complete NDN Name TLV wire format, including the outer `07 <length>` bytes.
48///
49/// ```
50/// # use ndn_security::did::encoding::name_to_did;
51/// # use ndn_packet::Name;
52/// let name: Name = "/com/acme/alice".parse().unwrap();
53/// let did = name_to_did(&name);
54/// assert!(did.starts_with("did:ndn:"));
55/// // The method-specific-id is base64url — no colons, no v1: prefix.
56/// assert!(!did["did:ndn:".len()..].contains(':'));
57/// ```
58pub fn name_to_did(name: &Name) -> String {
59    let tlv = encode_name_tlv(name);
60    let encoded = base64_url_encode(&tlv);
61    format!("did:ndn:{encoded}")
62}
63
64/// Decode a `did:ndn` DID string back to an NDN [`Name`].
65///
66/// Accepts:
67/// - **Current form**: `did:ndn:<base64url(Name TLV)>` — no colons in the
68///   method-specific identifier.
69/// - **Deprecated `v1:` form**: `did:ndn:v1:<base64url(Name TLV)>` — parsed
70///   for backward compatibility but no longer produced by [`name_to_did`].
71/// - **Deprecated simple form**: `did:ndn:com:acme:alice` — parsed for backward
72///   compatibility as colon-separated GenericNameComponent ASCII values.
73pub fn did_to_name(did: &str) -> Result<Name, DidError> {
74    let rest = did
75        .strip_prefix("did:ndn:")
76        .ok_or_else(|| DidError::InvalidDid(did.to_string()))?;
77
78    if rest.contains(':') {
79        // Legacy forms (both use colons, which are not in the base64url alphabet).
80        if let Some(encoded) = rest.strip_prefix("v1:") {
81            // Deprecated v1: binary form.
82            let bytes = base64_url_decode(encoded)
83                .map_err(|_| DidError::InvalidDid(format!("invalid base64url in {did}")))?;
84            decode_name_tlv(&bytes)
85                .map_err(|_| DidError::InvalidDid(format!("invalid TLV name in {did}")))
86        } else {
87            // Deprecated simple colon-encoded form.
88            colon_decode(rest)
89                .ok_or_else(|| DidError::InvalidDid(format!("invalid did:ndn: {did}")))
90        }
91    } else {
92        // Current binary form: the entire method-specific-id is base64url.
93        let bytes = base64_url_decode(rest)
94            .map_err(|_| DidError::InvalidDid(format!("invalid base64url in {did}")))?;
95        decode_name_tlv(&bytes)
96            .map_err(|_| DidError::InvalidDid(format!("invalid TLV name in {did}")))
97    }
98}
99
100// ── Legacy helpers (kept for did_to_name backward compat) ────────────────────
101
102fn colon_decode(s: &str) -> Option<Name> {
103    if s.is_empty() {
104        return Some(Name::root());
105    }
106    let mut name = Name::root();
107    for part in s.split(':') {
108        if part.is_empty() {
109            return None;
110        }
111        name = name.append(part);
112    }
113    Some(name)
114}
115
116// ── TLV encoding / decoding ───────────────────────────────────────────────────
117
118fn encode_name_tlv(name: &Name) -> Vec<u8> {
119    let mut inner: Vec<u8> = Vec::new();
120    for comp in name.components() {
121        write_tlv_to(&mut inner, comp.typ, &comp.value);
122    }
123    let mut out = Vec::new();
124    write_tlv_to(&mut out, NAME_TLV_TYPE, &inner);
125    out
126}
127
128fn decode_name_tlv(data: &[u8]) -> Result<Name, ()> {
129    let (typ, inner, _) = read_tlv(data).ok_or(())?;
130    if typ != NAME_TLV_TYPE {
131        return Err(());
132    }
133    let mut comps = Vec::new();
134    let mut rest = inner;
135    while !rest.is_empty() {
136        let (typ, val, remaining) = read_tlv(rest).ok_or(())?;
137        comps.push(NameComponent {
138            typ,
139            value: bytes::Bytes::copy_from_slice(val),
140        });
141        rest = remaining;
142    }
143    Ok(Name::from_components(comps))
144}
145
146fn write_tlv_to(buf: &mut Vec<u8>, typ: u64, value: &[u8]) {
147    write_varu64(buf, typ);
148    write_varu64(buf, value.len() as u64);
149    buf.extend_from_slice(value);
150}
151
152fn write_varu64(buf: &mut Vec<u8>, v: u64) {
153    if v <= 252 {
154        buf.push(v as u8);
155    } else if v <= 0xFFFF {
156        buf.push(0xFD);
157        buf.extend_from_slice(&(v as u16).to_be_bytes());
158    } else if v <= 0xFFFF_FFFF {
159        buf.push(0xFE);
160        buf.extend_from_slice(&(v as u32).to_be_bytes());
161    } else {
162        buf.push(0xFF);
163        buf.extend_from_slice(&v.to_be_bytes());
164    }
165}
166
167fn read_varu64(buf: &[u8]) -> Option<(u64, usize)> {
168    let first = *buf.first()?;
169    match first {
170        0..=252 => Some((first as u64, 1)),
171        0xFD => {
172            let b = buf.get(1..3)?;
173            Some((u16::from_be_bytes([b[0], b[1]]) as u64, 3))
174        }
175        0xFE => {
176            let b = buf.get(1..5)?;
177            Some((u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as u64, 5))
178        }
179        0xFF => {
180            let b = buf.get(1..9)?;
181            Some((u64::from_be_bytes(b.try_into().ok()?), 9))
182        }
183    }
184}
185
186fn read_tlv(buf: &[u8]) -> Option<(u64, &[u8], &[u8])> {
187    let (typ, t_sz) = read_varu64(buf)?;
188    let rest = &buf[t_sz..];
189    let (len, l_sz) = read_varu64(rest)?;
190    let rest = &rest[l_sz..];
191    let len = len as usize;
192    if rest.len() < len {
193        return None;
194    }
195    Some((typ, &rest[..len], &rest[len..]))
196}
197
198fn base64_url_encode(data: &[u8]) -> String {
199    use base64::Engine;
200    base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
201}
202
203fn base64_url_decode(s: &str) -> Result<Vec<u8>, ()> {
204    use base64::Engine;
205    base64::engine::general_purpose::URL_SAFE_NO_PAD
206        .decode(s)
207        .map_err(|_| ())
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use ndn_packet::Name;
214
215    #[test]
216    fn roundtrip_ascii_name() {
217        let name: Name = "/com/acme/alice".parse().unwrap();
218        let did = name_to_did(&name);
219        // Current form: binary, no colons in method-specific-id.
220        assert!(did.starts_with("did:ndn:"));
221        assert!(!did["did:ndn:".len()..].contains(':'));
222        let back = did_to_name(&did).unwrap();
223        assert_eq!(back, name);
224    }
225
226    #[test]
227    fn roundtrip_root() {
228        let name = Name::root();
229        let did = name_to_did(&name);
230        assert!(did.starts_with("did:ndn:"));
231        let back = did_to_name(&did).unwrap();
232        assert_eq!(back, name);
233    }
234
235    #[test]
236    fn roundtrip_versioned_component() {
237        let name: Name = "/com/acme".parse().unwrap();
238        let name = name.append_version(42);
239        let did = name_to_did(&name);
240        // Must be binary form — no v1: prefix.
241        assert!(did.starts_with("did:ndn:"));
242        assert!(!did["did:ndn:".len()..].starts_with("v1:"));
243        let back = did_to_name(&did).unwrap();
244        assert_eq!(back, name);
245    }
246
247    #[test]
248    fn no_v1_ambiguity() {
249        // A name literally starting with "v1" must round-trip correctly.
250        // Under the old scheme this was ambiguous; under binary-only it is not.
251        let name: Name = "/v1/BwEA".parse().unwrap();
252        let did = name_to_did(&name);
253        assert!(did.starts_with("did:ndn:"));
254        // The DID does NOT contain "v1:" — it's all base64url.
255        assert!(!did.contains("v1:"));
256        let back = did_to_name(&did).unwrap();
257        assert_eq!(back, name);
258    }
259
260    // ── Backward-compat parsing ───────────────────────────────────────────────
261
262    #[test]
263    fn compat_simple_form() {
264        // Old `did:ndn:com:acme:alice` form still parses.
265        let name = did_to_name("did:ndn:com:acme:alice").unwrap();
266        assert_eq!(name, "/com/acme/alice".parse::<Name>().unwrap());
267    }
268
269    #[test]
270    fn compat_v1_binary_form() {
271        // Old `did:ndn:v1:<base64>` form still parses.
272        let original: Name = "/com/acme".parse().unwrap();
273        let original = original.append_version(42);
274        // Produce old form manually.
275        let tlv = encode_name_tlv(&original);
276        let b64 = base64_url_encode(&tlv);
277        let old_did = format!("did:ndn:v1:{b64}");
278        let back = did_to_name(&old_did).unwrap();
279        assert_eq!(back, original);
280    }
281}