polyjose 0.1.2

Pure-Rust JOSE: JWA, JWK, JWS, JWE and JWT per the RFCs, with no OpenSSL
Documentation

polyjose

JSON Object Signing and Encryption in pure Rust: JWS, JWE, JWK, JWA and JWT (RFC 7515 to 7519), built from RustCrypto and dalek primitives, with no OpenSSL, ring or aws-lc. Verify and decrypt always take an explicit algorithm allow-list, none is never accepted, and secrets are zeroized when dropped (RFC 8725).

The same core ships as @polyjose/node, @polyjose/wasm, a Kotlin library for Android and the JVM, and a Swift package. See https://polyjose.dev for every platform.

Install

cargo add polyjose

Rust 1.89 or later. The crate has no default features. deflate adds JWE compression (zip: DEF, RFC 7516 §4.1.3), which is off by default because compressing before encrypting can leak plaintext (RFC 8725 §3.6). Never enable insecure-test-vectors: it exists only for polyjose's own tests.

Quick start: sign and verify a JWT

use polyjose::jwa::SigningAlg;
use polyjose::jwt::{self, Audience, Claims, Clock, IssueOptions, Keys, NumericDate, Policy, SystemClock};
use polyjose::keys;

fn main() -> Result<(), polyjose::Error> {
    // An ES256 key pair (RFC 7518 §3.4). The verifier needs only the public half.
    let private_key = keys::generate_for(SigningAlg::ES256)?;
    let public_key = private_key.to_public().expect("an EC key has a public half");

    // Issue a token that expires in five minutes (RFC 7519 §4.1).
    let now = SystemClock.now()?;
    let mut claims = Claims::new();
    claims.iss = Some("https://issuer.example".into());
    claims.sub = Some("user-123".into());
    claims.aud = Some(Audience::One("https://api.example".into()));
    claims.exp = NumericDate::from_secs(now + 300);
    let token = jwt::sign(&claims, &IssueOptions::new(SigningAlg::ES256), &private_key)?;

    // Verify it. The allow-list names the algorithms you accept: the token's own `alg` is never
    // trusted alone (RFC 8725 §3.1). `exp` is required and checked with 60 seconds of leeway, and
    // the issuer and audience must match (RFC 8725 §3.8, §3.9).
    let algs = [SigningAlg::ES256];
    let policy = Policy::new(&SystemClock, &algs)?
        .issuer("https://issuer.example")
        .audience("https://api.example");
    let verified = jwt::verify(&token, Keys::One(&public_key), &policy)?;
    assert_eq!(verified.claims.sub.as_deref(), Some("user-123"));
    Ok(())
}

Every failure is a polyjose::Error; err.code() gives a stable string, such as TokenExpired or AlgorithmNotAllowed, that is the same on every platform.

Encrypt and decrypt (JWE)

use polyjose::jwa::{ContentEncAlg, KeyMgmtAlg};
use polyjose::jwe::{self, DecryptPolicy, Encryptor, Recipient};
use polyjose::jwk::EcCurve;
use polyjose::keys::{self, KeySpec};

fn main() -> Result<(), polyjose::Error> {
    // The recipient's P-256 key pair. The sender needs only the public half.
    let recipient = keys::generate(KeySpec::Ec(EcCurve::P256))?;
    let recipient_public = recipient.to_public().expect("an EC key has a public half");

    // ECDH-ES+A256KW (RFC 7518 §4.6) wraps a fresh A256GCM content key (RFC 7518 §5.3).
    let token = Encryptor::new(ContentEncAlg::A256GCM)
        .recipient(Recipient::new(KeyMgmtAlg::EcdhEsA256KW, &recipient_public))
        .encrypt_compact(b"top secret")?;

    // Decrypting names the `alg` and `enc` values you accept (RFC 8725 §3.1).
    let policy = DecryptPolicy::new(&[KeyMgmtAlg::EcdhEsA256KW], &[ContentEncAlg::A256GCM])?;
    let decrypted = jwe::decrypt_compact(&token, &recipient, &policy)?;
    assert_eq!(decrypted.plaintext(), b"top secret");
    Ok(())
}

The plaintext is wiped from memory when decrypted is dropped.

Documentation

License

MIT. See https://github.com/bharathkontham/polyjose/blob/main/LICENSE.