ndn_security/did/
convert.rs

1//! Conversion between NDN [`Certificate`]s and [`DidDocument`]s.
2//!
3//! An NDN certificate is structurally equivalent to a DID Document:
4//! the namespace IS the identifier, and the certificate's public key
5//! is the verification method. This module makes that equivalence explicit
6//! and provides a builder for zone-based DID Documents.
7
8use std::sync::Arc;
9
10use ndn_packet::Name;
11
12use crate::Certificate;
13use crate::did::{
14    document::{DidDocument, VerificationMethod, VerificationRef},
15    encoding::name_to_did,
16};
17
18/// TLV type for GenericNameComponent.
19const GENERIC_NAME_COMPONENT: u64 = 8;
20const KEY_COMPONENT: &[u8] = b"KEY";
21
22/// Convert an NDN certificate to a W3C [`DidDocument`].
23///
24/// The certificate's `name` is expected to be a KEY name like
25/// `/com/acme/alice/KEY/v=123/self`. The identity DID is derived from
26/// the prefix before `/KEY/`.
27///
28/// # Key agreement key
29///
30/// Pass `x25519_key` to include a `keyAgreement` entry. This is an X25519
31/// public key separate from the Ed25519 signing key — critical for NDA's
32/// encrypted content tier. The X25519 key is typically derived from the
33/// Ed25519 seed (e.g., via RFC 7748 conversion) or generated independently.
34pub fn cert_to_did_document(cert: &Certificate, x25519_key: Option<&[u8]>) -> DidDocument {
35    let identity_name = strip_key_suffix(cert.name.as_ref());
36    let did = name_to_did(&identity_name);
37    let key_id = format!("{did}#key-0");
38
39    let vm = VerificationMethod::ed25519_jwk(&key_id, &did, &cert.public_key);
40
41    let mut doc = DidDocument {
42        context: vec![
43            "https://www.w3.org/ns/did/v1".to_string(),
44            "https://w3id.org/security/suites/jws-2020/v1".to_string(),
45        ],
46        id: did.clone(),
47        controller: None,
48        verification_methods: vec![vm],
49        authentication: vec![VerificationRef::Reference(key_id.clone())],
50        assertion_method: vec![VerificationRef::Reference(key_id.clone())],
51        key_agreement: vec![],
52        capability_invocation: vec![VerificationRef::Reference(key_id.clone())],
53        capability_delegation: vec![VerificationRef::Reference(key_id)],
54        service: vec![],
55        also_known_as: vec![],
56    };
57
58    // Add X25519 key agreement VM if provided.
59    if let Some(x25519_bytes) = x25519_key {
60        let ka_id = format!("{did}#key-agreement-0");
61        let ka_vm = VerificationMethod::x25519_jwk(&ka_id, &did, x25519_bytes);
62        doc.verification_methods.push(ka_vm);
63        doc.key_agreement.push(VerificationRef::Reference(ka_id));
64    }
65
66    // Set also_known_as from issuer name if different from subject.
67    if let Some(issuer) = &cert.issuer {
68        let issuer_identity = strip_key_suffix(issuer.as_ref());
69        let issuer_did = name_to_did(&issuer_identity);
70        if issuer_did != did {
71            doc.also_known_as.push(issuer_did);
72        }
73    }
74
75    doc
76}
77
78/// Build a W3C DID Document for a self-certifying [`ZoneKey`].
79///
80/// The resulting document:
81/// - Has `id` = `did:ndn:<base64url(zone-root-name-TLV)>` (binary-only encoding)
82/// - Lists the Ed25519 signing key as `authentication`, `assertionMethod`,
83///   `capabilityInvocation`, and `capabilityDelegation`
84/// - Optionally lists an X25519 key as `keyAgreement`
85/// - Has no `controller` (the zone controls itself)
86///
87/// Publish this document as a signed Data packet at the zone root name so
88/// that `NdnDidResolver` can fetch and verify it.
89pub fn build_zone_did_document(
90    zone_key: &crate::zone::ZoneKey,
91    x25519_key: Option<&[u8]>,
92    services: Vec<crate::did::document::Service>,
93) -> DidDocument {
94    let did = zone_key.zone_root_did();
95    let key_id = format!("{did}#key-0");
96
97    let vm = VerificationMethod::ed25519_jwk(&key_id, &did, zone_key.public_key_bytes());
98
99    let mut doc = DidDocument {
100        context: vec![
101            "https://www.w3.org/ns/did/v1".to_string(),
102            "https://w3id.org/security/suites/jws-2020/v1".to_string(),
103        ],
104        id: did.clone(),
105        controller: None,
106        verification_methods: vec![vm],
107        authentication: vec![VerificationRef::Reference(key_id.clone())],
108        assertion_method: vec![VerificationRef::Reference(key_id.clone())],
109        key_agreement: vec![],
110        capability_invocation: vec![VerificationRef::Reference(key_id.clone())],
111        capability_delegation: vec![VerificationRef::Reference(key_id)],
112        service: services,
113        also_known_as: vec![],
114    };
115
116    if let Some(x25519_bytes) = x25519_key {
117        let ka_id = format!("{did}#key-agreement-0");
118        let ka_vm = VerificationMethod::x25519_jwk(&ka_id, &did, x25519_bytes);
119        doc.verification_methods.push(ka_vm);
120        doc.key_agreement.push(VerificationRef::Reference(ka_id));
121    }
122
123    doc
124}
125
126/// Build a deactivated zone DID Document expressing zone succession.
127///
128/// When a zone owner rotates to a new zone, they publish this document at the
129/// old zone root name. The `successor_did` is listed in `alsoKnownAs`.
130/// Resolvers that check [`DidResolutionResult::is_deactivated`] will follow
131/// the succession chain.
132pub fn build_zone_succession_document(
133    old_zone_key: &crate::zone::ZoneKey,
134    successor_did: impl Into<String>,
135) -> DidDocument {
136    let did = old_zone_key.zone_root_did();
137    let key_id = format!("{did}#key-0");
138    let vm = VerificationMethod::ed25519_jwk(&key_id, &did, old_zone_key.public_key_bytes());
139
140    DidDocument {
141        context: vec!["https://www.w3.org/ns/did/v1".to_string()],
142        id: did.clone(),
143        controller: None,
144        verification_methods: vec![vm],
145        authentication: vec![VerificationRef::Reference(key_id.clone())],
146        assertion_method: vec![],
147        key_agreement: vec![],
148        capability_invocation: vec![],
149        capability_delegation: vec![],
150        service: vec![],
151        also_known_as: vec![successor_did.into()],
152    }
153}
154
155/// Attempt to reconstruct a trust anchor [`Certificate`] from a [`DidDocument`].
156///
157/// Returns `None` if the document does not contain a recognised Ed25519 key.
158pub fn did_document_to_trust_anchor(doc: &DidDocument, name: Arc<Name>) -> Option<Certificate> {
159    let key_bytes = doc.ed25519_public_key()?;
160    Some(Certificate {
161        name,
162        public_key: bytes::Bytes::copy_from_slice(&key_bytes),
163        valid_from: 0,
164        valid_until: u64::MAX,
165        issuer: None,
166        signed_region: None,
167        sig_value: None,
168    })
169}
170
171/// Strip the `/KEY/<version>/<issuer>` suffix from a certificate name.
172///
173/// `/com/acme/alice/KEY/v=123/self` → `/com/acme/alice`
174pub(crate) fn strip_key_suffix(name: &Name) -> Name {
175    let comps = name.components();
176    let key_pos = comps
177        .iter()
178        .rposition(|c| c.typ == GENERIC_NAME_COMPONENT && c.value.as_ref() == KEY_COMPONENT);
179    match key_pos {
180        Some(pos) if pos > 0 => Name::from_components(comps[..pos].iter().cloned()),
181        _ => name.clone(),
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn strip_key_suffix_basic() {
191        let name: Name = "/com/acme/alice/KEY/v=123/self".parse().unwrap();
192        let stripped = strip_key_suffix(&name);
193        let expected: Name = "/com/acme/alice".parse().unwrap();
194        assert_eq!(stripped, expected);
195    }
196
197    #[test]
198    fn strip_key_suffix_no_key() {
199        let name: Name = "/com/acme/alice".parse().unwrap();
200        let stripped = strip_key_suffix(&name);
201        assert_eq!(stripped, name);
202    }
203
204    #[test]
205    fn cert_to_did_doc_has_required_relationships() {
206        use bytes::Bytes;
207        let name: Name = "/com/acme/alice/KEY/v=1/self".parse().unwrap();
208        let cert = Certificate {
209            name: Arc::new(name),
210            public_key: Bytes::from(vec![0u8; 32]),
211            valid_from: 0,
212            valid_until: u64::MAX,
213            issuer: None,
214            signed_region: None,
215            sig_value: None,
216        };
217        let doc = cert_to_did_document(&cert, None);
218        // name_to_did always produces binary-encoded DIDs; verify the id round-trips
219        // back to the identity name rather than asserting the deprecated simple form.
220        let identity_name: Name = "/com/acme/alice".parse().unwrap();
221        let expected_did = crate::did::encoding::name_to_did(&identity_name);
222        assert_eq!(doc.id, expected_did);
223        assert!(!doc.authentication.is_empty());
224        assert!(!doc.assertion_method.is_empty());
225        assert!(!doc.capability_invocation.is_empty());
226        assert!(!doc.capability_delegation.is_empty());
227        assert!(doc.key_agreement.is_empty()); // no X25519 supplied
228    }
229
230    #[test]
231    fn cert_to_did_doc_with_x25519() {
232        use bytes::Bytes;
233        let name: Name = "/com/acme/alice/KEY/v=1/self".parse().unwrap();
234        let cert = Certificate {
235            name: Arc::new(name),
236            public_key: Bytes::from(vec![0u8; 32]),
237            valid_from: 0,
238            valid_until: u64::MAX,
239            issuer: None,
240            signed_region: None,
241            sig_value: None,
242        };
243        let x25519 = [0xABu8; 32];
244        let doc = cert_to_did_document(&cert, Some(&x25519));
245        assert!(!doc.key_agreement.is_empty());
246        assert_eq!(doc.verification_methods.len(), 2);
247        // The X25519 VM should have crv=X25519.
248        let ka_vm = &doc.verification_methods[1];
249        let crv = ka_vm
250            .public_key_jwk
251            .as_ref()
252            .and_then(|jwk| jwk.get("crv"))
253            .and_then(|v| v.as_str());
254        assert_eq!(crv, Some("X25519"));
255    }
256}