#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod common;
use polyjose::Error;
use polyjose::header::Header;
use polyjose::jwa::{ContentEncAlg, KeyMgmtAlg, SigningAlg};
use polyjose::jwe::{DecryptPolicy, Encryptor, JsonForm, Recipient};
use polyjose::jwk::{EcCurve, Jwk, OkpCurve};
use polyjose::jws::{ProtectedHeader, SignOptions, sign_compact};
use polyjose::jwt::{
self, Audience, Claims, FixedClock, IssueOptions, Keys, NumericDate, Policy, VerifiedNestedJwt,
};
use polyjose::keys::{KeySpec, generate};
use polyjose::util::{b64url_decode, b64url_encode};
use ContentEncAlg as E;
use KeyMgmtAlg as A;
const P256: &str = r#"{"kty":"EC","crv":"P-256","x":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU","y":"x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0","d":"jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI"}"#;
const KEK: &str = r#"{"kty":"oct","k":"GZy6sIZ6wl9NJOKB-jnmVQ"}"#;
const PASSWORD: &[u8] = b"entrap_o\xe2\x80\x93peter_long\xe2\x80\x93credit_tun";
const RSA_PUBLIC: &str = r#"{"kty":"RSA","n":"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4QlL-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUsLA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ","e":"AQAB"}"#;
const EXP: i64 = 1_300_819_380;
const NOW: i64 = EXP - 60;
const LATE: i64 = EXP + 60;
const ISSUER: &str = "https://issuer.example";
const AUDIENCE: &str = "https://api.example";
const SIGNING: [SigningAlg; 2] = [SigningAlg::ES256, SigningAlg::EdDSA];
fn jwk(json: &str) -> Jwk {
Jwk::from_json(json.as_bytes()).unwrap()
}
fn header(json: &str) -> Header {
Header::from_json(json.as_bytes()).unwrap()
}
fn code<T>(r: polyjose::Result<T>) -> &'static str {
match r {
Ok(_) => "ok",
Err(e) => e.code(),
}
}
fn es256_key() -> Jwk {
jwk(P256)
}
fn ed25519_key() -> Jwk {
jwk(&format!(
r#"{{"kty":"OKP","crv":"Ed25519","x":"{}","d":"{}"}}"#,
common::ED25519_X,
common::ED25519_D
))
}
fn public(key: &Jwk) -> Jwk {
key.to_public().unwrap()
}
fn claims() -> Claims {
let mut c = Claims::new();
c.iss = Some(ISSUER.into());
c.sub = Some("alice".into());
c.exp = NumericDate::from_secs(EXP);
c
}
fn signed(claims: &Claims, alg: SigningAlg, key: &Jwk) -> String {
jwt::sign(claims, &IssueOptions::new(alg), key).unwrap()
}
fn inner_es256() -> String {
signed(&claims(), SigningAlg::ES256, &es256_key())
}
fn sign_raw(header_json: &str, payload: &[u8], key: &Jwk) -> String {
let protected = ProtectedHeader::from_encoded(&b64url_encode(header_json.as_bytes())).unwrap();
sign_compact(&protected, payload, key, &SignOptions::new()).unwrap()
}
fn encryptor(enc: ContentEncAlg, recipient: Recipient, protected: Header) -> Encryptor {
Encryptor::new(enc)
.recipient(recipient)
.protected_header(protected)
}
fn kw(protected: &str) -> Encryptor {
encryptor(
E::A128GCM,
Recipient::new(A::A128KW, &jwk(KEK)),
header(protected),
)
}
fn nest_raw(plaintext: &str, protected: &str) -> String {
kw(protected).encrypt_compact(plaintext.as_bytes()).unwrap()
}
fn nest(inner: &str) -> String {
jwt::encrypt_nested(inner, &kw("{}")).unwrap()
}
fn outer_header_of(token: &str) -> Header {
let first = token.split('.').next().unwrap();
Header::from_json(&b64url_decode(first).unwrap()).unwrap()
}
fn decrypt_policy() -> DecryptPolicy {
DecryptPolicy::new(
&[
A::Dir,
A::A128KW,
A::EcdhEs,
A::EcdhEsA128KW,
A::Pbes2Hs256A128KW,
A::RsaOaep256,
],
E::KNOWN,
)
.unwrap()
}
fn open_at(token: &str, now: i64) -> polyjose::Result<VerifiedNestedJwt> {
let clock = FixedClock(now);
let policy = Policy::new(&clock, &SIGNING).unwrap();
let key = public(&es256_key());
jwt::decrypt_nested(
token,
&jwk(KEK),
&decrypt_policy(),
Keys::One(&key),
&policy,
)
}
fn open(token: &str) -> polyjose::Result<VerifiedNestedJwt> {
open_at(token, NOW)
}
fn flip_tag(token: &str) -> String {
let (head, tag) = token.rsplit_once('.').unwrap();
let mut octets = b64url_decode(tag).unwrap();
*octets.last_mut().unwrap() ^= 0x01;
format!("{head}.{}", b64url_encode(&octets))
}
#[test]
fn every_outer_mode_decrypts_then_verifies() {
let dir = generate(KeySpec::Oct { bytes: 32 }).unwrap();
let p256 = generate(KeySpec::Ec(EcCurve::P256)).unwrap();
let x25519 = generate(KeySpec::Okp(OkpCurve::X25519)).unwrap();
let password = jwk(&format!(
r#"{{"kty":"oct","k":"{}"}}"#,
b64url_encode(PASSWORD)
));
let cases: [(Recipient, ContentEncAlg, &Jwk, &str); 5] = [
(Recipient::new(A::Dir, &dir), E::A256GCM, &dir, "dir"),
(
Recipient::new(A::A128KW, &jwk(KEK)),
E::A128GCM,
&jwk(KEK),
"A128KW",
),
(
Recipient::new(A::EcdhEsA128KW, &public(&p256)),
E::A128GCM,
&p256,
"ECDH-ES+A128KW",
),
(
Recipient::new(A::EcdhEs, &public(&x25519)),
E::A256GCM,
&x25519,
"ECDH-ES",
),
(
Recipient::new(A::Pbes2Hs256A128KW, &password).p2c(1_000),
E::A128GCM,
&password,
"PBES2-HS256+A128KW",
),
];
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap();
let verifier = public(&es256_key());
let inner = inner_es256();
for (recipient, enc, decrypt_key, alg) in cases {
let e = Encryptor::new(enc.clone()).recipient(recipient);
let token = jwt::encrypt_nested(&inner, &e).unwrap_or_else(|e| panic!("{alg}: {e}"));
assert_eq!(token.split('.').count(), 5, "{alg}");
assert_eq!(outer_header_of(&token).cty.as_deref(), Some("JWT"), "{alg}");
let v = jwt::decrypt_nested(
&token,
decrypt_key,
&decrypt_policy(),
Keys::One(&verifier),
&policy,
)
.unwrap_or_else(|e| panic!("{alg}: {e}"));
assert_eq!(v.inner.claims, claims(), "{alg}");
assert_eq!(v.inner.alg, SigningAlg::ES256, "{alg}");
assert_eq!(v.inner.header.typ.as_deref(), Some("JWT"), "{alg}");
assert_eq!(v.outer_header.alg.as_deref(), Some(alg));
assert_eq!(v.outer_header.enc, Some(enc), "{alg}");
assert_eq!(v.outer_header.cty.as_deref(), Some("JWT"), "{alg}");
assert_eq!(v.recipient, 0, "{alg}");
}
}
fn rsa_private() -> Jwk {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/data/keys/rfc/rfc7515-a2-rsa-private.jwk.json");
let key = Jwk::from_json(&std::fs::read(path).unwrap()).unwrap();
assert!(!key.is_public());
assert_eq!(
key.to_public().unwrap().to_json().as_str(),
jwk(RSA_PUBLIC).to_json().as_str()
);
key
}
#[test]
fn rsa_oaep_256_nested_jwt_decrypts_with_the_private_key_only() {
let rsa = jwk(RSA_PUBLIC);
let inner = signed(&claims(), SigningAlg::EdDSA, &ed25519_key());
let e = Encryptor::new(E::A256GCM).recipient(Recipient::new(A::RsaOaep256, &rsa));
let token = jwt::encrypt_nested(&inner, &e).unwrap();
assert_eq!(token.split('.').count(), 5);
let h = outer_header_of(&token);
assert_eq!(h.alg.as_deref(), Some("RSA-OAEP-256"));
assert_eq!(h.cty.as_deref(), Some("JWT"));
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap();
let verifier = public(&ed25519_key());
let r = jwt::decrypt_nested(
&token,
&rsa,
&decrypt_policy(),
Keys::One(&verifier),
&policy,
);
assert_eq!(r.map(|_| ()), Err(Error::KeyAlgorithmMismatch));
let v = jwt::decrypt_nested(
&token,
&rsa_private(),
&decrypt_policy(),
Keys::One(&verifier),
&policy,
)
.unwrap();
assert_eq!(v.inner.claims, claims());
assert_eq!(v.inner.alg, SigningAlg::EdDSA);
assert_eq!(v.outer_header.alg.as_deref(), Some("RSA-OAEP-256"));
assert_eq!(v.recipient, 0);
}
#[test]
fn rs256_inner_jwt_under_rsa_oaep_256_round_trips() {
let private = rsa_private();
let rsa = jwk(RSA_PUBLIC);
let inner = signed(&claims(), SigningAlg::RS256, &private);
let e = Encryptor::new(E::A128CbcHs256).recipient(Recipient::new(A::RsaOaep256, &rsa));
let token = jwt::encrypt_nested(&inner, &e).unwrap();
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &[SigningAlg::RS256]).unwrap();
let v = jwt::decrypt_nested(
&token,
&private,
&decrypt_policy(),
Keys::One(&rsa),
&policy,
)
.unwrap();
assert_eq!(v.inner.claims, claims());
assert_eq!(v.inner.alg, SigningAlg::RS256);
assert_eq!(
jwt::sign(&claims(), &IssueOptions::new(SigningAlg::RS256), &rsa).map(|_| ()),
Err(Error::KeyAlgorithmMismatch)
);
}
#[test]
fn an_eddsa_inner_jwt_verifies() {
let inner = signed(&claims(), SigningAlg::EdDSA, &ed25519_key());
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap();
let verifier = public(&ed25519_key());
let v = jwt::decrypt_nested(
&nest(&inner),
&jwk(KEK),
&decrypt_policy(),
Keys::One(&verifier),
&policy,
)
.unwrap();
assert_eq!(v.inner.alg, SigningAlg::EdDSA);
assert_eq!(v.inner.claims, claims());
}
#[test]
fn encrypt_nested_sets_cty_jwt_and_keeps_the_callers_header() {
let inner = inner_es256();
let e = kw(r#"{"typ":"JWT","kid":"outer-1","x-tenant":"acme"}"#);
let first = jwt::encrypt_nested(&inner, &e).unwrap();
let second = jwt::encrypt_nested(&inner, &e).unwrap();
assert_ne!(first, second, "fresh CEK and IV each time (ADR 0005)");
for token in [&first, &second] {
let h = outer_header_of(token);
assert_eq!(h.cty.as_deref(), Some("JWT"));
assert_eq!(h.typ.as_deref(), Some("JWT"));
assert_eq!(h.kid.as_deref(), Some("outer-1"));
assert!(h.other_params().contains("x-tenant"));
let v = open(token).unwrap();
assert_eq!(v.outer_header, h);
}
for cty in ["JWT", "jwt", "application/JWT", "Application/jwt"] {
let token = jwt::encrypt_nested(&inner, &kw(&format!(r#"{{"cty":"{cty}"}}"#))).unwrap();
assert_eq!(outer_header_of(&token).cty.as_deref(), Some("JWT"), "{cty}");
assert_eq!(code(open(&token)), "ok", "{cty}");
}
}
#[test]
fn encrypt_nested_refuses_a_different_caller_cty() {
let inner = inner_es256();
for cty in ["JOSE", "at+jwt", "JWT ", "text/JWT", "application/jwt;v=1"] {
let r = jwt::encrypt_nested(&inner, &kw(&format!(r#"{{"cty":"{cty}"}}"#)));
assert!(matches!(r, Err(Error::InvalidHeader(_))), "{cty:?}: {r:?}");
}
}
#[test]
fn encrypt_nested_needs_a_compact_jws() {
let inner = inner_es256();
let jwe = nest(&inner);
let json = format!(
r#"{{"payload":"{}","protected":"{}","signature":"{}"}}"#,
inner.split('.').nth(1).unwrap(),
inner.split('.').next().unwrap(),
inner.split('.').nth(2).unwrap(),
);
let with_newline = format!("{inner}\n");
let (head, rest) = inner.split_once('.').unwrap();
let (payload, _) = rest.split_once('.').unwrap();
let no_signature = format!("{head}.{payload}.");
let with_header = |h: &[u8]| format!("{}.{payload}.c2ln", b64url_encode(h));
let not_json = with_header(b"not json");
let an_array = with_header(br#"["alg","none"]"#);
let duplicate_alg = with_header(br#"{"alg":"ES256","alg":"none"}"#);
let not_utf8 = with_header(b"{\"alg\":\"\xff\"}");
for input in [
"",
"not a jws",
"a.b",
jwe.as_str(),
json.as_str(),
with_newline.as_str(),
".eyJ9.sig",
no_signature.as_str(),
"eyJhbGciOiJub25lIn0.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.",
"eyJhbGciOiJub25lIn0.eyJzdWIiOiJ4In0.",
"e.eyJ9.sig",
"QR.eyJ9.sig",
not_json.as_str(),
an_array.as_str(),
duplicate_alg.as_str(),
not_utf8.as_str(),
] {
for e in [kw("{}"), kw(r#"{"cty":"JOSE"}"#), kw("{}").compress()] {
assert!(
matches!(jwt::encrypt_nested(input, &e), Err(Error::InvalidFormat(_))),
"{input:?}"
);
}
}
}
#[test]
fn encrypt_nested_refuses_an_unsecured_or_alg_less_jws() {
let encryptors = [
kw("{}"),
kw(r#"{"typ":"JWT","kid":"outer-1"}"#),
kw(r#"{"cty":"JOSE"}"#),
kw("{}").compress(),
kw(r#"{"cty":"JOSE"}"#).compress(),
];
let with_header = |h: &str| format!("{}.eyJzdWIiOiJ4In0.c2ln", b64url_encode(h.as_bytes()));
for h in [
r#"{"alg":"none"}"#,
r#"{"typ":"JWT","alg":"none"}"#,
r#"{"alg":"none","kid":"k1","crit":["exp"],"exp":1}"#,
" {\"alg\" : \"none\"}\n",
] {
let input = with_header(h);
for e in &encryptors {
assert_eq!(
jwt::encrypt_nested(&input, e),
Err(Error::AlgorithmNotAllowed),
"{h}"
);
}
}
for e in &encryptors {
assert_eq!(
jwt::encrypt_nested("eyJhbGciOiJub25lIn0.eyJzdWIiOiJ4In0.c2ln", e),
Err(Error::AlgorithmNotAllowed)
);
}
for h in [
"{}",
r#"{"typ":"JWT"}"#,
r#"{"alg":null}"#,
r#"{"alg":1}"#,
r#"{"alg":["none"]}"#,
r#"{"alg":{"none":true}}"#,
r#"{"Alg":"none"}"#,
] {
let input = with_header(h);
for e in &encryptors {
assert_eq!(
jwt::encrypt_nested(&input, e),
Err(Error::InvalidHeader("alg")),
"{h}"
);
}
}
let other = with_header(r#"{"alg":"x-unregistered"}"#);
assert_eq!(code(jwt::encrypt_nested(&other, &kw("{}"))), "ok");
}
#[test]
fn encrypt_nested_refuses_compression() {
let inner = inner_es256();
for e in [
kw("{}").compress(),
kw(r#"{"cty":"JWT"}"#).compress(),
kw(r#"{"typ":"JWT","kid":"outer-1"}"#).compress(),
kw(r#"{"zip":"DEF"}"#),
kw(r#"{"zip":"XYZ"}"#),
kw(r#"{"zip":"def"}"#),
kw(r#"{"cty":"JWT","zip":"DEF"}"#),
kw(r#"{"zip":"DEF"}"#).compress(),
] {
assert_eq!(
jwt::encrypt_nested(&inner, &e),
Err(Error::InvalidHeader("zip")),
"{e:?}"
);
}
for e in [
kw(r#"{"cty":"JOSE"}"#).compress(),
kw(r#"{"cty":"JOSE","zip":"DEF"}"#),
] {
assert_eq!(
jwt::encrypt_nested(&inner, &e),
Err(Error::InvalidHeader("cty")),
"{e:?}"
);
}
}
#[cfg(feature = "deflate")]
#[test]
fn encrypt_nested_refuses_deflate_but_decrypt_nested_accepts_it() {
let inner = inner_es256();
let compressing = kw("{}").compress();
let plain_jwe = compressing.encrypt_compact(inner.as_bytes()).unwrap();
assert_eq!(outer_header_of(&plain_jwe).zip.as_deref(), Some("DEF"));
assert_eq!(
jwt::encrypt_nested(&inner, &compressing),
Err(Error::InvalidHeader("zip"))
);
let received = kw(r#"{"cty":"JWT"}"#)
.compress()
.encrypt_compact(inner.as_bytes())
.unwrap();
assert_eq!(outer_header_of(&received).zip.as_deref(), Some("DEF"));
let v = open(&received).unwrap();
assert_eq!(v.inner.claims.iss.as_deref(), Some(ISSUER));
assert_eq!(v.outer_header.zip.as_deref(), Some("DEF"));
}
#[test]
fn encrypt_nested_is_compact_only() {
let inner = inner_es256();
let no_recipient = Encryptor::new(E::A128GCM);
let shared = kw("{}").shared_unprotected_header(header(r#"{"x":1}"#));
let aad = kw("{}").aad(b"aad".to_vec());
for e in [no_recipient, shared, aad] {
assert!(
matches!(
jwt::encrypt_nested(&inner, &e),
Err(Error::InvalidFormat(_))
),
"{e:?}"
);
}
}
#[test]
fn an_inner_signature_from_another_key_is_invalid_signature() {
let other = generate(KeySpec::Ec(EcCurve::P256)).unwrap();
let inner = signed(&claims(), SigningAlg::ES256, &other);
assert_eq!(code(open(&nest(&inner))), "InvalidSignature");
}
#[test]
fn an_expired_inner_jwt_is_token_expired() {
let token = nest(&inner_es256());
assert_eq!(code(open_at(&token, NOW)), "ok");
assert_eq!(open_at(&token, LATE), Err(Error::TokenExpired));
}
#[test]
fn a_tampered_outer_tag_is_decryption_failed() {
let token = flip_tag(&nest(&inner_es256()));
assert_eq!(open(&token), Err(Error::DecryptionFailed));
}
#[test]
fn an_outer_enc_outside_the_decrypt_policy_is_algorithm_not_allowed() {
let token = nest(&inner_es256());
let only_a256gcm = DecryptPolicy::new(&[A::A128KW], &[E::A256GCM]).unwrap();
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap();
let key = public(&es256_key());
let r = jwt::decrypt_nested(&token, &jwk(KEK), &only_a256gcm, Keys::One(&key), &policy);
assert!(matches!(r, Err(Error::AlgorithmNotAllowed)), "{r:?}");
let only_a256kw = DecryptPolicy::new(&[A::A256KW], &[E::A128GCM]).unwrap();
let r = jwt::decrypt_nested(&token, &jwk(KEK), &only_a256kw, Keys::One(&key), &policy);
assert!(matches!(r, Err(Error::AlgorithmNotAllowed)), "{r:?}");
}
#[test]
fn an_inner_alg_outside_the_jwt_policy_is_algorithm_not_allowed() {
let hmac = jwk(
r#"{"kty":"oct","k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"}"#,
);
let inner = signed(&claims(), SigningAlg::HS256, &hmac);
assert!(matches!(
open(&nest(&inner)),
Err(Error::AlgorithmNotAllowed)
));
}
#[test]
fn an_inner_alg_none_is_algorithm_not_allowed() {
let payload = b64url_encode(claims().to_json().as_bytes());
let header = b64url_encode(br#"{"alg":"none"}"#);
for inner in [
format!("{header}.{payload}."),
format!("{header}.{payload}.c2ln"),
] {
assert!(jwt::encrypt_nested(&inner, &kw("{}")).is_err(), "{inner}");
assert!(
matches!(
open(&nest_raw(&inner, r#"{"cty":"JWT"}"#)),
Err(Error::AlgorithmNotAllowed)
),
"{inner}"
);
}
}
#[test]
fn an_outer_cty_other_than_jwt_is_invalid_header() {
let inner = inner_es256();
for protected in [
r#"{}"#,
r#"{"cty":"JOSE"}"#,
r#"{"cty":"at+jwt"}"#,
r#"{"cty":"JWT "}"#,
r#"{"cty":"text/JWT"}"#,
] {
let token = nest_raw(&inner, protected);
assert!(
matches!(open(&token), Err(Error::InvalidHeader(_))),
"{protected}"
);
}
let encrypt_only = nest_raw(&claims().to_json(), r#"{}"#);
assert!(matches!(open(&encrypt_only), Err(Error::InvalidHeader(_))));
}
#[test]
fn an_outer_cty_compares_as_a_media_type() {
let inner = inner_es256();
for cty in ["JWT", "jwt", "application/JWT", "Application/jwt"] {
let token = nest_raw(&inner, &format!(r#"{{"cty":"{cty}"}}"#));
let v = open(&token).unwrap_or_else(|e| panic!("{cty}: {e}"));
assert_eq!(v.outer_header.cty.as_deref(), Some(cty));
}
}
#[test]
fn an_inner_jwe_is_invalid_format() {
let doubly_nested = nest_raw(&nest(&inner_es256()), r#"{"cty":"JWT"}"#);
assert!(matches!(open(&doubly_nested), Err(Error::InvalidFormat(_))));
for plaintext in ["", "a.b", "a.b.c.d", "{\"iss\":\"x\"}"] {
assert!(
matches!(
open(&nest_raw(plaintext, r#"{"cty":"JWT"}"#)),
Err(Error::InvalidFormat(_))
),
"{plaintext:?}"
);
}
}
#[test]
fn an_inner_jws_with_cty_jwt_is_invalid_header() {
let payload = claims().to_json();
for cty in ["JWT", "jwt", "application/jwt"] {
let inner = sign_raw(
&format!(r#"{{"alg":"ES256","cty":"{cty}"}}"#),
payload.as_bytes(),
&es256_key(),
);
assert!(
matches!(open(&nest(&inner)), Err(Error::InvalidHeader(_))),
"{cty}"
);
}
}
#[test]
fn signed_and_nested_jwts_have_separate_entry_points() {
let inner = inner_es256();
assert!(matches!(open(&inner), Err(Error::InvalidFormat(_))));
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap();
let key = public(&es256_key());
assert!(matches!(
jwt::verify(&nest(&inner), Keys::One(&key), &policy),
Err(Error::InvalidFormat(_))
));
}
fn access_token_claims() -> Claims {
let mut c = claims();
c.aud = Some(Audience::One(AUDIENCE.into()));
c.insert("client_id", "s6BhdRkqt3").unwrap();
c.iat = NumericDate::from_secs(EXP - 1_000);
c.jti = Some("dbe39bf3a3ba4238a513f51d6e1691c4".into());
c
}
#[test]
fn explicit_typing_is_checked_on_the_inner_jws() {
let key = es256_key();
let at = jwt::sign(
&access_token_claims(),
&IssueOptions::new(SigningAlg::ES256).access_token(),
&key,
)
.unwrap();
let generic = signed(&access_token_claims(), SigningAlg::ES256, &key);
let clock = FixedClock(NOW);
let policy = Policy::access_token(&clock, &SIGNING, &[ISSUER], &[AUDIENCE]).unwrap();
let verifier = public(&key);
let run = |token: &str| {
jwt::decrypt_nested(
token,
&jwk(KEK),
&decrypt_policy(),
Keys::One(&verifier),
&policy,
)
};
let v = run(&nest(&at)).unwrap();
assert_eq!(v.inner.header.typ.as_deref(), Some("at+jwt"));
assert_eq!(v.outer_header.typ, None);
assert_eq!(
code(run(&nest_raw(&at, r#"{"cty":"JWT","typ":"at+jwt"}"#))),
"ok"
);
for outer in [
r#"{"cty":"JWT","typ":"secevent+jwt"}"#,
r#"{"cty":"JWT","typ":"JWT"}"#,
] {
assert!(
matches!(run(&nest_raw(&at, outer)), Err(Error::InvalidHeader(_))),
"{outer}"
);
}
assert!(matches!(
run(&nest_raw(&generic, r#"{"cty":"JWT","typ":"at+jwt"}"#)),
Err(Error::InvalidHeader(_))
));
}
#[test]
fn the_default_typ_mode_applies_to_the_outer_typ() {
let inner = inner_es256();
for ok in [
r#"{"cty":"JWT","typ":"JWT"}"#,
r#"{"cty":"JWT","typ":"jwt"}"#,
] {
assert_eq!(code(open(&nest_raw(&inner, ok))), "ok", "{ok}");
}
let token = nest_raw(&inner, r#"{"cty":"JWT","typ":"secevent+jwt"}"#);
assert!(matches!(open(&token), Err(Error::InvalidHeader(_))));
}
#[test]
fn an_outer_typ_jwt_fails_an_explicit_type_policy() {
let key = es256_key();
let at = jwt::sign(
&access_token_claims(),
&IssueOptions::new(SigningAlg::ES256).access_token(),
&key,
)
.unwrap();
let clock = FixedClock(NOW);
let verifier = public(&key);
let policies = [
Policy::access_token(&clock, &SIGNING, &[ISSUER], &[AUDIENCE]).unwrap(),
Policy::new(&clock, &SIGNING)
.unwrap()
.audiences(&[AUDIENCE])
.expect_typ("at+jwt"),
];
let omitted = jwt::encrypt_nested(&at, &kw("{}")).unwrap();
let outer_jwt = jwt::encrypt_nested(&at, &kw(r#"{"typ":"JWT"}"#)).unwrap();
let outer_same = jwt::encrypt_nested(&at, &kw(r#"{"typ":"at+jwt"}"#)).unwrap();
assert_eq!(outer_header_of(&omitted).typ, None);
assert_eq!(outer_header_of(&outer_jwt).typ.as_deref(), Some("JWT"));
for policy in &policies {
let run = |token: &str| {
jwt::decrypt_nested(
token,
&jwk(KEK),
&decrypt_policy(),
Keys::One(&verifier),
policy,
)
};
assert_eq!(code(run(&omitted)), "ok");
assert_eq!(code(run(&outer_same)), "ok");
assert_eq!(
run(&outer_jwt).map(|_| ()),
Err(Error::InvalidHeader("typ"))
);
}
}
fn nest_with(inner: &str, members: &str) -> String {
nest_raw(inner, &format!(r#"{{"cty":"JWT",{members}}}"#))
}
#[test]
fn replicated_claims_equal_to_the_inner_ones_pass() {
let mut c = claims();
c.aud = Some(Audience::One("a".into()));
let inner = signed(&c, SigningAlg::ES256, &es256_key());
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap().audience("a");
let key = public(&es256_key());
let run = |token: &str| {
jwt::decrypt_nested(
token,
&jwk(KEK),
&decrypt_policy(),
Keys::One(&key),
&policy,
)
};
for members in [
format!(r#""iss":"{ISSUER}""#),
r#""sub":"alice""#.to_owned(),
r#""aud":"a""#.to_owned(),
format!(r#""iss":"{ISSUER}","sub":"alice","aud":"a""#),
] {
let v = run(&nest_with(&inner, &members)).unwrap_or_else(|e| panic!("{members}: {e}"));
assert!(v.outer_header.contains(members.split('"').nth(1).unwrap()));
}
let mut many = claims();
many.aud = Some(Audience::Many(vec!["a".into(), "b".into()]));
let inner = signed(&many, SigningAlg::ES256, &es256_key());
assert_eq!(code(run(&nest_with(&inner, r#""aud":["a","b"]"#))), "ok");
}
#[test]
fn replicated_claims_that_differ_are_invalid_claim() {
let mut c = claims();
c.aud = Some(Audience::One("a".into()));
let inner = signed(&c, SigningAlg::ES256, &es256_key());
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap().audience("a");
let key = public(&es256_key());
let run = |token: &str| {
jwt::decrypt_nested(
token,
&jwk(KEK),
&decrypt_policy(),
Keys::One(&key),
&policy,
)
};
let cases = [
(r#""iss":"https://evil.example""#, "iss"),
(r#""iss":"HTTPS://ISSUER.EXAMPLE""#, "iss"),
(r#""sub":"mallory""#, "sub"),
(r#""aud":["a"]"#, "aud"),
(r#""aud":"b""#, "aud"),
(r#""aud":1"#, "aud"),
];
for (members, name) in cases {
assert_eq!(
run(&nest_with(&inner, members)),
Err(Error::InvalidClaim(name)),
"{members}"
);
}
assert_eq!(code(run(&nest_with(&inner, r#""jti":"x""#))), "ok");
let mut no_sub = c.clone();
no_sub.sub = None;
let inner = signed(&no_sub, SigningAlg::ES256, &es256_key());
assert_eq!(
run(&nest_with(&inner, r#""sub":"alice""#)),
Err(Error::InvalidClaim("sub"))
);
}
#[test]
fn the_layers_fail_in_the_adr_0027_order() {
let other = generate(KeySpec::Ec(EcCurve::P256)).unwrap();
let bad_signature = signed(&claims(), SigningAlg::ES256, &other);
assert_eq!(
open(&flip_tag(&nest(&bad_signature))),
Err(Error::DecryptionFailed)
);
let no_cty = nest_raw(&inner_es256(), r#"{}"#);
assert!(matches!(
open_at(&no_cty, LATE),
Err(Error::InvalidHeader(_))
));
assert!(matches!(
open(&nest_raw(&bad_signature, r#"{}"#)),
Err(Error::InvalidHeader(_))
));
let mismatch = nest_with(&inner_es256(), r#""iss":"https://evil.example""#);
assert_eq!(open_at(&mismatch, LATE), Err(Error::TokenExpired));
assert_eq!(open_at(&mismatch, NOW), Err(Error::InvalidClaim("iss")));
let mismatch = nest_with(&bad_signature, r#""iss":"https://evil.example""#);
assert_eq!(code(open(&mismatch)), "InvalidSignature");
let only_a256gcm = DecryptPolicy::new(&[A::A128KW], &[E::A256GCM]).unwrap();
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap();
let key = public(&es256_key());
assert!(matches!(
jwt::decrypt_nested(&no_cty, &jwk(KEK), &only_a256gcm, Keys::One(&key), &policy),
Err(Error::AlgorithmNotAllowed)
));
}
#[test]
fn a_json_jwe_is_invalid_format() {
let inner = inner_es256();
let recipient = || Recipient::new(A::A128KW, &jwk(KEK));
let protected_cty = || encryptor(E::A128GCM, recipient(), header(r#"{"cty":"JWT"}"#));
let unprotected_cty = || {
Encryptor::new(E::A128GCM)
.recipient(recipient())
.shared_unprotected_header(header(r#"{"cty":"JWT"}"#))
};
for form in [JsonForm::Flattened, JsonForm::General] {
for e in [protected_cty(), unprotected_cty()] {
let jwe = e.encrypt_json(inner.as_bytes(), form).unwrap();
assert!(
matches!(open(&jwe), Err(Error::InvalidFormat(_))),
"{form:?} {jwe}"
);
let spaced = format!("\r\n {jwe}");
assert!(matches!(open(&spaced), Err(Error::InvalidFormat(_))));
}
}
}
#[test]
fn the_inner_token_never_appears_in_debug_or_errors() {
const MARKER: &str = "MARKER-7c1f2b9e-secret-claim-value";
let mut c = claims();
c.iss = Some(format!("{MARKER}-iss"));
c.sub = Some(format!("{MARKER}-sub"));
c.aud = Some(Audience::Many(vec![
AUDIENCE.into(),
format!("{MARKER}-aud"),
]));
c.jti = Some(format!("{MARKER}-jti"));
c.insert("secret", MARKER).unwrap();
let inner = signed(&c, SigningAlg::ES256, &es256_key());
let key = public(&es256_key());
let open_at = |token: &str, now: i64| {
let clock = FixedClock(now);
let policy = Policy::new(&clock, &SIGNING).unwrap().audience(AUDIENCE);
jwt::decrypt_nested(
token,
&jwk(KEK),
&decrypt_policy(),
Keys::One(&key),
&policy,
)
};
let open = |token: &str| open_at(token, NOW);
let payload_b64 = inner.split('.').nth(1).unwrap().to_owned();
let signature_b64 = inner.split('.').nth(2).unwrap().to_owned();
let leaks = |text: &str| {
text.contains(MARKER)
|| text.contains(&inner)
|| text.contains(&payload_b64)
|| text.contains(&signature_b64)
};
let v = open(&nest(&inner)).unwrap();
assert_eq!(v.inner.claims, c);
let debug = format!("{v:?} {v:#?} {:?}", v.inner);
assert!(!leaks(&debug), "{debug}");
let other = generate(KeySpec::Ec(EcCurve::P256)).unwrap();
let errors = [
open(&nest_with(&inner, r#""iss":"https://evil.example""#)),
open_at(&nest(&inner), LATE),
open(&nest(&signed(&c, SigningAlg::ES256, &other))),
open(&nest(&sign_raw(
r#"{"alg":"ES256","cty":"JWT"}"#,
c.to_json().as_bytes(),
&es256_key(),
))),
open(&nest_raw(&inner, r#"{}"#)),
open(&flip_tag(&nest(&inner))),
];
for r in errors {
let e = r.unwrap_err();
let text = format!("{e} {e:?} {}", e.code());
assert!(!leaks(&text), "{text}");
}
}
#[test]
fn the_pbes2_budget_applies_to_nested_decryption() {
let password =
Jwk::from_json(format!(r#"{{"kty":"oct","k":"{}"}}"#, b64url_encode(PASSWORD)).as_bytes())
.unwrap();
let token = jwt::encrypt_nested(
&inner_es256(),
&Encryptor::new(E::A128GCM)
.recipient(Recipient::new(A::Pbes2Hs256A128KW, &password).p2c(1_000)),
)
.unwrap();
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &SIGNING).unwrap();
let key = public(&es256_key());
let run = |budget: u32| {
let dp = DecryptPolicy::new(&[A::Pbes2Hs256A128KW], &[E::A128GCM])
.unwrap()
.max_pbes2_count(budget);
jwt::decrypt_nested(&token, &password, &dp, Keys::One(&key), &policy)
};
assert_eq!(run(999).unwrap_err(), Error::DecryptionFailed);
assert_eq!(run(1).unwrap_err(), Error::DecryptionFailed);
assert!(run(1_000).is_ok());
assert!(run(10_000).is_ok());
}
#[test]
fn encrypt_nested_error_order_is_exact_for_every_pair() {
let good = inner_es256();
let hdr = |h: &str| format!("{}.eyJzdWIiOiJ4In0.c2ln", b64url_encode(h.as_bytes()));
let none = hdr(r#"{"alg":"none"}"#);
let no_alg = hdr("{}");
let bad_shape = "a.b".to_string();
let bad_cty = || kw(r#"{"cty":"JOSE"}"#);
let zipped = || kw(r#"{"zip":"DEF"}"#);
let enc_fail = || kw("{}").aad(b"aad".to_vec());
let both = |c: &str| kw(&format!(r#"{{"cty":"JOSE","zip":"{c}"}}"#));
let cases: Vec<(&str, Encryptor, &str)> = vec![
(&bad_shape, kw("{}"), "InvalidFormat"),
(&bad_shape, bad_cty(), "InvalidFormat"),
(&bad_shape, zipped(), "InvalidFormat"),
(&bad_shape, enc_fail(), "InvalidFormat"),
(&none, bad_cty(), "AlgorithmNotAllowed"),
(&none, zipped(), "AlgorithmNotAllowed"),
(&none, enc_fail(), "AlgorithmNotAllowed"),
(&no_alg, bad_cty(), "InvalidHeader"),
(&no_alg, zipped(), "InvalidHeader"),
(&good, both("DEF"), "InvalidHeader"),
(&good, bad_cty().aad(b"x".to_vec()), "InvalidHeader"),
(&good, zipped().aad(b"x".to_vec()), "InvalidHeader"),
(&good, enc_fail(), "ok_or_other"),
];
for (input, e, want) in cases {
let got = jwt::encrypt_nested(input, &e);
if want == "ok_or_other" {
assert!(got.is_err(), "encrypt failure must surface");
continue;
}
assert_eq!(code(got), want, "{input}");
}
assert_eq!(
jwt::encrypt_nested(&no_alg, &bad_cty()),
Err(Error::InvalidHeader("alg"))
);
assert_eq!(
jwt::encrypt_nested(&good, &both("DEF")),
Err(Error::InvalidHeader("cty"))
);
assert_eq!(
jwt::encrypt_nested(&good, &zipped().aad(b"x".to_vec())),
Err(Error::InvalidHeader("zip"))
);
assert_eq!(
jwt::encrypt_nested(&none, &zipped()),
Err(Error::AlgorithmNotAllowed)
);
}
#[test]
fn encrypt_nested_never_decodes_the_payload() {
let h = |s: &str| b64url_encode(s.as_bytes());
for payload in ["A", "QR", "AAAAA", "_-_-_"] {
let ok = format!("{}.{payload}.c2ln", h(r#"{"alg":"ES256"}"#));
assert_eq!(code(jwt::encrypt_nested(&ok, &kw("{}"))), "ok", "{payload}");
let none = format!("{}.{payload}.c2ln", h(r#"{"alg":"none"}"#));
assert_eq!(
jwt::encrypt_nested(&none, &kw("{}")),
Err(Error::AlgorithmNotAllowed),
"{payload}"
);
}
let empty = format!("{}..c2ln", h(r#"{"alg":"ES256"}"#));
assert_eq!(code(jwt::encrypt_nested(&empty, &kw("{}"))), "ok");
}
#[test]
fn encrypt_nested_inner_alg_edge_spellings() {
let hdr = |h: &str| format!("{}.eyJ9.c2ln", b64url_encode(h.as_bytes()));
for h in [
r#"{"alg":"none","alg":"ES256"}"#,
r#"{"alg":"ES256","alg":"none"}"#,
] {
assert!(
matches!(
jwt::encrypt_nested(&hdr(h), &kw("{}")),
Err(Error::InvalidFormat(_))
),
"{h}"
);
}
for h in [r#"{"alg":null}"#, r#"{"alg":1}"#, r#"{"alg":true}"#] {
assert_eq!(
jwt::encrypt_nested(&hdr(h), &kw("{}")),
Err(Error::InvalidHeader("alg")),
"{h}"
);
}
assert_eq!(
jwt::encrypt_nested(&hdr(r#"{"alg":"none"}"#), &kw("{}")),
Err(Error::AlgorithmNotAllowed)
);
assert_eq!(
jwt::encrypt_nested(&hdr("{\"alg\":\"\\u006eone\"}"), &kw("{}")),
Err(Error::AlgorithmNotAllowed)
);
}