Skip to main content

authkestra_devsig/
config.rs

1//! Verifier configuration.
2
3use std::collections::HashSet;
4use std::time::Duration;
5
6use jsonwebtoken::Algorithm;
7
8/// Static configuration for [`crate::verify`].
9///
10/// `alg` never comes from either token — it is always looked up here. Symmetric algorithms
11/// (`HS256`/`HS384`/`HS512`) must never appear in `allowed_algs`: [`VerifierConfig::new`] filters
12/// them out defensively, and `verify()` *also* hard-rejects the whole HMAC family unconditionally
13/// regardless of what a caller manages to put in `allowed_algs`. This scheme's private key never
14/// leaves the device, so there is no symmetric secret the verifier could ever legitimately share
15/// with a signer — belt and braces, not a suggestion.
16#[derive(Debug, Clone)]
17pub struct VerifierConfig {
18    /// Attestation `iss` values this verifier trusts.
19    pub trusted_issuers: HashSet<String>,
20    /// Algorithms accepted for both the request signature and the attestation. Must be
21    /// asymmetric only — see the type-level docs above.
22    pub allowed_algs: Vec<Algorithm>,
23    /// Clock skew tolerance applied to both the attestation's and the signature's freshness
24    /// windows.
25    pub max_clock_skew: Duration,
26    /// Upper bound on `sig.exp - sig.iat`. Rejects long-lived signatures outright even if `exp`
27    /// itself is still in the future — a short-lived signing key policy is only meaningful if
28    /// the lifetime is bounded independently of the clock.
29    pub max_signature_lifetime: Duration,
30    /// The audience this verifier expects requests to be signed for (`sig.aud`). Prevents a
31    /// signature captured for one service being replayed against another.
32    pub audience: String,
33}
34
35impl VerifierConfig {
36    /// Builds a config, silently dropping any symmetric algorithm from `allowed_algs`.
37    ///
38    /// Dropping rather than erroring keeps this ergonomic for the common case (a caller passes
39    /// `[Algorithm::ES256]` and never has to think about it) while still making the
40    /// "asymmetric only" rule impossible to defeat by misconfiguration — the algorithm-family
41    /// check inside `verify()` is the actual enforcement point either way.
42    pub fn new(
43        trusted_issuers: impl IntoIterator<Item = impl Into<String>>,
44        allowed_algs: impl IntoIterator<Item = Algorithm>,
45        max_clock_skew: Duration,
46        max_signature_lifetime: Duration,
47        audience: impl Into<String>,
48    ) -> Self {
49        let allowed_algs: Vec<Algorithm> = allowed_algs
50            .into_iter()
51            .filter(|alg| !is_symmetric(*alg))
52            .collect();
53
54        Self {
55            trusted_issuers: trusted_issuers.into_iter().map(Into::into).collect(),
56            allowed_algs,
57            max_clock_skew,
58            max_signature_lifetime,
59            audience: audience.into(),
60        }
61    }
62}
63
64pub(crate) fn is_symmetric(alg: Algorithm) -> bool {
65    matches!(alg, Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512)
66}