Skip to main content

ppoppo_token/
verify_config.rs

1//! Per-request verification configuration shared by all entry points.
2//!
3//! SSOT for the policy values that drive `engine::verify`. The same
4//! struct is consumed by PAS self-verify, PCS service-side check, and the
5//! `pas-external` consumer middleware so policy never drifts between the
6//! three surfaces (STANDARDS_JWT_DETAILS §3).
7//!
8//! ── Phase 5 — orthogonal port slots ────────────────────────────────────
9//!
10//! Three optional ports model the orthogonal revocation axes (M35-M38 +
11//! sv). Each is `Option<Arc<dyn ...>>` so callers wire only what their
12//! deployment substrate supports — `None` short-circuits the gate
13//! (legacy admit / sibling-test config / migration phases).
14//!
15//! - `replay`  — M35 jti uniqueness window
16//! - `session` — M36 per-session row liveness
17//! - `epoch`   — sv per-account version (chat-auth migration target)
18//!
19//! Phase 10 split: these slots stay on `access_token::VerifyConfig`;
20//! `id_token::VerifyConfig` carries its own (`expected_nonce`,
21//! `max_age`, `acr_values`) and never imports these traits.
22
23use std::sync::Arc;
24
25use crate::epoch_revocation::EpochRevocation;
26use crate::replay_defense::ReplayDefense;
27use crate::session_revocation::SessionRevocation;
28
29/// Sealed JWS signature algorithm whitelist (Phase 7 §6.8 — structural M51/M52/M54).
30///
31/// Only `EdDSA` exists. Consumer attempts to construct `Algorithm::HS256`
32/// or any other variant fail at compile time (`variant not found`),
33/// making M51/M52/M54 enforcement structural rather than lint-based.
34/// `jsonwebtoken::Algorithm` is no longer re-exported — `crates/shared/ppoppo-token`
35/// owns the algorithm vocabulary.
36///
37/// Adding a new variant (e.g., for OIDC interop in Phase 10) is a deliberate
38/// spec change — the matrix M02/M06 rows must be revisited and the negative
39/// regression in `tests/jwt_negative.rs` reinstated to cover the cfg-vs-header
40/// SSOT invariant.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum Algorithm {
43    /// EdDSA over Ed25519 — RFC 9068 access-token profile.
44    EdDSA,
45}
46
47impl std::str::FromStr for Algorithm {
48    type Err = ();
49
50    /// Parse the `alg` header field. Anything other than `"EdDSA"` is
51    /// rejected — family-level rejections (HS/RS/ES) fire earlier in
52    /// `check_algorithm::run` to give audit logs the family signal.
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        match s {
55            "EdDSA" => Ok(Algorithm::EdDSA),
56            _ => Err(()),
57        }
58    }
59}
60
61#[derive(Debug, Clone)]
62#[allow(dead_code)] // fields consumed across commits 1.2-1.16 + Phase 2+
63pub struct VerifyConfig {
64    pub(crate) issuer: String,
65    pub(crate) audience: String,
66    pub(crate) expected_typ: &'static str,
67    pub(crate) max_token_size: usize,
68    pub(crate) algorithms: Vec<Algorithm>,
69
70    // ── Phase 5 revocation port slots ──────────────────────────────────
71    /// M35 jti replay defense (Phase 5 commit 5.1). `None` skips the
72    /// gate — appropriate for tests / non-revocation-aware deployments.
73    pub(crate) replay: Option<Arc<dyn ReplayDefense>>,
74
75    /// M36 session-row liveness (Phase 5 commit 5.2). `None` skips —
76    /// appropriate when issuance hasn't started emitting `sid` yet
77    /// (gradual-rollout pattern).
78    pub(crate) session: Option<Arc<dyn SessionRevocation>>,
79
80    /// sv-port per-account epoch (Phase 5 commits 5.5-5.7). `None` skips
81    /// — chat-auth migration sets this to the substrate adapter that
82    /// internally composes its existing cache + fetcher.
83    pub(crate) epoch: Option<Arc<dyn EpochRevocation>>,
84}
85
86impl VerifyConfig {
87    /// Build the canonical access-token config: `at+jwt` typ, EdDSA-only
88    /// algorithm whitelist, 8 KB token size cap (M34). All revocation
89    /// port slots default to `None`; callers opt in via the
90    /// `with_replay_defense` / `with_session_revocation` /
91    /// `with_epoch_revocation` builders.
92    pub fn access_token(issuer: impl Into<String>, audience: impl Into<String>) -> Self {
93        Self {
94            issuer: issuer.into(),
95            audience: audience.into(),
96            expected_typ: "at+jwt",
97            max_token_size: 8 * 1024,
98            algorithms: vec![Algorithm::EdDSA],
99            replay: None,
100            session: None,
101            epoch: None,
102        }
103    }
104
105    /// Override the algorithm whitelist. Test-only escape hatch — production
106    /// callers MUST go through `access_token` (or a future profile-specific
107    /// constructor) so the EdDSA pin is the default, not an override.
108    #[must_use]
109    pub fn with_algorithms(mut self, algorithms: Vec<Algorithm>) -> Self {
110        self.algorithms = algorithms;
111        self
112    }
113
114    /// Wire the M35 jti replay defense port. Call site (PCS chat-auth /
115    /// pas-external SDK) constructs the substrate adapter (KVRocks,
116    /// in-memory test stand-in) and hands the `Arc<dyn ...>` here.
117    #[must_use]
118    pub fn with_replay_defense(mut self, port: Arc<dyn ReplayDefense>) -> Self {
119        self.replay = Some(port);
120        self
121    }
122
123    /// Wire the M36 session-row liveness port.
124    #[must_use]
125    pub fn with_session_revocation(mut self, port: Arc<dyn SessionRevocation>) -> Self {
126        self.session = Some(port);
127        self
128    }
129
130    /// Wire the sv-port per-account epoch revocation. Implementations
131    /// internally compose their cache + fetcher (e.g. chat-auth's
132    /// existing `SessionVersionCache` + `SessionVersionFetcher` pair) —
133    /// the engine boundary sees a single port.
134    #[must_use]
135    pub fn with_epoch_revocation(mut self, port: Arc<dyn EpochRevocation>) -> Self {
136        self.epoch = Some(port);
137        self
138    }
139}