Skip to main content

ppoppo_token/
algorithm.rs

1//! Sealed JWS signature algorithm whitelist (Phase 7 §6.8 — structural M51/M52/M54).
2//!
3//! Shared by both token profiles (RFC 9068 access tokens, OIDC Core 1.0 id
4//! tokens) — the algorithm vocabulary is JOSE-level, not profile-level.
5//! Living at the crate root so neither `access_token::*` nor `id_token::*`
6//! can claim ownership.
7//!
8//! Only `EdDSA` exists. Consumer attempts to construct `Algorithm::HS256`
9//! or any other variant fail at compile time (`variant not found`),
10//! making M51/M52/M54 enforcement structural rather than lint-based.
11//! `jsonwebtoken::Algorithm` is no longer re-exported — `crates/shared/ppoppo-token`
12//! owns the algorithm vocabulary.
13//!
14//! Adding a new variant is a deliberate spec change — the matrix M02/M06
15//! rows must be revisited and the negative regression in
16//! `tests/jwt_negative.rs` reinstated to cover the cfg-vs-header SSOT
17//! invariant.
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum Algorithm {
21    /// EdDSA over Ed25519 — RFC 9068 access-token profile and (post-Phase
22    /// 10.1) OIDC Core 1.0 id-token profile.
23    EdDSA,
24}
25
26impl Algorithm {
27    /// Every algorithm in the sealed signing vocabulary, in JWKS /
28    /// discovery advertisement order. The one hand-maintained enumeration
29    /// of the variants (no `strum` in the tree); guarded by
30    /// `all_lists_every_algorithm_with_jose_wire_strings`. Consumed as the
31    /// verify whitelist (`VerifyConfig::{access_token, id_token}`) and the
32    /// discovery `id_token_signing_alg_values_supported` array — so the set
33    /// PAS advertises, the set it verifies, and the vocabulary it can sign
34    /// with cannot drift apart.
35    pub const ALL: &'static [Algorithm] = &[Algorithm::EdDSA];
36
37    /// The JOSE `alg` wire token (RFC 7518 §3.1 registry; `EdDSA` per
38    /// RFC 8037 §3.1). The sole place each algorithm's wire string is
39    /// written — the JWKS `alg` member and the discovery
40    /// `id_token_signing_alg_values_supported` array both project from here,
41    /// and [`from_str`](Algorithm::from_str) parses against it.
42    pub const fn as_jose_str(self) -> &'static str {
43        match self {
44            Algorithm::EdDSA => "EdDSA",
45        }
46    }
47}
48
49impl std::str::FromStr for Algorithm {
50    type Err = ();
51
52    /// Parse the `alg` header field. Derived from [`ALL`](Algorithm::ALL) +
53    /// [`as_jose_str`](Algorithm::as_jose_str), so there is no second
54    /// wire-string list to keep in sync. Anything other than a known
55    /// variant's JOSE string is rejected — family-level rejections
56    /// (HS/RS/ES) fire earlier in `check_algorithm::run` to give audit logs
57    /// the family signal.
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        Algorithm::ALL
60            .iter()
61            .copied()
62            .find(|a| a.as_jose_str() == s)
63            .ok_or(())
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn all_lists_every_algorithm_with_jose_wire_strings() {
73        // Independent oracle: RFC 8037 §3.1 — Ed25519's JOSE `alg` value is
74        // "EdDSA". Anchoring `ALL.map(as_jose_str)` to the hardcoded JOSE
75        // literal keeps this from being an identity check on the
76        // implementation (the tautology-test trap). `ALL` is the residual
77        // hand-maintained list (no `strum` in the tree), so adding/removing a
78        // variant is a conscious change visible right here.
79        assert_eq!(
80            Algorithm::ALL
81                .iter()
82                .map(|a| a.as_jose_str())
83                .collect::<Vec<_>>(),
84            ["EdDSA"],
85        );
86    }
87
88    #[test]
89    fn from_str_round_trips_all_and_rejects_unknown() {
90        // `from_str` derives from ALL + as_jose_str — every variant's JOSE
91        // string round-trips, and family-level junk (HS/RS/ES) is rejected.
92        for a in Algorithm::ALL {
93            assert_eq!(a.as_jose_str().parse::<Algorithm>(), Ok(*a));
94        }
95        assert_eq!("HS256".parse::<Algorithm>(), Err(()));
96        assert_eq!("RS256".parse::<Algorithm>(), Err(()));
97        assert_eq!("".parse::<Algorithm>(), Err(()));
98    }
99}