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)]
17#[non_exhaustive]
18pub struct VerifierConfig {
19 /// Attestation `iss` values this verifier trusts.
20 pub trusted_issuers: HashSet<String>,
21 /// Algorithms accepted for both the request signature and the attestation. Must be
22 /// asymmetric only — see the type-level docs above.
23 pub allowed_algs: Vec<Algorithm>,
24 /// Clock skew tolerance applied to both the attestation's and the signature's freshness
25 /// windows.
26 pub max_clock_skew: Duration,
27 /// Upper bound on `sig.exp - sig.iat`. Rejects long-lived signatures outright even if `exp`
28 /// itself is still in the future — a short-lived signing key policy is only meaningful if
29 /// the lifetime is bounded independently of the clock.
30 pub max_signature_lifetime: Duration,
31 /// The audience this verifier expects requests to be signed for (`sig.aud`). Prevents a
32 /// signature captured for one service being replayed against another.
33 pub audience: String,
34}
35
36impl VerifierConfig {
37 /// Builds a config, silently dropping any symmetric algorithm from `allowed_algs`.
38 ///
39 /// Dropping rather than erroring keeps this ergonomic for the common case (a caller passes
40 /// `[Algorithm::ES256]` and never has to think about it) while still making the
41 /// "asymmetric only" rule impossible to defeat by misconfiguration — the algorithm-family
42 /// check inside `verify()` is the actual enforcement point either way.
43 pub fn new(
44 trusted_issuers: impl IntoIterator<Item = impl Into<String>>,
45 allowed_algs: impl IntoIterator<Item = Algorithm>,
46 max_clock_skew: Duration,
47 max_signature_lifetime: Duration,
48 audience: impl Into<String>,
49 ) -> Self {
50 let allowed_algs: Vec<Algorithm> = allowed_algs
51 .into_iter()
52 .filter(|alg| !is_symmetric(*alg))
53 .collect();
54
55 Self {
56 trusted_issuers: trusted_issuers.into_iter().map(Into::into).collect(),
57 allowed_algs,
58 max_clock_skew,
59 max_signature_lifetime,
60 audience: audience.into(),
61 }
62 }
63}
64
65pub(crate) fn is_symmetric(alg: Algorithm) -> bool {
66 matches!(alg, Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512)
67}