ppoppo-token 0.2.0

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
//! Algorithm checks — M01-M06 (RFC 8725 §3.1, §3.2).
//!
//! Strategy: parse the header segment as raw JSON ourselves rather than
//! routing through `jsonwebtoken::decode_header`. The library's `Algorithm`
//! enum has no `None` variant in 9.3.1, so an `alg: none` token would
//! surface as a generic parse error and bury the M01 distinction. Hand-
//! parsing lets us map each rejected family to its own `SharedAuthError`
//! variant (audit log dimension).

use std::str::FromStr;

use crate::algorithm::Algorithm;
use crate::engine::raw::parse_header_json;
use crate::engine::shared_config::SharedVerifyConfig;
use crate::engine::shared_error::SharedAuthError;

pub(crate) fn run(token: &str, cfg: &SharedVerifyConfig) -> Result<(), SharedAuthError> {
    let header = parse_header_json(token)?;
    let alg = header
        .get("alg")
        .and_then(|v| v.as_str())
        .ok_or(SharedAuthError::HeaderUnparseable)?;

    // M01: explicit `alg: none` (case-insensitive — RFC says lowercase but
    // attackers will probe variants).
    if alg.eq_ignore_ascii_case("none") {
        return Err(SharedAuthError::AlgNone);
    }

    // M03/M04/M05: family-level rejections fire *before* the generic
    // whitelist check so audit logs carry the family signal even when an
    // operator widens the whitelist by mistake.
    if alg.starts_with("HS") {
        return Err(SharedAuthError::AlgHmacRejected);
    }
    if alg.starts_with("RS") || alg.starts_with("PS") {
        return Err(SharedAuthError::AlgRsaRejected);
    }
    if alg.starts_with("ES") {
        return Err(SharedAuthError::AlgEcdsaRejected);
    }

    // M02 + M06: the SSOT for which algorithms are acceptable is
    // `cfg.algorithms`, never `header.alg`. We parse the header's alg only
    // to compare it against the configured set; an unparseable or
    // non-whitelisted value is rejected. This is what RFC 8725 §3.2 calls
    // "use the algorithm sent by the application, not the one sent in the
    // header".
    let parsed = Algorithm::from_str(alg).map_err(|_| SharedAuthError::AlgNotWhitelisted)?;
    if !cfg.algorithms.contains(&parsed) {
        return Err(SharedAuthError::AlgNotWhitelisted);
    }

    Ok(())
}