Skip to main content

attested_request/
token.rs

1//! Attestation Gateway integrity tokens: the JWT that binds a device key to an attested app.
2//!
3//! Verification accepts ES256 and nothing else. The algorithm is pinned here rather than read
4//! from the token or the JWKS, so neither can choose how the token is checked.
5
6use std::{
7    collections::HashMap,
8    error::Error,
9    future::Future,
10    pin::Pin,
11    sync::Arc,
12    time::{Duration, SystemTime, UNIX_EPOCH},
13};
14
15use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode, decode_header};
16use p256::ecdsa::VerifyingKey;
17use serde::Deserialize;
18
19use crate::{
20    device::{DeviceKey, DeviceKeyError, EcJwk},
21    profile::Platform,
22};
23
24const TOKEN_ALG: &str = "ES256";
25
26/// A boxed, sendable future.
27pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
28
29/// An issuer's token signing keys, looked up by `kid`.
30pub trait IssuerKeys: Send + Sync {
31    /// Resolves `kid`. Returns `Ok(None)` when the issuer has no such key and
32    /// [`KeysUnavailable`] when the keys cannot be obtained at all.
33    fn key<'a>(
34        &'a self,
35        kid: &'a str,
36    ) -> BoxFuture<'a, Result<Option<VerifyingKey>, KeysUnavailable>>;
37}
38
39/// The issuer's keys could not be obtained. This is a dependency failure, not the client's.
40#[derive(Debug, thiserror::Error)]
41#[error("issuer keys are unavailable")]
42pub struct KeysUnavailable(#[source] pub Box<dyn Error + Send + Sync>);
43
44/// A fixed set of issuer keys.
45#[derive(Debug, Clone, Default)]
46pub struct StaticKeys(HashMap<String, VerifyingKey>);
47
48impl StaticKeys {
49    /// Keys from a JWKS document. See [`parse_jwks`].
50    ///
51    /// # Errors
52    ///
53    /// Returns a [`JwksError`] when the document is not a JWKS.
54    pub fn from_jwks(document: &[u8]) -> Result<Self, JwksError> {
55        parse_jwks(document).map(Self)
56    }
57
58    /// Adds a key under `kid`.
59    #[must_use]
60    pub fn with_key(mut self, kid: impl Into<String>, key: VerifyingKey) -> Self {
61        self.0.insert(kid.into(), key);
62        self
63    }
64}
65
66impl IssuerKeys for StaticKeys {
67    fn key<'a>(
68        &'a self,
69        kid: &'a str,
70    ) -> BoxFuture<'a, Result<Option<VerifyingKey>, KeysUnavailable>> {
71        Box::pin(async move { Ok(self.0.get(kid).copied()) })
72    }
73}
74
75/// The document is not a JWKS.
76#[derive(Debug, thiserror::Error)]
77#[error("not a JWKS document")]
78pub struct JwksError(#[source] serde_json::Error);
79
80/// Parses the ES256 signing keys of a JWKS document, keyed by `kid`.
81///
82/// Keys without a `kid`, keys other than EC P-256, and keys marked for another algorithm or use
83/// are skipped: a JWKS may legitimately publish them, and no token may be verified with them.
84///
85/// # Errors
86///
87/// Returns a [`JwksError`] when the document is not a JWKS.
88pub fn parse_jwks(document: &[u8]) -> Result<HashMap<String, VerifyingKey>, JwksError> {
89    #[derive(Deserialize)]
90    struct JwkSet {
91        keys: Vec<Jwk>,
92    }
93    #[derive(Deserialize)]
94    struct Jwk {
95        kid: Option<String>,
96        alg: Option<String>,
97        #[serde(rename = "use")]
98        key_use: Option<String>,
99        #[serde(flatten)]
100        ec: Option<EcJwk>,
101    }
102
103    let set: JwkSet = serde_json::from_slice(document).map_err(JwksError)?;
104    Ok(set
105        .keys
106        .into_iter()
107        .filter(|jwk| jwk.alg.as_deref().is_none_or(|alg| alg == TOKEN_ALG))
108        .filter(|jwk| {
109            jwk.key_use
110                .as_deref()
111                .is_none_or(|key_use| key_use == "sig")
112        })
113        .filter_map(|jwk| Some((jwk.kid?, jwk.ec?.to_verifying_key().ok()?)))
114        .collect())
115}
116
117/// Why an integrity token was refused.
118#[derive(Debug, thiserror::Error)]
119pub enum TokenError {
120    /// The token is not a compact JWS with the expected header and claims.
121    #[error("integrity token is malformed")]
122    Malformed,
123    /// The token is not signed with ES256.
124    #[error("integrity token is not ES256")]
125    UnsupportedAlgorithm,
126    /// The token's issuer is not trusted.
127    #[error("integrity token issuer is not trusted")]
128    UntrustedIssuer,
129    /// The issuer has no key with the token's `kid`.
130    #[error("integrity token kid is unknown")]
131    UnknownKey,
132    /// The issuer's keys could not be obtained.
133    #[error(transparent)]
134    KeysUnavailable(#[from] KeysUnavailable),
135    /// The token signature does not verify.
136    #[error("integrity token signature does not verify")]
137    BadSignature,
138    /// The token is expired.
139    #[error("integrity token is expired")]
140    Expired,
141    /// The token is not valid yet.
142    #[error("integrity token is not valid yet")]
143    NotYetValid,
144    /// The token is for another audience.
145    #[error("integrity token audience is not accepted")]
146    WrongAudience,
147    /// The token carries no `pass` verdict.
148    #[error("integrity token has no pass claim")]
149    MissingPass,
150    /// The Attestation Gateway judged the device untrustworthy.
151    #[error("device failed the attestation check")]
152    IntegrityFailed,
153    /// The token's platform is not supported.
154    #[error("integrity token platform is not supported")]
155    UnsupportedPlatform,
156    /// The token's `cnf.jwk` is not a usable device key.
157    #[error(transparent)]
158    DeviceKey(#[from] DeviceKeyError),
159}
160
161/// A device the Attestation Gateway attested, from a verified integrity token.
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub struct AttestedDevice {
164    /// The attested device key, from `cnf.jwk`.
165    pub key: DeviceKey,
166    /// The attested platform.
167    pub platform: Platform,
168    /// The token issuer.
169    pub issuer: String,
170    /// The accepted audience the token was issued for.
171    pub audience: String,
172    /// When the token expires.
173    pub expires_at: SystemTime,
174}
175
176/// Verifies integrity tokens against trusted issuers and accepted audiences.
177#[derive(Clone)]
178pub struct TokenVerifier {
179    /// Trusted issuer key providers.
180    issuers: HashMap<String, Arc<dyn IssuerKeys>>,
181    /// Preference order when a token names multiple accepted audiences.
182    audiences: Vec<String>,
183    /// Fixed JWT algorithm and audience policy.
184    validation: Validation,
185}
186
187/// A [`TokenVerifier`] could not be built from its configuration.
188#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
189pub enum TokenVerifierConfigError {
190    /// No issuer is trusted.
191    #[error("at least one trusted issuer is required")]
192    NoIssuer,
193    /// No audience is accepted.
194    #[error("at least one audience is required")]
195    NoAudience,
196}
197
198/// JWT permits one audience or a list of audiences.
199#[derive(Deserialize)]
200#[serde(untagged)]
201enum Audience {
202    One(String),
203    Many(Vec<String>),
204}
205
206/// Integrity-token claims; Serde preserves the standard JWT wire names.
207#[derive(Deserialize)]
208struct Claims {
209    /// Identifies the configured signing-key provider.
210    #[serde(rename = "iss")]
211    issuer: String,
212    /// Intended recipients, as a string or list.
213    #[serde(rename = "aud")]
214    audience: Audience,
215    /// Exclusive expiry, in Unix seconds.
216    #[serde(rename = "exp")]
217    expires_at: u64,
218    /// Inclusive validity start, in Unix seconds.
219    #[serde(rename = "nbf")]
220    not_before: Option<u64>,
221    /// Optional to distinguish an absent verdict from a failed one.
222    #[serde(rename = "pass")]
223    attestation_passed: Option<bool>,
224    /// Parsed separately to report unsupported platforms explicitly.
225    platform: String,
226    /// Binds the attestation to the device's key.
227    #[serde(rename = "cnf")]
228    confirmation: Confirmation,
229}
230
231/// The device key bound to the attestation.
232#[derive(Deserialize)]
233struct Confirmation {
234    jwk: EcJwk,
235}
236
237impl TokenVerifier {
238    /// A verifier trusting `issuers` (issuer to its keys) and accepting any of `audiences`.
239    ///
240    /// Accepting more than one audience lets an audience be renamed without an outage.
241    ///
242    /// # Errors
243    ///
244    /// Returns a [`TokenVerifierConfigError`] when no issuer or no audience is given.
245    pub fn new(
246        issuers: impl IntoIterator<Item = (String, Arc<dyn IssuerKeys>)>,
247        audiences: impl IntoIterator<Item = String>,
248    ) -> Result<Self, TokenVerifierConfigError> {
249        let issuers: HashMap<_, _> = issuers.into_iter().collect();
250        let audiences: Vec<_> = audiences
251            .into_iter()
252            .filter(|audience| !audience.trim().is_empty())
253            .collect();
254        if issuers.is_empty() {
255            return Err(TokenVerifierConfigError::NoIssuer);
256        }
257        if audiences.is_empty() {
258            return Err(TokenVerifierConfigError::NoAudience);
259        }
260
261        let mut validation = Validation::new(Algorithm::ES256);
262        validation.set_audience(&audiences);
263        // Time is checked against the caller's clock in verify, with no expiry leeway.
264        validation.validate_exp = false;
265        validation.validate_nbf = false;
266
267        Ok(Self {
268            issuers,
269            audiences,
270            validation,
271        })
272    }
273
274    /// Verifies `token` at `now` and returns the device it attests.
275    ///
276    /// Unverified claims are used only to select a trusted issuer and its signing key.
277    ///
278    /// # Errors
279    ///
280    /// Returns a [`TokenError`] describing why the token was refused.
281    pub async fn verify(&self, token: &str, now: SystemTime) -> Result<AttestedDevice, TokenError> {
282        let header = decode_header(token).map_err(token_error)?;
283        if header.alg != Algorithm::ES256 {
284            return Err(TokenError::UnsupportedAlgorithm);
285        }
286        if header.crit.is_some() {
287            return Err(TokenError::Malformed);
288        }
289        let kid = header
290            .kid
291            .filter(|kid| !kid.is_empty())
292            .ok_or(TokenError::Malformed)?;
293
294        // Unverified claims only select a configured issuer; no token URL is ever fetched.
295        let unverified: Claims =
296            jsonwebtoken::dangerous::insecure_decode_claims(token).map_err(token_error)?;
297        let keys = self
298            .issuers
299            .get(&unverified.issuer)
300            .ok_or(TokenError::UntrustedIssuer)?;
301        let issuer_key = keys.key(&kid).await?.ok_or(TokenError::UnknownKey)?;
302        let key = DecodingKey::from_ec_der(issuer_key.to_encoded_point(false).as_bytes());
303        let claims = decode::<Claims>(token, &key, &self.validation)
304            .map_err(token_error)?
305            .claims;
306
307        let now_secs = now
308            .duration_since(UNIX_EPOCH)
309            .map_or(0, |elapsed| elapsed.as_secs());
310        if now_secs >= claims.expires_at {
311            return Err(TokenError::Expired);
312        }
313        if claims.not_before.is_some_and(|start| now_secs < start) {
314            return Err(TokenError::NotYetValid);
315        }
316        let audience = self
317            .accepted_audience(&claims.audience)
318            .ok_or(TokenError::WrongAudience)?;
319        match claims.attestation_passed {
320            None => return Err(TokenError::MissingPass),
321            Some(false) => return Err(TokenError::IntegrityFailed),
322            Some(true) => {}
323        }
324        let platform =
325            Platform::from_claim(&claims.platform).ok_or(TokenError::UnsupportedPlatform)?;
326        let key = DeviceKey::new(claims.confirmation.jwk.to_verifying_key()?);
327
328        Ok(AttestedDevice {
329            key,
330            platform,
331            issuer: claims.issuer,
332            audience: audience.to_owned(),
333            expires_at: UNIX_EPOCH
334                .checked_add(Duration::from_secs(claims.expires_at))
335                .ok_or(TokenError::Malformed)?,
336        })
337    }
338
339    /// Selects the first configured audience present in the verified token.
340    fn accepted_audience(&self, audience: &Audience) -> Option<&str> {
341        let offered: &[String] = match audience {
342            Audience::One(audience) => std::slice::from_ref(audience),
343            Audience::Many(audiences) => audiences,
344        };
345        self.audiences
346            .iter()
347            .find(|accepted| offered.contains(accepted))
348            .map(String::as_str)
349    }
350}
351
352/// Maps JWT failures to the profile's rejection taxonomy.
353fn token_error(error: jsonwebtoken::errors::Error) -> TokenError {
354    use jsonwebtoken::errors::ErrorKind;
355    match error.into_kind() {
356        ErrorKind::InvalidSignature => TokenError::BadSignature,
357        ErrorKind::InvalidAlgorithm
358        | ErrorKind::InvalidAlgorithmName
359        | ErrorKind::UnsupportedAlgorithm => TokenError::UnsupportedAlgorithm,
360        ErrorKind::InvalidIssuer => TokenError::UntrustedIssuer,
361        ErrorKind::InvalidAudience => TokenError::WrongAudience,
362        _ => TokenError::Malformed,
363    }
364}