ndn_identity/
ca.rs

1//! [`NdncertCa`] — a full NDNCERT CA that serves requests over the NDN network.
2//!
3//! Uses `ndn-app::Producer` to register under `/<prefix>/CA/*` and dispatches
4//! to [`CaState`](ndn_cert::CaState) for protocol logic.
5
6use std::sync::Arc;
7use std::time::Duration;
8
9use ndn_cert::{CaConfig, CaState, ChallengeHandler, HierarchicalPolicy, NamespacePolicy};
10use ndn_packet::Name;
11use ndn_security::SecurityManager;
12use tracing::{debug, warn};
13
14use crate::{error::IdentityError, identity::NdnIdentity};
15
16/// Builder for [`NdncertCa`].
17pub struct NdncertCaBuilder {
18    prefix: Option<Name>,
19    info: String,
20    identity: Option<Arc<SecurityManager>>,
21    challenges: Vec<Box<dyn ChallengeHandler>>,
22    policy: Box<dyn NamespacePolicy>,
23    default_validity: Duration,
24    max_validity: Duration,
25}
26
27impl NdncertCaBuilder {
28    fn new() -> Self {
29        Self {
30            prefix: None,
31            info: "NDN Certificate Authority".to_string(),
32            identity: None,
33            challenges: Vec::new(),
34            policy: Box::new(HierarchicalPolicy),
35            default_validity: Duration::from_secs(86400), // 24h
36            max_validity: Duration::from_secs(365 * 86400), // 1 year
37        }
38    }
39
40    pub fn name(mut self, prefix: impl AsRef<str>) -> Result<Self, IdentityError> {
41        let name: Name = prefix
42            .as_ref()
43            .parse()
44            .map_err(|_| IdentityError::Name(prefix.as_ref().to_string()))?;
45        self.prefix = Some(name);
46        Ok(self)
47    }
48
49    pub fn info(mut self, info: impl Into<String>) -> Self {
50        self.info = info.into();
51        self
52    }
53
54    pub fn signing_identity(mut self, identity: &NdnIdentity) -> Self {
55        self.identity = Some(identity.manager_arc());
56        self
57    }
58
59    pub fn challenge(mut self, handler: impl ChallengeHandler + 'static) -> Self {
60        self.challenges.push(Box::new(handler));
61        self
62    }
63
64    pub fn policy(mut self, policy: impl NamespacePolicy + 'static) -> Self {
65        self.policy = Box::new(policy);
66        self
67    }
68
69    pub fn cert_lifetime(mut self, d: Duration) -> Self {
70        self.default_validity = d;
71        self
72    }
73
74    pub fn max_cert_lifetime(mut self, d: Duration) -> Self {
75        self.max_validity = d;
76        self
77    }
78
79    pub fn build(self) -> Result<NdncertCa, IdentityError> {
80        let prefix = self
81            .prefix
82            .ok_or_else(|| IdentityError::Name("CA prefix not set".to_string()))?;
83        let manager = self.identity.ok_or(IdentityError::NotEnrolled)?;
84
85        if self.challenges.is_empty() {
86            return Err(IdentityError::Enrollment(
87                "at least one challenge handler is required".to_string(),
88            ));
89        }
90
91        let config = CaConfig {
92            prefix: prefix.clone(),
93            info: self.info,
94            default_validity: self.default_validity,
95            max_validity: self.max_validity,
96            challenges: self.challenges,
97            policy: self.policy,
98        };
99
100        Ok(NdncertCa {
101            state: Arc::new(CaState::new(config, manager)),
102            prefix,
103        })
104    }
105}
106
107/// A running NDNCERT certificate authority.
108///
109/// Serves `/<prefix>/CA/INFO`, `/<prefix>/CA/NEW`, and
110/// `/<prefix>/CA/CHALLENGE/<id>` Interests.
111pub struct NdncertCa {
112    state: Arc<CaState>,
113    prefix: Name,
114}
115
116impl NdncertCa {
117    pub fn builder() -> NdncertCaBuilder {
118        NdncertCaBuilder::new()
119    }
120
121    /// The CA's NDN prefix.
122    pub fn prefix(&self) -> &Name {
123        &self.prefix
124    }
125
126    /// Serve NDNCERT requests using the provided Producer.
127    ///
128    /// This method runs indefinitely (until the Producer is dropped or errors).
129    pub async fn serve(self, producer: ndn_app::Producer) -> Result<(), IdentityError> {
130        let state = self.state.clone();
131        let ca_prefix = self.prefix.clone();
132
133        producer
134            .serve(move |interest, responder| {
135                let state = state.clone();
136                let ca_prefix = ca_prefix.clone();
137                async move {
138                    if let Some(wire) = handle_interest(&state, &ca_prefix, interest).await {
139                        responder.respond_bytes(wire).await.ok();
140                    }
141                }
142            })
143            .await?;
144
145        Ok(())
146    }
147}
148
149async fn handle_interest(
150    state: &CaState,
151    ca_prefix: &Name,
152    interest: ndn_packet::Interest,
153) -> Option<bytes::Bytes> {
154    let name = &*interest.name;
155    let name_str = name.to_string();
156    let ca_prefix_str = ca_prefix.to_string();
157
158    debug!(name = %name_str, "NDNCERT: received Interest");
159
160    let suffix = name_str.strip_prefix(&ca_prefix_str).unwrap_or(&name_str);
161
162    if suffix == "/CA/INFO" || suffix.ends_with("/CA/INFO") {
163        let body = state.handle_info();
164        return Some(bytes::Bytes::from(body));
165    }
166
167    if suffix.contains("/CA/NEW") {
168        let body = interest.app_parameters().cloned().unwrap_or_default();
169        match state.handle_new(&body).await {
170            Ok(resp) => return Some(bytes::Bytes::from(resp)),
171            Err(e) => {
172                warn!(error = %e, "NDNCERT NEW failed");
173                return None;
174            }
175        }
176    }
177
178    if suffix.contains("/CA/CHALLENGE/") {
179        // Extract request ID from the last name component.
180        let request_id = name
181            .components()
182            .last()
183            .and_then(|c| std::str::from_utf8(&c.value).ok())
184            .map(|s| s.to_string())?;
185
186        let body = interest.app_parameters().cloned().unwrap_or_default();
187        match state.handle_challenge(&request_id, &body).await {
188            Ok(resp) => return Some(bytes::Bytes::from(resp)),
189            Err(e) => {
190                warn!(error = %e, "NDNCERT CHALLENGE failed");
191                return None;
192            }
193        }
194    }
195
196    warn!(name = %name_str, "NDNCERT: unrecognised Interest");
197    None
198}