Skip to main content

cratestack_cose/auth/
device_resolver.rs

1//! `DeviceKeyResolver` -> [`CoseVerifierResolver`].
2
3use std::fmt;
4use std::sync::Arc;
5
6use cratestack_auth::DeviceKeyResolver;
7use cratestack_core::CratestackError;
8
9use crate::alg::CoseAlg;
10use crate::keys::{CoseVerifierResolver, CoseVerifyKey};
11use crate::thumbprint::KID_LEN;
12
13/// Resolves COSE_Sign1 device requests through the service's
14/// `cratestack_auth::DeviceKeyResolver` (ADR 0006 §8), by its required
15/// `lookup_device_verifying_keys_by_thumbprint`.
16///
17/// What it maps, and why:
18///
19/// - **Only Ed25519 (`-19`).** Device keys are Ed25519 keys (the
20///   resolver's type says so). For any other algorithm this returns no
21///   candidate without asking the backend, so an ESP256 or HMAC header can
22///   neither trigger a device-key lookup nor be verified with a device key.
23/// - **Only an 8-byte `kid` is looked up.** The opener never passes
24///   another length (the header parser refuses it), but `resolve` is
25///   public; a caller that passes a shorter prefix would otherwise turn
26///   one lookup into "every key starting with these bytes". Anything but
27///   [`KID_LEN`] bytes gets no candidate and no backend call, which is
28///   what lets `DeviceKeyResolver`'s docs promise implementors exactly 8.
29/// - **Unknown or revoked is `Ok(vec![])`**, which the opener turns into
30///   the coarse `401`, like every other verification failure (§10).
31/// - **A backend `Err` is `CratestackError::Internal`**, so a `500`, and
32///   its text never reaches the peer (`Internal` keeps it out of
33///   `public_message()`). Mapping it to "no key" instead would make an
34///   outage look like an attack in the logs, and look like an unknown key
35///   to the client.
36/// - **No `kid` filtering here.** The opener recomputes every candidate's
37///   `kid` from the key and accepts only the key whose `kid` is the
38///   header's (`open.rs`, step 4), so a registry that returns extra keys
39///   for a prefix cannot let one device sign as another. Filtering here as
40///   well would be a second copy of that check.
41#[derive(Clone)]
42pub struct DeviceKeyCoseResolver {
43    devices: Arc<dyn DeviceKeyResolver>,
44}
45
46impl DeviceKeyCoseResolver {
47    pub fn new(devices: Arc<dyn DeviceKeyResolver>) -> Self {
48        Self { devices }
49    }
50}
51
52impl fmt::Debug for DeviceKeyCoseResolver {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.debug_struct("DeviceKeyCoseResolver")
55            .finish_non_exhaustive()
56    }
57}
58
59#[async_trait::async_trait]
60impl CoseVerifierResolver for DeviceKeyCoseResolver {
61    async fn resolve(
62        &self,
63        kid: &[u8],
64        alg: CoseAlg,
65    ) -> Result<Vec<CoseVerifyKey>, CratestackError> {
66        if alg != CoseAlg::Ed25519 || kid.len() != KID_LEN {
67            return Ok(Vec::new());
68        }
69        let keys = self
70            .devices
71            .lookup_device_verifying_keys_by_thumbprint(kid)
72            .await
73            .map_err(|error| {
74                CratestackError::Internal(format!("device key lookup failed: {error}"))
75            })?;
76        Ok(keys.into_iter().map(CoseVerifyKey::from_ed25519).collect())
77    }
78}