use crate::error::{Error, Result};
use crate::jwk::{Jwk, JwkSet};
use crate::jws::{CompactPrechecked, FAMILIES, Families, Verified, finish_compact, furthest};
use super::Policy;
pub(crate) fn select_and_verify(
pre: &CompactPrechecked<'_>,
set: &JwkSet,
p: &Policy<'_>,
) -> Result<(Verified, usize)> {
select_and_verify_using(&FAMILIES, pre, set, p)
}
fn select_and_verify_using(
families: &Families,
pre: &CompactPrechecked<'_>,
set: &JwkSet,
p: &Policy<'_>,
) -> Result<(Verified, usize)> {
let candidates = candidates(pre, set, p.key_candidate_limit())?;
let mut failures = Vec::with_capacity(candidates.len());
for (index, key) in candidates {
match finish_compact(families, pre, key) {
Ok(verified) => return Ok((verified, index)),
Err(error) => failures.push(error),
}
}
debug_assert!(
!failures.is_empty(),
"candidates() returns at least one key or NoMatchingKey (ADR 0029 §4)"
);
Err(furthest(failures).unwrap_or(Error::NoMatchingKey))
}
fn candidates<'a>(
pre: &'a CompactPrechecked<'_>,
set: &'a JwkSet,
limit: usize,
) -> Result<Vec<(usize, &'a Jwk)>> {
let kid = pre.header().kid.as_deref();
let found: Vec<(usize, &Jwk)> = set
.verification_candidates(pre.alg(), kid)
.take(limit.saturating_add(1))
.collect();
if found.is_empty() || found.len() > limit {
return Err(Error::NoMatchingKey);
}
Ok(found)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::jwa::SigningAlg as A;
use crate::jws::precheck_compact;
use crate::jws::{ProtectedHeader, SignOptions, sign_compact};
use crate::jwt::FixedClock;
use crate::test_counters::{jws_family_signs as signs, jws_family_verifies as verifies};
use crate::util::b64url_encode;
struct Ec {
x: &'static str,
y: &'static str,
d: &'static str,
}
const KEY_A: Ec = Ec {
x: "9k4oxJo3yFhw4XvUDFOFlmhgwqGrC25aBd7xhpkFB6Y",
y: "zE-06UddsK2b0rpGKPXn9gz55y3tneIwQbTHPYn4o_I",
d: "LCPRTP8np_fQxQvMdfDwaCr30xArOCNFsTMqauaxMF8",
};
const KEY_B: Ec = Ec {
x: "kYUQsFzddzDVWQ2zrOHzLvP5Xh0vfUGlmngWR9XMJTw",
y: "8uWP4BrVkuiNMETAGTs-RXFQpR9p5DtW6MzL2D6yXX8",
d: "77vIcbbx8enORFALjumhmnHnLbvyhFLORpLTTENmDdA",
};
const KEY_C: Ec = Ec {
x: "Tb3zGAzRHxCqmkYrR0PJBzFoP-eeWagnQAwuoQEqBXA",
y: "32fch9uDiHQleNvG6vnJRjtKAYay5rt63qo37QGgzjU",
d: "iHOPbFusnHIn0GPpRrq0xQEgS833Dv78gj_w-sm7jeg",
};
const CAND_1: Ec = Ec {
x: "veFmhYCtqSHviMxLOuvn3KOaC1prHxYfq5GI9sCTgMc",
y: "aVnGcc1rZHjUHOPyuGItVPHbMds_wEoTO9mil0DSgTE",
d: "gGRJxEwTfHm6o1cdxJ5K3HOaQKOuB3pJQ06XXuBfleA",
};
const P384_PUBLIC: &str = r#""kty":"EC","crv":"P-384","x":"YU4rRUzdmVqmRtWOs2OpDE_T5fsNIodcG8G5FWPrTPMyxpzsSOGaQLpe2FpxBmu2","y":"A8-yxCHxkfBz3hKZfI1jUYMjUhsEveZ9THuwFjH2sCNdtksRJU7D5-SkgaFL1ETP""#;
const ED25519_PUBLIC: &str =
r#""kty":"OKP","crv":"Ed25519","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo""#;
const ED25519_D: &str = "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A";
fn p256_member(key: &Ec, extra: &str) -> String {
format!(
r#"{{"kty":"EC","crv":"P-256","x":"{}","y":"{}"{extra}}}"#,
key.x, key.y
)
}
fn p256_signer(key: &Ec) -> Jwk {
Jwk::from_json(p256_member(key, &format!(r#","d":"{}""#, key.d)).as_bytes()).unwrap()
}
fn set(members: &[&str]) -> JwkSet {
JwkSet::from_json(format!(r#"{{"keys":[{}]}}"#, members.join(",")).as_bytes()).unwrap()
}
fn selection() -> JwkSet {
let oct = format!(
r#"{{"kty":"oct","k":"{}","kid":"h"}}"#,
b64url_encode(&[0x11; 32])
);
let set = set(&[
&p256_member(&KEY_A, r#","kid":"a""#),
&p256_member(&KEY_B, r#","kid":"b""#),
&p256_member(&CAND_1, ""),
&format!(r#"{{{P384_PUBLIC},"kid":"a"}}"#),
&oct,
&format!(r#"{{{ED25519_PUBLIC},"use":"enc"}}"#),
&p256_member(&KEY_C, r#","key_ops":["sign"]"#),
]);
assert_eq!((set.len(), set.skipped()), (7, 0));
set
}
const ALGS: &[A] = &[A::ES256, A::ES384, A::EdDSA, A::HS256, A::RS256];
const CLOCK: FixedClock = FixedClock(0);
fn policy() -> Policy<'static> {
Policy::new(&CLOCK, ALGS).unwrap()
}
const PAYLOAD: &[u8] = br#"{"sub":"keyset"}"#;
fn token(header: &str, key: &Jwk) -> String {
let protected = ProtectedHeader::from_encoded(&b64url_encode(header.as_bytes())).unwrap();
sign_compact(&protected, PAYLOAD, key, &SignOptions::new()).unwrap()
}
fn es256(header: &str, key: &Ec) -> String {
token(header, &p256_signer(key))
}
fn candidate_indices(token: &str, set: &JwkSet, p: &Policy<'_>) -> Result<Vec<usize>> {
let opts = p.verify_options();
let pre = precheck_compact(token, &opts)?;
let found = candidates(&pre, set, p.key_candidate_limit())?;
Ok(found.into_iter().map(|(index, _)| index).collect())
}
const COUNTING: Families = Families {
sign: |alg, key, input| {
signs::tick();
(FAMILIES.sign)(alg, key, input)
},
verify: |alg, key, input, signature| {
verifies::tick();
(FAMILIES.verify)(alg, key, input, signature)
},
};
fn counted(token: &str, set: &JwkSet, p: &Policy<'_>) -> (Result<usize>, u64, u64) {
let ((result, verify_calls), sign_calls) =
signs::count(|| verifies::count(|| verify_with_set_using(&COUNTING, token, set, p)));
(result.map(|(_, index)| index), verify_calls, sign_calls)
}
fn verify_with_set(token: &str, set: &JwkSet, p: &Policy<'_>) -> Result<(Verified, usize)> {
verify_with_set_using(&FAMILIES, token, set, p)
}
fn verify_with_set_using(
families: &Families,
token: &str,
set: &JwkSet,
p: &Policy<'_>,
) -> Result<(Verified, usize)> {
let opts = p.verify_options();
let pre = precheck_compact(token, &opts)?;
select_and_verify_using(families, &pre, set, p)
}
fn selected(token: &str, set: &JwkSet) -> Result<usize> {
verify_with_set(token, set, &policy()).map(|(_, index)| index)
}
#[test]
fn kid_b_selects_exactly_the_p256_b_key() {
let jws = es256(r#"{"alg":"ES256","kid":"b"}"#, &KEY_B);
assert_eq!(
candidate_indices(&jws, &selection(), &policy()),
Ok(vec![1])
);
let (verified, index) = verify_with_set(&jws, &selection(), &policy()).unwrap();
assert_eq!(index, 1);
assert_eq!(verified.alg, A::ES256);
assert_eq!(&verified.payload[..], PAYLOAD);
}
#[test]
fn kid_a_selects_only_the_p256_a_key() {
let jws = es256(r#"{"alg":"ES256","kid":"a"}"#, &KEY_A);
assert_eq!(
candidate_indices(&jws, &selection(), &policy()),
Ok(vec![0])
);
assert_eq!(selected(&jws, &selection()), Ok(0));
}
#[test]
fn no_kid_selects_every_compatible_p256_key_in_set_order() {
let jws = es256(r#"{"alg":"ES256"}"#, &CAND_1);
assert_eq!(
candidate_indices(&jws, &selection(), &policy()),
Ok(vec![0, 1, 2])
);
assert_eq!(counted(&jws, &selection(), &policy()), (Ok(2), 3, 0));
}
#[test]
fn unknown_kid_is_no_matching_key_before_any_signature() {
let jws = es256(r#"{"alg":"ES256","kid":"zzz"}"#, &KEY_A);
assert_eq!(
counted(&jws, &selection(), &policy()),
(Err(Error::NoMatchingKey), 0, 0)
);
}
#[test]
fn eddsa_finds_no_signature_key() {
let signer =
Jwk::from_json(format!(r#"{{{ED25519_PUBLIC},"d":"{ED25519_D}"}}"#).as_bytes())
.unwrap();
let jws = token(r#"{"alg":"EdDSA"}"#, &signer);
assert_eq!(selected(&jws, &selection()), Err(Error::NoMatchingKey));
}
#[test]
fn kid_comparison_is_exact() {
for kid in ["B", "b ", " b", ""] {
let jws = es256(&format!(r#"{{"alg":"ES256","kid":"{kid}"}}"#), &KEY_B);
assert_eq!(
selected(&jws, &selection()),
Err(Error::NoMatchingKey),
"{kid:?}"
);
}
}
#[test]
fn a_p256_key_with_jwk_alg_es384_is_excluded_for_es256() {
let set = set(&[
&p256_member(&KEY_A, r#","kid":"a","alg":"ES384""#),
&p256_member(&KEY_B, r#","kid":"b","alg":"ES256""#),
]);
assert_eq!((set.len(), set.skipped()), (1, 1));
let by_kid = es256(r#"{"alg":"ES256","kid":"a"}"#, &KEY_A);
assert_eq!(selected(&by_kid, &set), Err(Error::NoMatchingKey));
let kid_less = es256(r#"{"alg":"ES256"}"#, &KEY_A);
assert_eq!(candidate_indices(&kid_less, &set, &policy()), Ok(vec![0]));
assert_eq!(selected(&kid_less, &set), Err(Error::InvalidSignature));
let by_b = es256(r#"{"alg":"ES256","kid":"b"}"#, &KEY_B);
assert_eq!(selected(&by_b, &set), Ok(0));
}
#[test]
fn steps_1_to_7_run_before_selection() {
let empty = set(&[]);
let jws = es256(r#"{"alg":"ES256"}"#, &KEY_A);
assert_eq!(selected(&jws, &empty), Err(Error::NoMatchingKey));
let es384_only = Policy::new(&CLOCK, &[A::ES384]).unwrap();
assert_eq!(
verify_with_set(&jws, &empty, &es384_only).map(|(_, i)| i),
Err(Error::AlgorithmNotAllowed)
);
let short = policy().max_input_len(jws.len() - 1);
assert!(matches!(
verify_with_set(&jws, &selection(), &short),
Err(Error::InvalidFormat(_))
));
assert_eq!(
selected("a.b", &selection()).map_err(|e| e.code()),
Err("InvalidFormat")
);
}
const CAND_2: Ec = Ec {
x: "jjgFcO4B9fBwnRZ8E1LOCM3ZJzRC-xfUm32gnyEtVew",
y: "C0MPLKegZh95o8tRrtCEI74o_ydt0_jw97NHWK_Oa3Q",
d: "JptMBJVQcY56OG-Ft3PeD14NrHqUZD0BcRKv-aUHnWg",
};
const CAND_3: Ec = Ec {
x: "E85pHKYaeBBNe-CI6UtPtYY5p8WHE3m3-1kCTweJiis",
y: "XeNaod_9kYFUDwhUCOecuRQsdcKYfr3KDmneZRXCkg4",
d: "CwPnivlhyi-CVp7dDOWXFkg20A3rLd1LeLV9etnpOcs",
};
const ATTACKER: Ec = Ec {
x: "uhAd3X7C07ijtmAyxKzts_abN-ZyyzTqGDHuIVAYZCw",
y: "QFlPJHN8yY_RShgvH0QSsFl1Ngxc5XsNzyfmLhg7iSw",
d: "VbIeLL_X7qVX9KkkZbWgnSAk0-lVOCVEU1hCnpgiai8",
};
fn three_candidates() -> JwkSet {
set(&[
&p256_member(&CAND_1, ""),
&p256_member(&CAND_2, ""),
&p256_member(&CAND_3, ""),
])
}
#[test]
fn the_third_of_three_candidates_verifies_with_key_index_2() {
let jws = es256(r#"{"alg":"ES256"}"#, &CAND_3);
assert_eq!(counted(&jws, &three_candidates(), &policy()), (Ok(2), 3, 0));
let (verified, index) = verify_with_set(&jws, &three_candidates(), &policy()).unwrap();
assert_eq!(index, 2);
assert_eq!(&verified.payload[..], PAYLOAD);
assert_eq!(verified.signature_index, 0);
}
#[test]
fn a_token_no_candidate_signed_is_invalid_signature() {
let jws = es256(r#"{"alg":"ES256"}"#, &ATTACKER);
assert_eq!(
counted(&jws, &three_candidates(), &policy()),
(Err(Error::InvalidSignature), 3, 0)
);
}
#[test]
fn more_candidates_than_the_limit_is_no_matching_key_before_any_signature() {
let jws = es256(r#"{"alg":"ES256"}"#, &CAND_3);
let two = policy().max_key_candidates(2);
assert_eq!(
counted(&jws, &three_candidates(), &two),
(Err(Error::NoMatchingKey), 0, 0)
);
let three = policy().max_key_candidates(3);
assert_eq!(counted(&jws, &three_candidates(), &three), (Ok(2), 3, 0));
let zero = policy().max_key_candidates(0);
assert_eq!(
counted(&jws, &three_candidates(), &zero),
(Err(Error::NoMatchingKey), 0, 0)
);
}
#[test]
fn the_default_limit_is_eight() {
let member = p256_member(&CAND_1, "");
let jws = es256(r#"{"alg":"ES256"}"#, &CAND_1);
let eight = set(&[member.as_str(); 8]);
assert_eq!(counted(&jws, &eight, &policy()), (Ok(0), 1, 0));
let nine = set(&[member.as_str(); 9]);
assert_eq!(
counted(&jws, &nine, &policy()),
(Err(Error::NoMatchingKey), 0, 0)
);
}
fn oct_member(fill: u8, len: usize) -> String {
format!(
r#"{{"kty":"oct","k":"{}"}}"#,
b64url_encode(&vec![fill; len])
)
}
#[test]
fn a_weak_key_and_a_wrong_signature_rank_invalid_signature() {
let signer = Jwk::from_json(oct_member(0x22, 32).as_bytes()).unwrap();
let jws = token(r#"{"alg":"HS256"}"#, &signer);
let weak = oct_member(0x33, 16);
let wrong = oct_member(0x44, 32);
assert_eq!(
selected(&jws, &set(&[&weak, &wrong])),
Err(Error::InvalidSignature)
);
assert_eq!(
selected(&jws, &set(&[&wrong, &weak])),
Err(Error::InvalidSignature)
);
assert!(matches!(
selected(&jws, &set(&[&weak, &weak])),
Err(Error::InvalidKey(_))
));
let right = oct_member(0x22, 32);
assert_eq!(selected(&jws, &set(&[&weak, &wrong, &right])), Ok(2));
}
const A2_TOKEN: &str = concat!(
"eyJhbGciOiJSUzI1NiJ9",
".eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
".cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5",
"jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb",
"1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QO",
"YEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw",
);
const A2_PUBLIC: &str = concat!(
r#"{"kty":"RSA","kid":"rfc7515-a2","e":"AQAB","n":""#,
"ofgWCuLjybRlzo0tZWJjNiuSfb4p4fAkd_wWJcyQoTbji9k0l8W26mPddxHmfHQp-Vaw-4qPCJrcS2mJPMEzP1Pt0Bm4d4Ql",
"L-yRT-SFd2lZS-pCgNMsD1W_YpRPEwOWvG6b32690r2jZ47soMZo9wGzjb_7OMg0LOL-bSf63kpaSHSXndS5z5rexMdbBYUs",
"LA9e-KXBdQOS-UTo7WTBEMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoud",
"F8NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ",
r#""}"#,
);
const RFC7520_TOKEN: &str = concat!(
"eyJhbGciOiJSUzI1NiIsImtpZCI6ImJpbGJvLmJhZ2dpbnNAaG9iYml0b24uZXhhbXBsZSJ9",
".SXTigJlzIGEgZGFuZ2Vyb3VzIGJ1c2luZXNzLCBGcm9kbywgZ29pbmcgb3V0IHlvdXIgZG9vci4gWW91IHN0ZXAgb250byB0",
"aGUgcm9hZCwgYW5kIGlmIHlvdSBkb24ndCBrZWVwIHlvdXIgZmVldCwgdGhlcmXigJlzIG5vIGtub3dpbmcgd2hlcmUgeW91",
"IG1pZ2h0IGJlIHN3ZXB0IG9mZiB0by4",
".MRjdkly7_-oTPTS3AXP41iQIGKa80A0ZmTuV5MEaHoxnW2e5CZ5NlKtainoFmKZopdHM1O2U4mwzJdQx996ivp83xuglII7P",
"NDi84wnB-BDkoBwA78185hX-Es4JIwmDLJK3lfWRa-XtL0RnltuYv746iYTh_qHRD68BNt1uSNCrUCTJDt5aAE6x8wW1Kt9e",
"Ro4QPocSadnHXFxnt8Is9UzpERV0ePPQdLuW3IS_de3xyIrDaLGdjluPxUAhb6L2aXic1U12podGU0KLUQSE_oI-ZnmKJ3F4",
"uOZDnd6QZWJushZ41Axf_fcIe8u9ipH84ogoree7vjbU5y18kDquDg",
);
const RFC7520_PUBLIC: &str = concat!(
r#"{"kty":"RSA","kid":"bilbo.baggins@hobbiton.example","use":"sig","e":"AQAB","n":""#,
"n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMa",
"JMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6c",
"S6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwd",
"q_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw",
r#""}"#,
);
#[test]
fn rfc7515_a2_rs256_verifies_through_a_set_without_a_kid() {
let both = set(&[RFC7520_PUBLIC, A2_PUBLIC]);
assert_eq!(
candidate_indices(A2_TOKEN, &both, &policy()),
Ok(vec![0, 1])
);
let (verified, index) = verify_with_set(A2_TOKEN, &both, &policy()).unwrap();
assert_eq!((index, &verified.alg), (1, &A::RS256));
assert!(verified.payload.starts_with(br#"{"iss":"joe""#));
assert_eq!(selected(A2_TOKEN, &set(&[A2_PUBLIC])), Ok(0));
assert_eq!(both.keys()[1].kid(), Some("rfc7515-a2"));
}
#[test]
fn an_rs256_token_with_a_kid_selects_its_key_by_kid() {
let both = set(&[A2_PUBLIC, RFC7520_PUBLIC]);
assert_eq!(
candidate_indices(RFC7520_TOKEN, &both, &policy()),
Ok(vec![1])
);
assert_eq!(counted(RFC7520_TOKEN, &both, &policy()), (Ok(1), 1, 0));
let a2_only = set(&[A2_PUBLIC]);
assert_eq!(selected(RFC7520_TOKEN, &a2_only), Err(Error::NoMatchingKey));
}
const A2_PRIVATE_MEMBER: &str = concat!(
r#"{"kty":"RSA","alg":"RS256","kid":"2011-04-29","e":"AQAB","#,
r#""n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw","#,
r#""d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q","#,
r#""p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs","#,
r#""q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk","#,
r#""dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0","#,
r#""dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk","#,
r#""qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU"}"#,
);
#[test]
fn a_set_with_an_rsa_private_member_fails_to_import() {
let first = p256_member(&KEY_A, r#","kid":"a""#);
let json = format!(r#"{{"keys":[{first},{A2_PRIVATE_MEMBER}]}}"#);
assert_eq!(
JwkSet::from_json(json.as_bytes()).unwrap_err(),
Error::UnsupportedAlgorithm
);
let jws = es256(r#"{"alg":"ES256","kid":"a"}"#, &KEY_A);
assert_eq!(selected(&jws, &set(&[&first])), Ok(0));
}
#[test]
fn unknown_kty_and_crv_members_are_skipped() {
let set = set(&[
r#"{"kty":"XYZ","kid":"a"}"#,
r#"{"kty":"OKP","crv":"Ed448","x":"AAAA","kid":"a"}"#,
r#"{"kty":"EC","crv":"P-192","x":"AAAA","y":"AAAA","kid":"a"}"#,
&p256_member(&KEY_A, r#","kid":"a""#),
]);
assert_eq!((set.len(), set.skipped()), (1, 3));
let jws = es256(r#"{"alg":"ES256","kid":"a"}"#, &KEY_A);
assert_eq!(selected(&jws, &set), Ok(0));
}
const MODULUS_8200: &str = concat!(
"rTphzws3aF2xHUrDRw7j7R0JuF-6mnSdJxusXRTdeVVx0N1gj3iBWnh7TUe3iRCs3swR_c4sfVx-xhm6wrHBWDQk",
"PQnJw6Jtnvd5JgybX_Qbh3LZj4hQrWKCSzXNkjsZ_JpYpWDWwwjut-lVCt1z7uWkTeUVjxcT2fD-JCRa2Fw0lxk4",
"TlSy3EivgtjOBpjKdvq60pF8DZa1q0zQ7pt_Evak4d8221qFZ4GKuifRKojcuzjCtJ7AG-9FCXad0lnjObOYwGdm",
"d3QOC3J_pHiNsGWMT6y0B7PxRu31HTsaXIPuQKyghhthhsA5XIpj9ByUNcepFPQ4D6o0Bi-nb14xAOMhW3zwbC8I",
"kKHVBRYjnP3379DluI8zS7yXup4TIsX-I1U69mHr5TmQGLf6uMTjye8DELEmE2XqywReZGchQN0qhounhXRX1du1",
"_af4vQ_dT81Pf8gzoQe5hL3iSDcMeK9pEN2u4vjU9GdlNLH1kvD3BnO6-gyhdSaXcGFW0vUoxqFQ-0aAVrM31dcy",
"20aJ9V1tttWVlVT9RYvQRiycKan-Ao5lJlM3APL0bLHPXubd0ugZ6gnZ0OJUIA1c8TU6DnOpr35vPo024JG9Y-95",
"TdFR2cwyJs0W7ij3awMzfFKB5IgYmujBRatP7ff8aGjT8634bwCdZNToWWdaxaSDHTW-EgIoLYnb3vc_cKFm48LZ",
"uMsLPqoTpXEOxFKsN8s8Yd6sgN2i8_EDUWz2GrUgXS0chIZexXC_O-IsmZyZYsLalcf10Ux7skk08vc2z5lXkIu6",
"lSxhh9Z_GDhBU9oEC3C33VS4nSIqkwJhYlS77yPTXD_rBhxrL0tU9sGfgk9a4Wji4HEz1Hew8qsTazW5_a03fUVo",
"opAvIwiEZF1NfSM-WcN5jPBitK48PaGp03_oPoa7hFcYElUIJo9CLBONZiWmeXDoOvGDbZizpyytRkQ0qf0ApJaP",
"yBOlurmOkSJ2DAa40KWRYTOIETCSWExH2PotJkGsqu5Mer39EAxI6UIzPUSR07OReca9eXmtZmuK438pa1m6KGml",
"KIUg4pu1bgEfGQZx_CYqH2R_kL4M61CVZ3l00nCC_qupUJwkMEPSj_c_YB2Uc83qMHvAo5GeTFDgc5MAA7ztE5H5",
"pbEs4LI3THcFyoKx2V3zu2k69EWmbtn0wbet1tkJxy5MhmdRVNIckR-teJiGMJ6jerOrewTzncUbF0ywyl7yBwjr",
"BcyGRo-L4QCSfwc_PHPmaMrdkH7dnZp9HMWzGz8uunZwZ-GL3_DNOtSZbgHaq0or9ajPQzjBAelNNzts3_Wl4L8s",
"ZsCkxEFtc_bRclaPYGA1zZRH6Qs-LxUVsNVivykD366X04M",
);
#[test]
fn an_oversized_rsa_member_is_skipped_and_gives_no_matching_key() {
let oversized = format!(r#"{{"kty":"RSA","n":"{MODULUS_8200}","e":"AQAB"}}"#);
let bits = crate::util::b64url_decode(MODULUS_8200).unwrap().len() * 8;
assert!(bits > 8192, "{bits}");
let set = set(&[&p256_member(&KEY_A, ""), &oversized]);
assert_eq!((set.len(), set.skipped()), (1, 1));
assert_eq!(
counted(A2_TOKEN, &set, &policy()),
(Err(Error::NoMatchingKey), 0, 0)
);
}
#[test]
fn an_embedded_jwk_never_supplies_a_key() {
let embedded = p256_member(&ATTACKER, r#","kid":"evil""#);
let by_kid = es256(
&format!(r#"{{"alg":"ES256","kid":"evil","jwk":{embedded}}}"#),
&ATTACKER,
);
assert_eq!(
counted(&by_kid, &selection(), &policy()),
(Err(Error::NoMatchingKey), 0, 0)
);
let kid_less = es256(&format!(r#"{{"alg":"ES256","jwk":{embedded}}}"#), &ATTACKER);
assert_eq!(
counted(&kid_less, &selection(), &policy()),
(Err(Error::InvalidSignature), 3, 0)
);
}
#[test]
fn jku_and_x5u_headers_are_never_dereferenced() {
let header = r#"{"alg":"ES256","kid":"b","jku":"https://attacker.example/jwks.json","x5u":"https://attacker.example/cert.pem"}"#;
assert_eq!(selected(&es256(header, &KEY_B), &selection()), Ok(1));
assert_eq!(
selected(&es256(header, &ATTACKER), &selection()),
Err(Error::InvalidSignature)
);
}
#[test]
fn the_module_has_no_io_api() {
let source = include_str!("keyset.rs");
let code = &source[..source.find("#[cfg(test)]").unwrap()];
for needle in [
"std::net",
"std::fs",
"std::io",
"std::process",
"TcpStream",
"UdpSocket",
"ToSocketAddrs",
] {
assert!(!code.contains(needle), "{needle}");
}
}
#[test]
fn an_unprotected_kid_is_never_read() {
let jws = es256(r#"{"alg":"ES256"}"#, &KEY_B);
let [protected, payload, signature]: [&str; 3] =
jws.split('.').collect::<Vec<_>>().try_into().unwrap();
let flattened = format!(
r#"{{"protected":"{protected}","header":{{"kid":"b"}},"payload":"{payload}","signature":"{signature}"}}"#
);
assert_eq!(
selected(&flattened, &selection()).map_err(|e| e.code()),
Err("InvalidFormat")
);
}
#[test]
fn kid_and_alg_in_the_payload_are_ignored() {
let protected =
ProtectedHeader::from_encoded(&b64url_encode(br#"{"alg":"ES256"}"#)).unwrap();
let jws = sign_compact(
&protected,
br#"{"kid":"zzz","alg":"HS256"}"#,
&p256_signer(&KEY_B),
&SignOptions::new(),
)
.unwrap();
assert_eq!(
candidate_indices(&jws, &selection(), &policy()),
Ok(vec![0, 1, 2])
);
assert_eq!(selected(&jws, &selection()), Ok(1));
}
#[test]
fn select_and_verify_matches_verify_with_set() {
let p = policy();
let opts = p.verify_options();
let tokens = [
es256(r#"{"alg":"ES256","kid":"b"}"#, &KEY_B),
es256(r#"{"alg":"ES256"}"#, &CAND_1),
es256(r#"{"alg":"ES256","kid":"zzz"}"#, &KEY_B),
es256(r#"{"alg":"ES256"}"#, &ATTACKER),
];
for jws in &tokens {
let pre = precheck_compact(jws, &opts).unwrap();
assert_eq!(
select_and_verify(&pre, &selection(), &p),
verify_with_set(jws, &selection(), &p)
);
}
}
}