ndn_security/did/resolver/
ndn.rs

1//! `did:ndn` resolver — resolves via NDN Interest/Data exchange.
2//!
3//! Two resolution strategies, chosen by decoding the binary DID back to the
4//! underlying NDN name and inspecting the first component type:
5//!
6//! **CA-anchored** (name components are all GenericNameComponents):
7//! Fetches the certificate at `<identity-name>/KEY` and converts it to a
8//! DID Document via [`cert_to_did_document`].
9//!
10//! **Zone** (first component is BLAKE3_DIGEST type):
11//! The zone root name IS the DID. The resolver fetches a published DID
12//! Document Data packet at `<zone-root-name>`. Zone owners must publish
13//! their DID Document as a signed Data packet for resolvers to find it.
14//! The resolver verifies that `blake3(ed25519_pubkey)` in the fetched
15//! document matches the zone root name component.
16//!
17//! **Stub mode** (no fetcher configured): returns `DidError::Resolution`.
18//! Wire up a live fetcher via [`NdnDidResolver::with_fetcher`] /
19//! [`NdnDidResolver::with_did_doc_fetcher`].
20
21use std::{future::Future, pin::Pin, sync::Arc};
22
23use ndn_packet::Name;
24
25use crate::{
26    Certificate,
27    did::{
28        convert::cert_to_did_document,
29        document::DidDocument,
30        encoding::did_to_name,
31        metadata::{DidResolutionError, DidResolutionResult},
32        resolver::DidResolver,
33    },
34};
35
36// ── Fetcher function types ────────────────────────────────────────────────────
37
38/// Fetch an NDN certificate by name (used for CA-anchored DIDs).
39pub type NdnFetchFn =
40    Arc<dyn Fn(Name) -> Pin<Box<dyn Future<Output = Option<Certificate>> + Send>> + Send + Sync>;
41
42/// Fetch an NDN DID Document Data packet by name (used for zone DIDs).
43///
44/// The returned bytes should be the JSON-LD `application/did+ld+json` content
45/// of a signed NDN Data packet at the zone root name.
46pub type NdnDidDocFetchFn =
47    Arc<dyn Fn(Name) -> Pin<Box<dyn Future<Output = Option<Vec<u8>>> + Send>> + Send + Sync>;
48
49// ── Resolver ─────────────────────────────────────────────────────────────────
50
51/// Resolves `did:ndn` DIDs by sending NDN Interests.
52///
53/// Configure via the builder methods:
54/// - [`with_fetcher`](Self::with_fetcher) — for CA-anchored DIDs (cert fetch)
55/// - [`with_did_doc_fetcher`](Self::with_did_doc_fetcher) — for zone DIDs
56///   (raw DID Document fetch)
57///
58/// Both can be configured on the same instance.
59#[derive(Default, Clone)]
60pub struct NdnDidResolver {
61    /// Fetcher for CA-anchored DIDs: fetches `<identity-name>/KEY`.
62    cert_fetcher: Option<NdnFetchFn>,
63    /// Fetcher for zone DIDs: fetches the zone root DID Document.
64    did_doc_fetcher: Option<NdnDidDocFetchFn>,
65}
66
67impl NdnDidResolver {
68    /// Attach a certificate fetch function for CA-anchored `did:ndn` DIDs.
69    ///
70    /// The function receives the identity name (e.g. `/com/acme/alice`) and
71    /// should return the certificate at `<name>/KEY` if found.
72    pub fn with_fetcher(mut self, f: NdnFetchFn) -> Self {
73        self.cert_fetcher = Some(f);
74        self
75    }
76
77    /// Attach a DID Document fetch function for zone `did:ndn:v1:…` DIDs.
78    ///
79    /// The function receives the zone root name and should return the raw
80    /// JSON-LD DID Document bytes from the Data packet at that name.
81    pub fn with_did_doc_fetcher(mut self, f: NdnDidDocFetchFn) -> Self {
82        self.did_doc_fetcher = Some(f);
83        self
84    }
85}
86
87impl DidResolver for NdnDidResolver {
88    fn method(&self) -> &str {
89        "ndn"
90    }
91
92    fn resolve<'a>(
93        &'a self,
94        did: &'a str,
95    ) -> Pin<Box<dyn Future<Output = DidResolutionResult> + Send + 'a>> {
96        let cert_fetcher = self.cert_fetcher.clone();
97        let did_doc_fetcher = self.did_doc_fetcher.clone();
98        let did = did.to_string();
99
100        Box::pin(async move {
101            let name = match did_to_name(&did) {
102                Ok(n) => n,
103                Err(e) => {
104                    return DidResolutionResult::err(
105                        DidResolutionError::InvalidDid,
106                        format!("cannot decode did:ndn name: {e}"),
107                    );
108                }
109            };
110
111            if name.is_zone_root() {
112                resolve_zone_did(&did, name, did_doc_fetcher).await
113            } else {
114                resolve_ca_did(&did, name, cert_fetcher).await
115            }
116        })
117    }
118}
119
120// ── CA-anchored resolution ────────────────────────────────────────────────────
121
122async fn resolve_ca_did(
123    did: &str,
124    identity_name: Name,
125    fetcher: Option<NdnFetchFn>,
126) -> DidResolutionResult {
127    let fetch = match fetcher {
128        Some(f) => f,
129        None => {
130            return DidResolutionResult::err(
131                DidResolutionError::InternalError,
132                "no NDN certificate fetcher configured for CA-anchored DID resolution",
133            );
134        }
135    };
136
137    let key_name = identity_name.append("KEY");
138    match fetch(key_name).await {
139        Some(cert) => DidResolutionResult::ok(cert_to_did_document(&cert, None)),
140        None => DidResolutionResult::err(
141            DidResolutionError::NotFound,
142            format!("certificate not found for DID: {did}"),
143        ),
144    }
145}
146
147// ── Zone DID resolution ───────────────────────────────────────────────────────
148
149async fn resolve_zone_did(
150    did: &str,
151    zone_root: Name,
152    fetcher: Option<NdnDidDocFetchFn>,
153) -> DidResolutionResult {
154    let fetch = match fetcher {
155        Some(f) => f,
156        None => {
157            return DidResolutionResult::err(
158                DidResolutionError::InternalError,
159                "no NDN DID document fetcher configured for zone DID resolution",
160            );
161        }
162    };
163
164    let raw = match fetch(zone_root.clone()).await {
165        Some(b) => b,
166        None => {
167            return DidResolutionResult::err(
168                DidResolutionError::NotFound,
169                format!("DID document not found for zone DID: {did}"),
170            );
171        }
172    };
173
174    let doc: DidDocument = match serde_json::from_slice(&raw) {
175        Ok(d) => d,
176        Err(e) => {
177            return DidResolutionResult::err(
178                DidResolutionError::InvalidDidDocument,
179                format!("failed to parse DID document: {e}"),
180            );
181        }
182    };
183
184    // Verify the DID document id matches the requested DID.
185    if doc.id != did {
186        return DidResolutionResult::err(
187            DidResolutionError::InvalidDidDocument,
188            format!(
189                "DID document id '{}' does not match requested DID '{did}'",
190                doc.id
191            ),
192        );
193    }
194
195    // Verify that blake3(pubkey) matches the zone root name component.
196    if let Some(pubkey_bytes) = doc.ed25519_public_key() {
197        let expected_zone = crate::zone::zone_root_from_pubkey(&pubkey_bytes);
198        if expected_zone != zone_root {
199            return DidResolutionResult::err(
200                DidResolutionError::InvalidDidDocument,
201                "zone root name does not match blake3(pubkey) from DID document".to_string(),
202            );
203        }
204    }
205
206    // Check deactivation — if the document itself says deactivated, reflect that.
207    // A zone owner signals deactivation by publishing a document with `alsoKnownAs`
208    // pointing to the successor zone DID and deactivated=true in the metadata.
209    // We can't know from the document alone; the caller is responsible for that.
210
211    DidResolutionResult::ok(doc)
212}