ndn_cert/challenge/
token.rs

1//! Token challenge — client proves identity by submitting a pre-provisioned
2//! one-time token.
3//!
4//! Tokens are issued out-of-band (e.g. at factory time, via a management UI)
5//! and are consumed on successful use. This is the primary challenge type for
6//! fleet zero-touch provisioning.
7
8use std::{future::Future, pin::Pin, sync::Arc};
9
10use dashmap::DashMap;
11
12use crate::{
13    challenge::{ChallengeHandler, ChallengeOutcome, ChallengeState},
14    error::CertError,
15    protocol::CertRequest,
16};
17
18/// Thread-safe store of valid one-time enrollment tokens.
19#[derive(Default, Clone)]
20pub struct TokenStore {
21    tokens: Arc<DashMap<String, ()>>,
22}
23
24impl TokenStore {
25    pub fn new() -> Self {
26        Self::default()
27    }
28
29    /// Add a token to the store.
30    pub fn add(&self, token: impl Into<String>) {
31        self.tokens.insert(token.into(), ());
32    }
33
34    /// Add multiple tokens.
35    pub fn add_many(&self, tokens: impl IntoIterator<Item = impl Into<String>>) {
36        for t in tokens {
37            self.add(t);
38        }
39    }
40
41    /// Check if a token exists and consume it (one-time use).
42    pub fn consume(&self, token: &str) -> bool {
43        self.tokens.remove(token).is_some()
44    }
45
46    /// Number of remaining tokens.
47    pub fn len(&self) -> usize {
48        self.tokens.len()
49    }
50
51    pub fn is_empty(&self) -> bool {
52        self.tokens.is_empty()
53    }
54}
55
56/// NDNCERT token challenge handler.
57pub struct TokenChallenge {
58    store: TokenStore,
59}
60
61impl TokenChallenge {
62    pub fn new(store: TokenStore) -> Self {
63        Self { store }
64    }
65}
66
67impl ChallengeHandler for TokenChallenge {
68    fn challenge_type(&self) -> &'static str {
69        "token"
70    }
71
72    fn begin<'a>(
73        &'a self,
74        _req: &'a CertRequest,
75    ) -> Pin<Box<dyn Future<Output = Result<ChallengeState, CertError>> + Send + 'a>> {
76        Box::pin(async move {
77            Ok(ChallengeState {
78                challenge_type: "token".to_string(),
79                data: serde_json::Value::Null,
80            })
81        })
82    }
83
84    fn verify<'a>(
85        &'a self,
86        _state: &'a ChallengeState,
87        parameters: &'a serde_json::Map<String, serde_json::Value>,
88    ) -> Pin<Box<dyn Future<Output = Result<ChallengeOutcome, CertError>> + Send + 'a>> {
89        let token = parameters
90            .get("token")
91            .and_then(|v| v.as_str())
92            .map(str::to_string);
93        Box::pin(async move {
94            match token {
95                None => Ok(ChallengeOutcome::Denied(
96                    "missing 'token' parameter".to_string(),
97                )),
98                Some(t) => {
99                    if self.store.consume(&t) {
100                        Ok(ChallengeOutcome::Approved)
101                    } else {
102                        Ok(ChallengeOutcome::Denied(
103                            "invalid or expired token".to_string(),
104                        ))
105                    }
106                }
107            }
108        })
109    }
110}