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}