ppoppo-token 0.4.1

JWT (RFC 9068, EdDSA) issuance + verification engine for the Ppoppo ecosystem. Single deep module with a small interface (issue, verify) hiding RFC 8725 mitigations M01-M45, JWKS handling, and substrate ports (epoch, session, replay).
Documentation
//! Sealed JWS signature algorithm whitelist (Phase 7 §6.8 — structural M51/M52/M54).
//!
//! Shared by both token profiles (RFC 9068 access tokens, OIDC Core 1.0 id
//! tokens) — the algorithm vocabulary is JOSE-level, not profile-level.
//! Living at the crate root so neither `access_token::*` nor `id_token::*`
//! can claim ownership.
//!
//! Only `EdDSA` exists. Consumer attempts to construct `Algorithm::HS256`
//! or any other variant fail at compile time (`variant not found`),
//! making M51/M52/M54 enforcement structural rather than lint-based.
//! `jsonwebtoken::Algorithm` is no longer re-exported — `crates/shared/ppoppo-token`
//! owns the algorithm vocabulary.
//!
//! Adding a new variant is a deliberate spec change — the matrix M02/M06
//! rows must be revisited and the negative regression in
//! `tests/jwt_negative.rs` reinstated to cover the cfg-vs-header SSOT
//! invariant.

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Algorithm {
    /// EdDSA over Ed25519 — RFC 9068 access-token profile and (post-Phase
    /// 10.1) OIDC Core 1.0 id-token profile.
    EdDSA,
}

impl Algorithm {
    /// Every algorithm in the sealed signing vocabulary, in JWKS /
    /// discovery advertisement order. The one hand-maintained enumeration
    /// of the variants (no `strum` in the tree); guarded by
    /// `all_lists_every_algorithm_with_jose_wire_strings`. Consumed as the
    /// verify whitelist (`VerifyConfig::{access_token, id_token}`) and the
    /// discovery `id_token_signing_alg_values_supported` array — so the set
    /// PAS advertises, the set it verifies, and the vocabulary it can sign
    /// with cannot drift apart.
    pub const ALL: &'static [Algorithm] = &[Algorithm::EdDSA];

    /// The JOSE `alg` wire token (RFC 7518 §3.1 registry; `EdDSA` per
    /// RFC 8037 §3.1). The sole place each algorithm's wire string is
    /// written — the JWKS `alg` member and the discovery
    /// `id_token_signing_alg_values_supported` array both project from here,
    /// and [`from_str`](Algorithm::from_str) parses against it.
    pub const fn as_jose_str(self) -> &'static str {
        match self {
            Algorithm::EdDSA => "EdDSA",
        }
    }
}

impl std::str::FromStr for Algorithm {
    type Err = ();

    /// Parse the `alg` header field. Derived from [`ALL`](Algorithm::ALL) +
    /// [`as_jose_str`](Algorithm::as_jose_str), so there is no second
    /// wire-string list to keep in sync. Anything other than a known
    /// variant's JOSE string is rejected — family-level rejections
    /// (HS/RS/ES) fire earlier in `check_algorithm::run` to give audit logs
    /// the family signal.
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Algorithm::ALL
            .iter()
            .copied()
            .find(|a| a.as_jose_str() == s)
            .ok_or(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn all_lists_every_algorithm_with_jose_wire_strings() {
        // Independent oracle: RFC 8037 §3.1 — Ed25519's JOSE `alg` value is
        // "EdDSA". Anchoring `ALL.map(as_jose_str)` to the hardcoded JOSE
        // literal keeps this from being an identity check on the
        // implementation (the tautology-test trap). `ALL` is the residual
        // hand-maintained list (no `strum` in the tree), so adding/removing a
        // variant is a conscious change visible right here.
        assert_eq!(
            Algorithm::ALL
                .iter()
                .map(|a| a.as_jose_str())
                .collect::<Vec<_>>(),
            ["EdDSA"],
        );
    }

    #[test]
    fn from_str_round_trips_all_and_rejects_unknown() {
        // `from_str` derives from ALL + as_jose_str — every variant's JOSE
        // string round-trips, and family-level junk (HS/RS/ES) is rejected.
        for a in Algorithm::ALL {
            assert_eq!(a.as_jose_str().parse::<Algorithm>(), Ok(*a));
        }
        assert_eq!("HS256".parse::<Algorithm>(), Err(()));
        assert_eq!("RS256".parse::<Algorithm>(), Err(()));
        assert_eq!("".parse::<Algorithm>(), Err(()));
    }
}