1use base64::Engine;
4use ndn_cert::EnrollmentSession;
5use ndn_packet::{Name, encode::InterestBuilder};
6use ndn_security::SecurityManager;
7
8use crate::{error::IdentityError, identity::NdnIdentity};
9
10#[derive(Debug, Clone)]
12pub enum ChallengeParams {
13 Token { token: String },
15 Possession {
17 cert_name: String,
18 signature: Vec<u8>,
20 },
21 Raw(serde_json::Map<String, serde_json::Value>),
23}
24
25impl ChallengeParams {
26 pub fn challenge_type(&self) -> &str {
27 match self {
28 ChallengeParams::Token { .. } => "token",
29 ChallengeParams::Possession { .. } => "possession",
30 ChallengeParams::Raw(_) => "raw",
31 }
32 }
33
34 pub fn to_map(&self) -> serde_json::Map<String, serde_json::Value> {
35 match self {
36 ChallengeParams::Token { token } => {
37 let mut m = serde_json::Map::new();
38 m.insert("token".to_string(), token.clone().into());
39 m
40 }
41 ChallengeParams::Possession {
42 cert_name,
43 signature,
44 } => {
45 let mut m = serde_json::Map::new();
46 m.insert("cert_name".to_string(), cert_name.clone().into());
47 m.insert(
48 "signature".to_string(),
49 base64::engine::general_purpose::URL_SAFE_NO_PAD
50 .encode(signature)
51 .into(),
52 );
53 m
54 }
55 ChallengeParams::Raw(map) => map.clone(),
56 }
57 }
58}
59
60pub struct EnrollConfig {
62 pub name: Name,
64 pub ca_prefix: Name,
66 pub validity_secs: u64,
68 pub challenge: ChallengeParams,
70 pub storage: Option<std::path::PathBuf>,
72}
73
74pub async fn run_enrollment(config: EnrollConfig) -> Result<NdnIdentity, IdentityError> {
79 let manager = SecurityManager::new();
80
81 let key_name = manager.generate_ed25519(config.name.clone())?;
83 let signer = manager.get_signer_sync(&key_name)?;
84 let pubkey = signer
85 .public_key()
86 .ok_or_else(|| IdentityError::Enrollment("signer has no public key".to_string()))?;
87
88 let mut session =
90 EnrollmentSession::new(config.name.clone(), pubkey.to_vec(), config.validity_secs);
91
92 let new_body = session.new_request_body()?;
93
94 let _ = new_body;
99
100 Err(IdentityError::Enrollment(
101 "direct enrollment requires a connected CA; use NdncertClient for network enrollment"
102 .to_string(),
103 ))
104}
105
106pub struct NdncertClient {
108 consumer: ndn_app::Consumer,
109 ca_prefix: Name,
110}
111
112impl NdncertClient {
113 pub fn new(consumer: ndn_app::Consumer, ca_prefix: Name) -> Self {
114 Self {
115 consumer,
116 ca_prefix,
117 }
118 }
119
120 pub async fn fetch_ca_profile(&mut self) -> Result<ndn_cert::CaProfile, IdentityError> {
122 let info_name = self.ca_prefix.clone().append("CA").append("INFO");
123 let data = self.consumer.fetch(info_name).await?;
124 let content = data.content().ok_or_else(|| {
125 IdentityError::Enrollment("CA INFO response has no content".to_string())
126 })?;
127 let profile: ndn_cert::CaProfile = serde_json::from_slice(content)
128 .map_err(|e| IdentityError::Enrollment(e.to_string()))?;
129 Ok(profile)
130 }
131
132 pub async fn enroll(
134 &mut self,
135 name: Name,
136 public_key: Vec<u8>,
137 validity_secs: u64,
138 challenge: ChallengeParams,
139 ) -> Result<ndn_security::Certificate, IdentityError> {
140 let mut session = EnrollmentSession::new(name.clone(), public_key, validity_secs);
141
142 let new_body = session.new_request_body()?;
144 let new_name = self
145 .ca_prefix
146 .clone()
147 .append("CA")
148 .append("NEW")
149 .append_version(now_ms());
150
151 let new_data = self
152 .consumer
153 .fetch_with(InterestBuilder::new(new_name).app_parameters(new_body))
154 .await?;
155 let new_content = new_data
156 .content()
157 .ok_or_else(|| IdentityError::Enrollment("NEW response has no content".to_string()))?;
158 session.handle_new_response(new_content)?;
159
160 let request_id = session
161 .request_id()
162 .ok_or_else(|| IdentityError::Enrollment("no request_id from CA".to_string()))?
163 .to_string();
164
165 let challenge_type = challenge.challenge_type().to_string();
167 let params = challenge.to_map();
168 let challenge_body = session.challenge_request_body(&challenge_type, params)?;
169 let challenge_name = self
170 .ca_prefix
171 .clone()
172 .append("CA")
173 .append("CHALLENGE")
174 .append(&request_id);
175
176 let challenge_data = self
177 .consumer
178 .fetch_with(InterestBuilder::new(challenge_name).app_parameters(challenge_body))
179 .await?;
180 let challenge_content = challenge_data.content().ok_or_else(|| {
181 IdentityError::Enrollment("CHALLENGE response has no content".to_string())
182 })?;
183 session.handle_challenge_response(challenge_content)?;
184
185 session
186 .into_certificate()
187 .ok_or_else(|| IdentityError::Enrollment("no certificate returned".to_string()))
188 }
189}
190
191fn now_ms() -> u64 {
192 use std::time::{SystemTime, UNIX_EPOCH};
193 SystemTime::now()
194 .duration_since(UNIX_EPOCH)
195 .unwrap_or_default()
196 .as_millis() as u64
197}