cratestack_cose/auth/signer_provider.rs
1//! [`CoseAuthProvider`]: a COSE-verified signer becomes an authenticated
2//! principal (cratestack#1077; ADR 0006 §12).
3//!
4//! A signed client sends no `Authorization` header, so the header-based
5//! provider has nothing to authenticate. This adapter is the first-success
6//! chain `first_success(inner).or(CoseSignerProvider)` (ADR 0021 §5,
7//! cratestack#1243): the wrapped provider, then the signer the envelope layer
8//! verified, which [`CoseSignerProvider`] looks up in the registries.
9//!
10//! The rules, in order (the chain's, applied to these two members):
11//!
12//! 1. The wrapped provider runs first. An authenticated answer (a valid
13//! `Authorization` header) is returned as is, and the registries are not
14//! consulted. An `Err` from it is returned as is: a header the inner
15//! provider *refuses* is never replaced by the signer. That holds only for
16//! an `Err`. A header the inner provider *accepts* but maps to anonymous
17//! (for example `SignedRequestAuthProvider` under
18//! `TransportCallerMode::Never`, for a key-only header) is not a refusal,
19//! and the signer supersedes it.
20//! 2. With no verified signer in the request's extensions, the answer is the
21//! wrapped provider's, which is anonymous. A signer is only ever read from
22//! the extensions. The envelope layer inserts it there after the seal
23//! verified, and nothing on the wire can set an extension, so a header or a
24//! body cannot fake one.
25//! 3. A verified signer is authenticated only if a registry still holds its
26//! key, matched by full thumbprint. The service registry is asked first,
27//! then, **only if device authentication is switched on**, the device
28//! registry. That is the order `cratestack-auth`'s verifier uses, so a
29//! device enrolment can never relabel a service key and a device registry
30//! outage does not fail a service signer. Unlike that verifier, which
31//! treats a JWKS/service error as a miss, a service-registry error here is
32//! NOT a miss: it fails the request (`500`), including for device signers.
33//! This fails closed. An unknown or revoked key, or a device key while the
34//! gate is off, gets the wrapped provider's anonymous answer, so policy
35//! refuses it. A backend failure of a registry that is asked is an `Err`.
36//!
37//! # Device authentication is opt-in
38//!
39//! A device key proves a device, not a user. Like `TransportCallerMode::Never`
40//! in `cratestack-auth`, this adapter does not turn a key alone into a caller
41//! unless told to. By default a service key authenticates (services are
42//! machine principals by design) and a device key does not: it resolves to
43//! anonymous, without a lookup in the device registry. A failing service
44//! registry still errors (`500`), as it is asked first.
45//! [`CoseAuthProvider::authenticate_devices`] switches device keys on.
46//!
47//! # Security
48//!
49//! With device authentication **on**, wrapping a provider with
50//! [`CoseAuthProvider`] makes **any request sealed by an enrolled device key
51//! authenticated**, with no user (no `id_jwt`), on every method, whatever the
52//! inner provider's `TransportCallerMode` says. `auth() != null` then means
53//! "holds an enrolled device or service key". It no longer means "a user
54//! signed in". What bounds access is revocation of the key, not token expiry:
55//! a device key stays good until it is removed from the registry. Turn it on
56//! only where device enrolment itself is gated, since whoever can enrol a key
57//! can then pass every `auth() != null` rule. A rule that needs a signed-in
58//! user cannot rely on `auth() != null` alone.
59//!
60//! Even with the gate off, a service key makes `auth() != null` true for the
61//! holder of that key. So [`CoseAuthProvider::with_service_keys`] must hold
62//! service keys only: a device key placed there authenticates as a service
63//! whatever the gate, because the service registry is asked first.
64//!
65//! The envelope's "Required" mode does not bind a header identity to the
66//! signer. When the inner provider authenticates from a header, that identity
67//! wins and the signer is not consulted, so a request may be sealed by one
68//! device and act as the user in another's header.
69//!
70//! # The principal
71//!
72//! The context carries `id` (`cose:<thumbprint>`, the default
73//! `ThumbprintPrincipal` form; with another prefix the two differ), `kind`
74//! (`device` or `service`), `thumbprint` (bare lowercase hex) and `alg` (the
75//! COSE algorithm id, an `Int`).
76
77use std::fmt;
78use std::sync::Arc;
79
80use cratestack_auth::DeviceKeyResolver;
81use cratestack_core::{
82 AuthProvider, CratestackContext, CratestackError, Credential, RequestContext, first_success,
83};
84
85use super::signer_member::CoseSignerProvider;
86use crate::keys::CoseVerifierResolver;
87
88/// Wraps an [`AuthProvider`] and authenticates a COSE-verified signer through
89/// a service key registry and, once switched on, the device key registry.
90///
91/// Each registry must be the one the envelope verifies with, or stricter, so
92/// revoking a key there revokes it here. But `with_service_keys` must hold
93/// service keys only (see "Security"): do not pass it a resolver that also
94/// holds device keys.
95/// The service registry is asked before the device registry, and the device
96/// registry only after [`authenticate_devices`](Self::authenticate_devices).
97///
98/// ```text
99/// // Services only (the default): a device key is anonymous.
100/// let provider = CoseAuthProvider::new(header_provider, device_registry)
101/// .with_service_keys(service_registry);
102/// // Also devices, where device enrolment is gated.
103/// let provider = provider.authenticate_devices(true);
104/// ```
105///
106/// # Security
107///
108/// A device key proves a device, not a user, so device authentication is off by
109/// default and a device signer is anonymous. With
110/// [`authenticate_devices(true)`](Self::authenticate_devices), any request
111/// sealed by an enrolled device key is authenticated, with no user (no
112/// `id_jwt`), on every method, regardless of the inner provider's
113/// `TransportCallerMode`. `auth() != null` then means "holds an enrolled device
114/// or service key", and revoking the key, not token expiry, is what bounds
115/// access. Turn it on only where device enrolment itself is gated. A service
116/// key authenticates either way. See the module docs.
117///
118/// `with_service_keys` must hold service keys only; a device key placed there
119/// authenticates as a service whatever the gate. The service registry is asked
120/// first and authenticates regardless of `authenticate_devices`, so passing it
121/// the resolver devices enrol into (for example the same
122/// `RegistryVerifierResolver` the envelope verifies with) makes every enrolled
123/// device a `kind: service` caller with the gate off.
124///
125/// An ESP256 or WebCrypto device key can authenticate only through the
126/// service registry (not behind the device gate), because `DeviceKeyResolver`
127/// is Ed25519-only.
128///
129/// The `id` is `cose:<thumbprint>` only with the default `ThumbprintPrincipal`
130/// prefix. The context also carries `kind`, `thumbprint` and `alg`.
131#[derive(Clone)]
132pub struct CoseAuthProvider<A> {
133 inner: A,
134 signer: CoseSignerProvider,
135}
136
137impl<A> CoseAuthProvider<A> {
138 /// Wrap `inner`. `devices`, a [`DeviceKeyResolver`] (its thumbprint lookup
139 /// is the one used), is consulted only after
140 /// [`authenticate_devices(true)`](Self::authenticate_devices).
141 pub fn new(inner: A, devices: Arc<dyn DeviceKeyResolver>) -> Self {
142 Self {
143 inner,
144 signer: CoseSignerProvider::new(devices),
145 }
146 }
147
148 /// Switch device authentication on or off (off by default).
149 ///
150 /// Off, a verified device signer is anonymous: the device registry is not
151 /// asked and policy refuses it (a failing service registry, asked first,
152 /// still errors with `500`). On, a
153 /// device key the registry still holds authenticates as `kind` `device`
154 /// (see the type docs, "Security": this makes the key alone a caller).
155 /// Service keys ([`with_service_keys`](Self::with_service_keys)) are not
156 /// affected.
157 pub fn authenticate_devices(mut self, enabled: bool) -> Self {
158 self.signer = self.signer.authenticate_devices(enabled);
159 self
160 }
161
162 /// Also authenticate verified service keys, which `services` holds under
163 /// any algorithm it verifies (for example a `RegistryVerifierResolver`).
164 /// `services` must hold service keys only: a device key here authenticates
165 /// as a service whatever the gate.
166 pub fn with_service_keys(mut self, services: Arc<dyn CoseVerifierResolver>) -> Self {
167 self.signer = self.signer.with_service_keys(services);
168 self
169 }
170}
171
172impl<A: fmt::Debug> fmt::Debug for CoseAuthProvider<A> {
173 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174 f.debug_struct("CoseAuthProvider")
175 .field("inner", &self.inner)
176 .finish_non_exhaustive()
177 }
178}
179
180impl<A: AuthProvider> AuthProvider for CoseAuthProvider<A> {
181 type Error = CratestackError;
182
183 async fn authenticate(
184 &self,
185 request: &RequestContext<'_>,
186 ) -> Result<CratestackContext, CratestackError> {
187 // The members are cheap to clone (`Arc`s and the inner provider's own
188 // `Clone`); the chain is the public combinator, not a private path.
189 first_success(self.inner.clone())
190 .or(self.signer.clone())
191 .authenticate(request)
192 .await
193 }
194
195 fn credential<'r>(&self, request: &RequestContext<'r>) -> Credential<'r> {
196 first_success(self.inner.clone())
197 .or(self.signer.clone())
198 .credential(request)
199 }
200}