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}