ndn_cert/challenge/
possession.rs

1//! Possession challenge — client proves they already hold a trusted certificate
2//! by signing the request ID with their existing key.
3//!
4//! This is used for:
5//! - Certificate renewal (prove you hold the expiring cert)
6//! - Device sub-namespace enrollment (vehicle proves it holds the fleet cert
7//!   before getting an ECU cert)
8//! - Zero-touch provisioning when a factory key is pre-installed
9
10use std::{future::Future, pin::Pin, sync::Arc};
11
12use base64::Engine;
13use ndn_security::{Certificate, Ed25519Verifier, Verifier, VerifyOutcome};
14
15use crate::{
16    challenge::{ChallengeHandler, ChallengeOutcome, ChallengeState},
17    error::CertError,
18    protocol::CertRequest,
19};
20
21/// NDNCERT possession challenge handler.
22///
23/// The client must:
24/// 1. Sign the `request_id` bytes with the private key corresponding to a
25///    certificate in `trusted_certs`.
26/// 2. Submit the certificate name and the base64url-encoded signature as
27///    `cert_name` and `signature` parameters.
28pub struct PossessionChallenge {
29    trusted_certs: Arc<Vec<Certificate>>,
30}
31
32impl PossessionChallenge {
33    /// Create a challenge that accepts possession of any cert in `trusted_certs`.
34    pub fn new(trusted_certs: Vec<Certificate>) -> Self {
35        Self {
36            trusted_certs: Arc::new(trusted_certs),
37        }
38    }
39}
40
41impl ChallengeHandler for PossessionChallenge {
42    fn challenge_type(&self) -> &'static str {
43        "possession"
44    }
45
46    fn begin<'a>(
47        &'a self,
48        req: &'a CertRequest,
49    ) -> Pin<Box<dyn Future<Output = Result<ChallengeState, CertError>> + Send + 'a>> {
50        // Store the request name as the nonce to be signed
51        let nonce = req.name.clone();
52        Box::pin(async move {
53            Ok(ChallengeState {
54                challenge_type: "possession".to_string(),
55                data: serde_json::json!({ "nonce": nonce }),
56            })
57        })
58    }
59
60    fn verify<'a>(
61        &'a self,
62        state: &'a ChallengeState,
63        parameters: &'a serde_json::Map<String, serde_json::Value>,
64    ) -> Pin<Box<dyn Future<Output = Result<ChallengeOutcome, CertError>> + Send + 'a>> {
65        let cert_name_str = parameters
66            .get("cert_name")
67            .and_then(|v| v.as_str())
68            .map(str::to_string);
69        let signature_b64 = parameters
70            .get("signature")
71            .and_then(|v| v.as_str())
72            .map(str::to_string);
73        let nonce = state
74            .data
75            .get("nonce")
76            .and_then(|v| v.as_str())
77            .unwrap_or("")
78            .to_string();
79        let trusted = self.trusted_certs.clone();
80
81        Box::pin(async move {
82            let cert_name_str = cert_name_str
83                .ok_or_else(|| CertError::InvalidRequest("missing 'cert_name'".to_string()))?;
84            let signature_b64 = signature_b64
85                .ok_or_else(|| CertError::InvalidRequest("missing 'signature'".to_string()))?;
86
87            let sig_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
88                .decode(&signature_b64)
89                .map_err(|_| CertError::InvalidRequest("invalid base64 signature".to_string()))?;
90
91            // Find the matching cert in our trust list
92            let cert = trusted.iter().find(|c| c.name.to_string() == cert_name_str);
93            let cert = match cert {
94                Some(c) => c,
95                None => {
96                    return Ok(ChallengeOutcome::Denied(format!(
97                        "certificate not trusted: {cert_name_str}"
98                    )));
99                }
100            };
101
102            // Verify: client signed the nonce with their cert's private key.
103            // Ed25519Verifier::verify(region, sig_value, public_key)
104            let outcome = Ed25519Verifier
105                .verify(nonce.as_bytes(), &sig_bytes, &cert.public_key)
106                .await
107                .map_err(CertError::Security)?;
108
109            match outcome {
110                VerifyOutcome::Valid => Ok(ChallengeOutcome::Approved),
111                VerifyOutcome::Invalid => Ok(ChallengeOutcome::Denied(
112                    "signature verification failed".to_string(),
113                )),
114            }
115        })
116    }
117}