ppoppo-token 0.42.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
//! Signature verification — M75 (RFC 7515 §5.2, RFC 8725 §3.1).
//!
//! Every other `check_*` module reads what the token *says*; this one
//! decides whether the issuer *said it*. It runs immediately after
//! `check_header` resolves the `kid` (M12) and before any claim is read,
//! so no claim — and no replay-cache, session-row or epoch lookup keyed on
//! one — is ever acted on for a token the issuer did not sign.
//!
//! The signing input is the first two segments **exactly as transmitted**
//! (RFC 7515 §5.2 step 8), never a re-encoding of the parsed JSON: a
//! re-encoding would verify a token other than the one presented.
//!
//! Until this module existed the pipeline resolved the `kid` and stopped
//! there, so any token naming a key in the set — with any payload and any
//! third segment — was admitted. The regression guards are
//! `test_reject_tampered_payload` and its siblings in
//! `tests/jwt_negative.rs`, with counterparts in `tests/id_token_negative.rs`
//! and `tests/id_token_logout_hint.rs`.

use jsonwebtoken::DecodingKey;

use crate::engine::raw::check_strict_base64url;
use crate::engine::shared_error::SharedAuthError;

/// The algorithm the signature is verified under — pinned by the engine,
/// never read from the token header (M06). `Algorithm` is sealed to
/// `EdDSA` (M02), so this is the only value `check_algorithm` can have
/// admitted; a second algorithm would have to be threaded through here
/// deliberately, keyed to the resolved key, not to the header.
const PINNED_ALGORITHM: jsonwebtoken::Algorithm = jsonwebtoken::Algorithm::EdDSA;

pub(crate) fn run(token: &str, key: &DecodingKey) -> Result<(), SharedAuthError> {
    let mut segments = token.split('.');
    let (Some(header), Some(payload), Some(signature), None) = (
        segments.next(),
        segments.next(),
        segments.next(),
        segments.next(),
    ) else {
        return Err(SharedAuthError::NotJwsCompact);
    };

    // M33 covers the signature segment too: standard-alphabet or padded
    // base64 is rejected with the same audit signal as the other two.
    check_strict_base64url(signature)?;

    let signing_input = &token[..header.len() + 1 + payload.len()];

    // `Ok(false)` is a well-formed signature that does not verify; `Err`
    // is one that could not be decoded or checked at all. Both mean the
    // issuer did not sign these bytes, and neither may admit.
    match jsonwebtoken::crypto::verify(signature, signing_input.as_bytes(), key, PINNED_ALGORITHM) {
        Ok(true) => Ok(()),
        Ok(false) | Err(_) => Err(SharedAuthError::SignatureInvalid),
    }
}