use zeroize::Zeroizing;
use crate::error::{Error, Result};
use crate::header::Header;
use crate::json::{self, Value};
use crate::jwa::SigningAlg;
#[cfg(doc)]
use crate::jwe::Decrypted;
use crate::jwe::{DecryptPolicy, Encryptor, decrypt_compact};
use crate::jwk::Jwk;
use crate::util::b64url_decode;
use super::typ::{JWT, Layer, check_jwt_header, media_type_eq};
use super::verify::verify_inner;
use super::{Audience, Claims, Keys, Policy, VerifiedJwt};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct VerifiedNestedJwt {
pub inner: VerifiedJwt,
pub outer_header: Header,
pub recipient: usize,
}
pub fn encrypt_nested(signed_jwt: &str, encryptor: &Encryptor) -> Result<String> {
let inner_header = check_compact_jws_shape(signed_jwt)?;
check_inner_alg(inner_header)?;
let mut protected = encryptor.protected().cloned().unwrap_or_default();
if protected
.cty
.as_deref()
.is_some_and(|cty| !media_type_eq(cty, JWT))
{
return Err(Error::InvalidHeader("cty"));
}
if encryptor.compresses() || protected.zip.is_some() {
return Err(Error::InvalidHeader("zip"));
}
protected.cty = Some(JWT.into());
encryptor
.clone()
.protected_header(protected)
.encrypt_compact(signed_jwt.as_bytes())
}
fn check_compact_jws_shape(token: &str) -> Result<&str> {
const NOT_A_JWS: Error = Error::InvalidFormat("a nested JWT encrypts a compact JWS");
let mut parts = token.split('.');
let (Some(header), Some(_), Some(signature), None) =
(parts.next(), parts.next(), parts.next(), parts.next())
else {
return Err(NOT_A_JWS);
};
let base64url = |b: u8| b.is_ascii_alphanumeric() || b == b'-' || b == b'_';
if header.is_empty()
|| signature.is_empty()
|| !token.bytes().all(|b| b == b'.' || base64url(b))
{
return Err(NOT_A_JWS);
}
Ok(header)
}
fn check_inner_alg(encoded: &str) -> Result<()> {
const UNDECODABLE: Error =
Error::InvalidFormat("the inner JWS Protected Header is not base64url JSON");
let json = b64url_decode(encoded).map_err(|_| UNDECODABLE)?;
let header = json::parse(&json)
.ok()
.and_then(Value::into_object)
.ok_or(UNDECODABLE)?;
match header.get("alg").and_then(Value::as_str) {
None => Err(Error::InvalidHeader("alg")),
Some(alg) if SigningAlg::from(alg) == SigningAlg::None => Err(Error::AlgorithmNotAllowed),
Some(_) => Ok(()),
}
}
pub fn decrypt_nested(
token: &str,
decrypt_key: &Jwk,
decrypt_policy: &DecryptPolicy,
keys: Keys<'_>,
policy: &Policy<'_>,
) -> Result<VerifiedNestedJwt> {
if is_json_serialization(token) {
return Err(Error::InvalidFormat("a JWT uses the compact serialization"));
}
let decrypted = decrypt_compact(token, decrypt_key, decrypt_policy)?;
let outer_header = decrypted
.protected_header()
.cloned()
.ok_or(Error::InvalidHeader("cty"))?;
check_outer(
&outer_header,
&[decrypted.shared_header(), decrypted.recipient_header()],
policy,
)?;
let recipient = decrypted.recipient();
let plaintext: Zeroizing<Vec<u8>> = decrypted.into_plaintext();
let inner_token = core::str::from_utf8(&plaintext)
.map_err(|_| Error::InvalidFormat("the inner token is not a compact JWS"))?;
let inner = verify_inner(inner_token, keys, policy)?;
check_replicated_claims(&outer_header, &inner.claims)?;
Ok(VerifiedNestedJwt {
inner,
outer_header,
recipient,
})
}
fn is_json_serialization(token: &str) -> bool {
token
.trim_start_matches([' ', '\t', '\n', '\r'])
.starts_with('{')
}
fn check_outer(protected: &Header, unprotected: &[Option<&Header>], p: &Policy<'_>) -> Result<()> {
for header in unprotected.iter().flatten() {
check_jwt_header(protected, Some(header), p, Layer::Outer)?;
}
check_jwt_header(protected, None, p, Layer::Outer)
}
fn check_replicated_claims(outer: &Header, claims: &Claims) -> Result<()> {
let string = |s: &Option<String>| s.as_deref().map(Value::from);
let audience = |aud: &Audience| match aud {
Audience::One(one) => Value::from(one.as_str()),
Audience::Many(many) => {
Value::Array(many.iter().map(|a| Value::from(a.as_str())).collect())
}
};
let replicated: [(&'static str, Option<Value>); 3] = [
("iss", string(&claims.iss)),
("sub", string(&claims.sub)),
("aud", claims.aud.as_ref().map(audience)),
];
for (name, inner) in replicated {
if let Some(outer_value) = outer.other_params().get(name)
&& inner.as_ref() != Some(outer_value)
{
return Err(Error::InvalidClaim(name));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jwa::{ContentEncAlg, KeyMgmtAlg, SigningAlg};
use crate::jwe::{JsonForm, Recipient, decrypt_json};
use crate::jws::{ProtectedHeader, SignOptions, sign_compact};
use crate::jwt::FixedClock;
const OCT_64: &str = r#"{"kty":"oct","k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"}"#;
const KEK: &str = r#"{"kty":"oct","k":"GZy6sIZ6wl9NJOKB-jnmVQ"}"#;
const HS256: [SigningAlg; 1] = [SigningAlg::HS256];
const NOW: i64 = 1_300_819_320;
fn jwk(json: &str) -> Jwk {
Jwk::from_json(json.as_bytes()).expect("test key")
}
fn decrypt_policy() -> DecryptPolicy {
DecryptPolicy::new(&[KeyMgmtAlg::A128KW], &[ContentEncAlg::A128GCM]).expect("policy")
}
fn header(json: &str) -> Header {
Header::from_json(json.as_bytes()).expect("header")
}
fn inner(claims: &str) -> String {
let mut h = Header::new();
h.alg = Some("HS256".into());
let protected = ProtectedHeader::new(h).expect("protected header");
sign_compact(
&protected,
claims.as_bytes(),
&jwk(OCT_64),
&SignOptions::new(),
)
.expect("JWS")
}
fn encryptor(protected: Option<Header>) -> Encryptor {
let e = Encryptor::new(ContentEncAlg::A128GCM)
.recipient(Recipient::new(KeyMgmtAlg::A128KW, &jwk(KEK)));
match protected {
Some(h) => e.protected_header(h),
None => e,
}
}
fn nested(claims: &str, protected: &str) -> String {
encryptor(Some(header(protected)))
.encrypt_compact(inner(claims).as_bytes())
.expect("JWE")
}
fn run(token: &str) -> Result<VerifiedNestedJwt> {
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &HS256).expect("policy");
let key = jwk(OCT_64);
decrypt_nested(
token,
&jwk(KEK),
&decrypt_policy(),
Keys::One(&key),
&policy,
)
}
const CLAIMS: &str = r#"{"iss":"joe","sub":"s","exp":1300819380}"#;
fn outer_header_of(token: &str) -> Header {
let first = token.split('.').next().expect("a part");
let json = crate::util::b64url_decode(first).expect("base64url");
Header::from_json(&json).expect("header")
}
#[test]
fn encrypt_nested_adds_cty_jwt_to_a_copy() {
let jws = inner(CLAIMS);
for (caller, typ) in [
(None, None),
(
Some(r#"{"typ":"JWT","kid":"k1","x-tenant":"a"}"#),
Some("JWT"),
),
(Some(r#"{"cty":"JWT"}"#), None),
(Some(r#"{"cty":"jwt"}"#), None),
(Some(r#"{"cty":"application/JWT"}"#), None),
] {
let e = encryptor(caller.map(header));
let before = e.protected().cloned();
let token = encrypt_nested(&jws, &e).expect("nested JWT");
assert_eq!(
e.protected().cloned(),
before,
"{caller:?}: caller unchanged"
);
let h = outer_header_of(&token);
assert_eq!(h.cty.as_deref(), Some("JWT"), "{caller:?}");
assert_eq!(h.typ.as_deref(), typ, "{caller:?}");
if caller.is_some_and(|c| c.contains("kid")) {
assert_eq!(h.kid.as_deref(), Some("k1"));
assert!(h.other_params().contains("x-tenant"));
}
let v = run(&token).expect("round trip");
assert_eq!(v.outer_header, h, "{caller:?}");
}
}
#[test]
fn encrypt_nested_refuses_a_different_caller_cty() {
let jws = inner(CLAIMS);
for cty in ["JOSE", "at+jwt", "JWT ", "text/JWT", "application/jose"] {
let e = encryptor(Some(header(&format!(r#"{{"cty":"{cty}"}}"#))));
assert!(
matches!(encrypt_nested(&jws, &e), Err(Error::InvalidHeader(_))),
"{cty:?}"
);
}
}
#[test]
fn encrypt_nested_needs_a_three_part_compact_jws() {
let jws = inner(CLAIMS);
let jwe = encrypt_nested(&jws, &encryptor(None)).expect("nested JWT");
let bad_cty = encryptor(Some(header(r#"{"cty":"JOSE"}"#)));
for input in [
"",
"a",
"a.b",
"a.b.c.d",
jwe.as_str(),
".b.c",
"a.b+.c",
"a.b.c=",
"a.b.",
" a.b.c",
"{\"payload\":\"x.y.z\"}",
] {
for e in [encryptor(None), bad_cty.clone()] {
assert!(
matches!(encrypt_nested(input, &e), Err(Error::InvalidFormat(_))),
"{input:?}"
);
}
}
}
#[test]
fn a_nested_jwt_decrypts_and_verifies() {
let token = nested(CLAIMS, r#"{"cty":"JWT"}"#);
let v = run(&token).expect("nested JWT");
assert_eq!(v.inner.claims.iss.as_deref(), Some("joe"));
assert_eq!(v.inner.alg, SigningAlg::HS256);
assert_eq!(v.recipient, 0);
assert_eq!(v.outer_header.cty.as_deref(), Some("JWT"));
assert_eq!(v.outer_header.alg.as_deref(), Some("A128KW"));
}
#[test]
fn an_unprotected_cty_or_typ_fails_the_outer_check() {
let clock = FixedClock(NOW);
let policy = Policy::new(&clock, &HS256).expect("policy");
let cases = [
(None, r#"{"cty":"JWT"}"#),
(Some(r#"{"typ":"JWT"}"#), r#"{"cty":"JWT"}"#),
(Some(r#"{"cty":"JWT"}"#), r#"{"typ":"JWT"}"#),
(Some(r#"{"cty":"JWT"}"#), r#"{"typ":"at+jwt"}"#),
];
for (protected, shared) in cases {
let jwe = encryptor(protected.map(header))
.shared_unprotected_header(header(shared))
.encrypt_json(inner(CLAIMS).as_bytes(), JsonForm::Flattened)
.expect("flattened JWE");
let d = decrypt_json(&jwe, &jwk(KEK), &decrypt_policy()).expect("0003 decrypts");
assert!(d.shared_header().is_some());
let empty = Header::new();
let protected_header = d.protected_header().unwrap_or(&empty);
assert!(
matches!(
check_outer(
protected_header,
&[d.shared_header(), d.recipient_header()],
&policy
),
Err(Error::InvalidHeader(_))
),
"{protected:?} {shared}"
);
assert!(matches!(run(&jwe), Err(Error::InvalidFormat(_))));
}
let protected = header(r#"{"alg":"A128KW","enc":"A128GCM","cty":"JWT"}"#);
assert_eq!(check_outer(&protected, &[None, None], &policy), Ok(()));
}
#[test]
fn replicated_claims_must_be_json_equal() {
let claims = Claims::from_json(br#"{"iss":"joe","sub":"s","aud":"a"}"#).expect("claims");
let many = Claims::from_json(br#"{"aud":["a","b"]}"#).expect("claims");
let ok = [
(r#"{}"#, &claims),
(r#"{"iss":"joe"}"#, &claims),
(r#"{"iss":"joe","sub":"s","aud":"a"}"#, &claims),
(r#"{"aud":["a","b"]}"#, &many),
(r#"{"jti":"x","x-other":1}"#, &claims),
];
for (h, c) in ok {
assert_eq!(check_replicated_claims(&header(h), c), Ok(()), "{h}");
}
let bad = [
(r#"{"iss":"Joe"}"#, &claims, "iss"),
(r#"{"iss":null}"#, &claims, "iss"),
(r#"{"iss":["joe"]}"#, &claims, "iss"),
(r#"{"sub":"t"}"#, &claims, "sub"),
(r#"{"iss":"joe","sub":1}"#, &claims, "sub"),
(r#"{"aud":["a"]}"#, &claims, "aud"),
(r#"{"aud":"a"}"#, &many, "aud"),
(r#"{"aud":["b","a"]}"#, &many, "aud"),
(r#"{"iss":"joe"}"#, &many, "iss"),
];
for (h, c, name) in bad {
assert_eq!(
check_replicated_claims(&header(h), c),
Err(Error::InvalidClaim(name)),
"{h}"
);
}
}
#[test]
fn a_json_serialization_is_invalid_format() {
for token in [
"{}",
" \t\r\n{\"protected\":\"a.b.c.d\"}",
"{\"a\":\"x.y.z.w.v\"}",
] {
assert!(
matches!(run(token), Err(Error::InvalidFormat(_))),
"{token:?}"
);
}
}
#[test]
fn a_non_utf8_inner_plaintext_is_invalid_format() {
let token = encryptor(Some(header(r#"{"cty":"JWT"}"#)))
.encrypt_compact(b"\xff.\xfe.\xfd")
.expect("JWE");
assert!(matches!(run(&token), Err(Error::InvalidFormat(_))));
}
}