Skip to main content

cratestack_cose/keys/
registry_resolver.rs

1//! A thread-safe [`CoseVerifierResolver`] whose keys change at run time.
2
3use std::collections::HashMap;
4use std::sync::{PoisonError, RwLock};
5
6use cratestack_core::CratestackError;
7
8use super::traits::CoseVerifierResolver;
9use super::verify_key::CoseVerifyKey;
10use crate::alg::CoseAlg;
11use crate::thumbprint::KID_LEN;
12
13#[derive(Debug, Default)]
14struct Keys {
15    by_kid: HashMap<[u8; KID_LEN], Vec<CoseVerifyKey>>,
16    len: usize,
17}
18
19/// A set of verification keys that can be registered and revoked while the
20/// server runs: the resolver for devices or peers that enrol after start-up
21/// (the immutable [`StaticVerifierResolver`](super::StaticVerifierResolver)
22/// is fixed at construction). Holds every key type the verifier accepts:
23/// Ed25519, ESP256 and the two HMAC algorithms.
24///
25/// Share it as an `Arc`: hand a clone to the envelope (an `Arc<dyn
26/// CoseVerifierResolver>`) and keep one for [`register`](Self::register) and
27/// [`revoke`](Self::revoke).
28///
29/// ```
30/// use std::sync::Arc;
31/// use cratestack_cose::{
32///     CoseAlg, CoseVerifierResolver, Ed25519Signer, RegistryVerifierResolver,
33/// };
34///
35/// # let rt = tokio::runtime::Builder::new_current_thread().build().unwrap();
36/// # rt.block_on(async {
37/// let registry = Arc::new(RegistryVerifierResolver::new());
38/// let for_envelope: Arc<dyn CoseVerifierResolver> = registry.clone();
39///
40/// let key = Ed25519Signer::from_seed(&[7; 32]).verify_key();
41/// assert!(for_envelope.resolve(&key.kid(), CoseAlg::Ed25519).await?.is_empty());
42///
43/// let kid = registry.register(key.clone())?; // a device enrols
44/// assert_eq!(kid, key.kid());
45/// assert_eq!(for_envelope.resolve(&kid, CoseAlg::Ed25519).await?, vec![key]);
46///
47/// assert_eq!(registry.revoke(&kid), 1); // and is cut off
48/// assert!(for_envelope.resolve(&kid, CoseAlg::Ed25519).await?.is_empty());
49/// # Ok::<(), cratestack_core::CratestackError>(())
50/// # }).unwrap();
51/// ```
52///
53/// **The `kid`** is the first 8 bytes of the key's RFC 9679 thumbprint,
54/// computed from the key (so it cannot be filed under another). Keys with
55/// the same `kid` (an 8-byte collision, or one HMAC secret registered for
56/// both HMAC algorithms) are all kept, and all returned for it.
57///
58/// **Concurrency.** Every call takes one short lock and none is held across
59/// an `.await`. A `register` or `revoke` is atomic: a concurrent
60/// [`resolve`](CoseVerifierResolver::resolve) sees the registry wholly
61/// before it or wholly after. A request whose `resolve` already returned the
62/// key still finishes verifying with it, so a revocation takes effect for
63/// requests that resolve after it returns, not for ones already in flight.
64/// A `register` that returned is visible to every later `resolve`.
65///
66/// Unknown `kid`s resolve to `Ok(vec![])`, never `Err`, as the trait
67/// requires. State is per process: replicas each need the same
68/// registrations.
69#[derive(Debug, Default)]
70pub struct RegistryVerifierResolver {
71    keys: RwLock<Keys>,
72    max_keys: Option<usize>,
73}
74
75impl RegistryVerifierResolver {
76    /// An empty registry with no bound on its size.
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// An empty registry holding at most `max_keys` keys. A server that
82    /// registers keys on behalf of callers uses this to bound memory.
83    ///
84    /// ```
85    /// use cratestack_cose::{Ed25519Signer, RegistryVerifierResolver};
86    ///
87    /// let registry = RegistryVerifierResolver::with_max_keys(1);
88    /// registry.register(Ed25519Signer::from_seed(&[1; 32]).verify_key())?;
89    /// assert!(registry
90    ///     .register(Ed25519Signer::from_seed(&[2; 32]).verify_key())
91    ///     .is_err());
92    /// # Ok::<(), cratestack_core::CratestackError>(())
93    /// ```
94    pub fn with_max_keys(max_keys: usize) -> Self {
95        Self {
96            max_keys: Some(max_keys),
97            ..Self::default()
98        }
99    }
100
101    /// Register `key` and return its `kid`. Registering a key that is
102    /// already present changes nothing and succeeds, even when the registry
103    /// is full. Fails with `CratestackError::Conflict` when a new key would
104    /// exceed the bound; the registry is then unchanged.
105    pub fn register(&self, key: CoseVerifyKey) -> Result<[u8; KID_LEN], CratestackError> {
106        let kid = key.kid();
107        let mut keys = self.keys.write().unwrap_or_else(PoisonError::into_inner);
108        if keys
109            .by_kid
110            .get(&kid)
111            .is_some_and(|held| held.contains(&key))
112        {
113            return Ok(kid);
114        }
115        if self.max_keys.is_some_and(|max| keys.len >= max) {
116            return Err(CratestackError::Conflict(
117                "the verification key registry is full".to_owned(),
118            ));
119        }
120        keys.by_kid.entry(kid).or_default().push(key);
121        keys.len += 1;
122        Ok(kid)
123    }
124
125    /// Remove every key filed under `kid` and return how many there were
126    /// (`0` for an unknown or malformed `kid`: revoking twice is harmless).
127    /// One HMAC secret registered for both algorithms goes with it, and so
128    /// does any other key whose 8-byte `kid` collides (an unrelated
129    /// device). To revoke one device, use [`revoke_key`](Self::revoke_key).
130    pub fn revoke(&self, kid: &[u8]) -> usize {
131        let Ok(kid) = <[u8; KID_LEN]>::try_from(kid) else {
132            return 0;
133        };
134        let mut keys = self.keys.write().unwrap_or_else(PoisonError::into_inner);
135        let removed = keys.by_kid.remove(&kid).map_or(0, |held| held.len());
136        keys.len -= removed;
137        removed
138    }
139
140    /// Remove exactly `key` and nothing else that shares its `kid`. `true`
141    /// if it was registered.
142    pub fn revoke_key(&self, key: &CoseVerifyKey) -> bool {
143        let kid = key.kid();
144        let mut keys = self.keys.write().unwrap_or_else(PoisonError::into_inner);
145        let Some(held) = keys.by_kid.get_mut(&kid) else {
146            return false;
147        };
148        let Some(at) = held.iter().position(|candidate| candidate == key) else {
149            return false;
150        };
151        held.remove(at);
152        if held.is_empty() {
153            keys.by_kid.remove(&kid);
154        }
155        keys.len -= 1;
156        true
157    }
158
159    /// The number of registered keys.
160    pub fn len(&self) -> usize {
161        self.keys.read().unwrap_or_else(PoisonError::into_inner).len
162    }
163
164    /// Whether no key is registered.
165    pub fn is_empty(&self) -> bool {
166        self.len() == 0
167    }
168}
169
170#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
171#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
172impl CoseVerifierResolver for RegistryVerifierResolver {
173    async fn resolve(
174        &self,
175        kid: &[u8],
176        alg: CoseAlg,
177    ) -> Result<Vec<CoseVerifyKey>, CratestackError> {
178        let Ok(kid) = <[u8; KID_LEN]>::try_from(kid) else {
179            return Ok(Vec::new());
180        };
181        let keys = self.keys.read().unwrap_or_else(PoisonError::into_inner);
182        Ok(keys
183            .by_kid
184            .get(&kid)
185            .into_iter()
186            .flatten()
187            .filter(|key| key.supports(alg))
188            .cloned()
189            .collect())
190    }
191}