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
//! Generate `JWT_SIGNING_KEYS_JSON` for PAS configuration. Outputs a
//! single-element JSON array with a fresh Ed25519 private key in PKCS8
//! v1 PEM, suitable for direct paste into the env var.
//!
//! Usage:
//!   cargo run -p ppoppo-token --bin generate-keys

use base64::Engine as _;
use ed25519_compact::KeyPair;
use time::format_description::well_known::Rfc3339;

/// PKCS8 v1 prefix for an Ed25519 private key (RFC 8410 ยง7).
/// 16-byte ASN.1 wrapper followed by the 32-byte seed.
const PKCS8_PREFIX: [u8; 16] = [
    0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
];

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let kp = KeyPair::generate();
    let seed = kp.sk.seed();

    let mut der = Vec::with_capacity(48);
    der.extend_from_slice(&PKCS8_PREFIX);
    der.extend_from_slice(&seed[..]);
    let b64 = base64::engine::general_purpose::STANDARD.encode(&der);

    let mut wrapped = String::with_capacity(b64.len() + 2);
    for (i, ch) in b64.chars().enumerate() {
        if i > 0 && i % 64 == 0 {
            wrapped.push('\n');
        }
        wrapped.push(ch);
    }
    let pem = format!(
        "-----BEGIN PRIVATE KEY-----\n{wrapped}\n-----END PRIVATE KEY-----\n"
    );

    let created_at = time::OffsetDateTime::now_utc().format(&Rfc3339)?;

    let json = serde_json::json!([{
        "kid": "local-key-1",
        "private_key_pem": pem,
        "status": "active",
        "created_at": created_at
    }]);

    println!("{json}");
    Ok(())
}