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}