use crate::json::Object;
use crate::jwa::SigningAlg;
use crate::jwk::{EcCurve, Jwk, KeyAlg, KeyMaterial};
use crate::test_support::wycheproof::{self, Deviation, Expected, Group, Outcome, Suite, Test};
use crate::{Error, Result};
use super::{ProtectedHeader, Verified, VerifyOptions, verify_compact, verify_json};
const SUITE: &str = "json_web_signature";
const DEVIATIONS: &[Deviation] = &[
Deviation::tests(
&[346, 350],
Outcome::Reject,
"RFC 8725 §3.1, RFC 7517 §4.4, ADR 0026 §2 steps 7-8: Wycheproof pairs the \
RFC 7520 §4.2 PS384 token (Figure 20) with a key whose \"alg\" is \"PS256\", so \
PS384 is not the group's algorithm (AlgorithmNotAllowed); with PS384 allowed, the \
key's alg still differs from the header's (KeyAlgorithmMismatch)",
),
Deviation::tests(
&[347, 351],
Outcome::Reject,
"RFC 7518 §3.1, RFC 7517 §4.4, ADR 0026 §2 step 8: Wycheproof pairs the \
RFC 7520 §4.3 ES512 token (Figure 27) with a key whose \"alg\" is \"ES521\", which \
is no registered JWS algorithm; a key's alg must equal the header's \
(KeyAlgorithmMismatch)",
),
Deviation::tests(
&[372, 373],
Outcome::Reject,
"RFC 7515 §2, §5.2 steps 2 and 6, ADR 0026 §2 step 3: base64url admits no \
additional characters, so a '?' inside the header or payload part is \
InvalidBase64 (upstream marks these valid, as a decoder that skips unknown \
characters would)",
),
];
const UPSTREAM_DUPLICATES: &[(u32, u32, &str)] = &[
(
367,
357,
"RFC 7515 §5.2: \"invalidBase64Padding\" carries no padding; it is tcId 357 byte for byte",
),
(
370,
357,
"RFC 7515 §5.2: \"invalidBase64PaddingInPayload\" carries no padding; it is tcId 357 \
byte for byte",
),
];
fn group_alg(key: &Jwk) -> SigningAlg {
if let Some(KeyAlg::Signing(alg)) = key.alg() {
return alg.clone();
}
match key.material() {
KeyMaterial::Rsa(_) => SigningAlg::RS256,
KeyMaterial::Ec(ec) if ec.crv() == EcCurve::P256 => SigningAlg::ES256,
KeyMaterial::Ec(ec) if ec.crv() == EcCurve::P521 => SigningAlg::ES512,
_ => panic!("no algorithm rule for this group's key"),
}
}
fn verification_key(group: &Group) -> Jwk {
Jwk::from_json(group.verification_jwk().as_bytes())
.unwrap_or_else(|e| panic!("group {:?}: key import failed: {e}", group.comment))
}
fn verify(group: &Group, test: &Test) -> Result<Verified> {
let key = verification_key(group);
let allowed = [group_alg(&key)];
verify_compact(test.jws(), &key, &VerifyOptions::new(&allowed)?)
}
fn split_off_duplicates() -> (Suite, Vec<(usize, Test)>) {
let mut suite = wycheproof::load(SUITE);
let mut removed = Vec::new();
for &(tc_id, twin_id, reason) in UPSTREAM_DUPLICATES {
assert!(
reason.starts_with("RFC "),
"tcId {tc_id}: reason cites no RFC"
);
let (index, group) = suite
.groups
.iter_mut()
.enumerate()
.find(|(_, g)| g.tests.iter().any(|t| t.tc_id == tc_id))
.unwrap_or_else(|| panic!("tcId {tc_id} is not in the suite"));
let twin = group
.tests
.iter()
.find(|t| t.tc_id == twin_id)
.unwrap_or_else(|| panic!("tcId {twin_id} is not in tcId {tc_id}'s group"));
assert_eq!(twin.result, Expected::Valid, "tcId {twin_id} must be valid");
let twin_jws = twin.jws().to_owned();
let at = group.tests.iter().position(|t| t.tc_id == tc_id).unwrap();
let test = group.tests.remove(at);
assert_eq!(
test.result,
Expected::Invalid,
"tcId {tc_id} must be invalid"
);
assert_eq!(
test.jws(),
twin_jws,
"tcId {tc_id} is no longer identical to tcId {twin_id}: drop it from \
UPSTREAM_DUPLICATES"
);
removed.push((index, test));
}
(suite, removed)
}
#[test]
fn wycheproof_json_web_signature() {
let (suite, removed) = split_off_duplicates();
let summary = wycheproof::assert_conforms(&suite, DEVIATIONS, |group, test| {
Outcome::of(&verify(group, test))
});
assert_eq!(summary.tests + removed.len(), 401);
assert_eq!(summary.deviations, 6);
}
#[test]
fn upstream_duplicates_verify_like_their_valid_twin() {
let (suite, removed) = split_off_duplicates();
assert_eq!(removed.len(), UPSTREAM_DUPLICATES.len());
for (index, test) in &removed {
let verified = verify(&suite.groups[*index], test)
.unwrap_or_else(|e| panic!("tcId {}: {e}", test.tc_id));
assert_eq!(*verified.payload, b"Test", "tcId {}", test.tc_id);
assert_eq!(verified.alg, SigningAlg::HS256);
}
}
const ERRORS: &[(&[u32], &str, &str)] = &[
(
&[
4, 7, 10, 12, 13, 14, 15, 17, 21, 24, 27, 29, 30, 36, 39, 42, 44, 45,
],
"InvalidFormat",
"step 2, RFC 7515 §7.1: not three parts (tcId 17 is a JSON JWS given to the compact API)",
),
(
&[
360, 361, 362, 363, 364, 365, 366, 368, 369, 371, 372, 373, 374, 375,
],
"InvalidBase64",
"step 3, RFC 7515 §2: spaces, '?', '#', or non-zero unused bits (RFC 4648 §3.5)",
),
(
&[9, 11, 26, 28, 41, 43],
"InvalidJson",
"step 4, RFC 7515 §5.2 step 3: an empty header part is no JSON object",
),
(
&[342],
"UnsupportedAlgorithm",
"step 6, RFC 7515 §4.1.1: \"NONE\" is no algorithm; names are case-sensitive",
),
(
&[16, 31, 332, 334, 336, 338, 340, 341, 343, 344, 346, 350],
"AlgorithmNotAllowed",
"step 7, RFC 8725 §3.1: the header alg is not the group's (or is none)",
),
(
&[347, 351, 353, 354, 355, 356],
"KeyAlgorithmMismatch",
"step 8, RFC 7517 §4.2-§4.4: the key's alg differs, its use is enc, or its \
key_ops lack verify",
),
];
#[test]
fn every_rejection_fails_at_its_adr_0026_step() {
let suite = wycheproof::load(SUITE);
let mut listed = std::collections::BTreeMap::new();
for (ids, code, why) in ERRORS {
assert!(why.contains("RFC "), "{code}: the reason cites no RFC");
for id in *ids {
assert!(
listed.insert(*id, *code).is_none(),
"tcId {id} listed twice"
);
}
}
let mut seen = 0;
for (group, test) in suite.tests() {
let id = test.tc_id;
match (verify(group, test), listed.get(&id)) {
(Ok(_), None) => {}
(Ok(_), Some(code)) => panic!("tcId {id} verified; ERRORS expects {code}"),
(Err(e), code) => {
assert_eq!(e.code(), *code.unwrap_or(&"InvalidSignature"), "tcId {id}");
}
}
seen += usize::from(listed.contains_key(&id));
}
assert_eq!(
seen,
listed.len(),
"ERRORS lists a tcId that is not in the suite"
);
}
fn find(suite: &Suite, tc_id: u32) -> (&Group, &Test) {
suite
.tests()
.find(|(_, t)| t.tc_id == tc_id)
.unwrap_or_else(|| panic!("tcId {tc_id} is not in the suite"))
}
fn flattened(compact: &str) -> Option<String> {
let parts: Vec<&str> = compact.split('.').collect();
let [protected, payload, signature] = parts[..] else {
return None;
};
let mut jws = Object::new();
jws.insert("payload", payload);
jws.insert("protected", protected);
jws.insert("signature", signature);
Some(jws.to_json())
}
#[test]
fn verify_json_agrees_with_verify_compact() {
let suite = wycheproof::load(SUITE);
let mut compared = 0;
for (group, test) in suite.tests() {
let Some(json) = flattened(test.jws()) else {
continue;
};
let key = verification_key(group);
let allowed = [group_alg(&key)];
let opts = VerifyOptions::new(&allowed).unwrap();
let compact = verify_compact(test.jws(), &key, &opts);
let json = verify_json(&json, &key, &opts);
let id = test.tc_id;
assert_eq!(Outcome::of(&json), Outcome::of(&compact), "tcId {id}");
if let (Ok(json), Ok(compact)) = (json, compact) {
assert_eq!(json.payload, compact.payload, "tcId {id}");
assert_eq!(json.protected, compact.protected, "tcId {id}");
}
compared += 1;
}
assert_eq!(compared, 383);
}
#[test]
fn json_serialization_case_through_verify_json() {
let suite = wycheproof::load(SUITE);
let (group, test) = find(&suite, 17);
assert!(test.has_flag("JsonSerialization"));
let key = verification_key(group);
let allowed = [SigningAlg::HS256];
let opts = VerifyOptions::new(&allowed).unwrap();
assert!(!test.jws().ends_with("]}"));
let error = verify_json(test.jws(), &key, &opts).map(drop).unwrap_err();
assert_eq!(error.code(), "InvalidJson");
let verified = verify_json(&format!("{}]}}", test.jws()), &key, &opts).unwrap();
assert_eq!(*verified.payload, b"foo");
assert_eq!(verified.alg, SigningAlg::HS256);
assert_eq!(verified.signature_index, 0);
assert!(verified.unprotected.unwrap().contains("unknown"));
}
#[test]
fn private_jwks_import_and_verify_like_the_public_ones() {
let suite = wycheproof::load(SUITE);
let (mut rsa, mut others, mut refused) = (0, 0, 0);
for group in &suite.groups {
let keys = group.jws_keys.as_ref().unwrap();
let private = Jwk::from_json(keys.private.as_bytes()).unwrap();
let public = verification_key(group);
if let KeyMaterial::Rsa(key) = private.material() {
assert!(key.is_private() && !private.is_public());
let KeyMaterial::Rsa(expected) = public.material() else {
panic!("an RSA group's public key is RSA");
};
assert_eq!((key.n(), key.e()), (expected.n(), expected.e()));
rsa += 1;
} else {
others += 1;
}
let allowed = [group_alg(&public)];
let opts = VerifyOptions::new(&allowed).unwrap();
let may_verify = private.permits(&crate::jwk::KeyOp::Verify);
if !may_verify {
refused += 1;
}
for test in &group.tests {
let with_private = verify_compact(test.jws(), &private, &opts).map(|v| v.payload);
if may_verify {
let with_public = verify_compact(test.jws(), &public, &opts).map(|v| v.payload);
assert_eq!(with_private, with_public, "tcId {}", test.tc_id);
} else {
assert_eq!(
with_private.map(drop),
Err(Error::KeyAlgorithmMismatch),
"tcId {}",
test.tc_id
);
}
}
}
assert_eq!((rsa, others, refused), (13, 10, 5));
}
#[test]
fn allowing_the_header_alg_still_fails_key_binding() {
let suite = wycheproof::load(SUITE);
let header_alg = |test: &Test| {
let protected = test.jws().split('.').next().unwrap();
let header = ProtectedHeader::from_encoded(protected).unwrap();
SigningAlg::from(header.header().alg.as_deref().unwrap())
};
for id in [31, 332, 334, 336, 338, 340, 346, 350] {
let (group, test) = find(&suite, id);
let key = verification_key(group);
let alg = header_alg(test);
assert_ne!(alg, group_alg(&key), "tcId {id}");
let allowed = [alg];
let result = verify_compact(test.jws(), &key, &VerifyOptions::new(&allowed).unwrap());
assert_eq!(
result.map(drop),
Err(Error::KeyAlgorithmMismatch),
"tcId {id}"
);
}
for id in 341..=344 {
let alg = header_alg(find(&suite, id).1);
assert_eq!(
VerifyOptions::new(&[alg]).map(drop),
Err(Error::AlgorithmNotAllowed),
"tcId {id}"
);
}
}