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}