use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde_json::json;
use super::*;
use crate::jwt::{compact_jws, hmac_sha256, EcdsaP256Key, PublicJwk};
const VERIFIER: &crate::jwt::P256Verifier = &crate::jwt::P256Verifier;
fn now() -> SystemTime {
UNIX_EPOCH + Duration::from_secs(1_700_000_000)
}
fn secs(t: SystemTime) -> u64 {
t.duration_since(UNIX_EPOCH).unwrap().as_secs()
}
const CLIENT: &str = "assertion-client";
const TOKEN_ENDPOINT: &str = "https://as.example/token";
const ISSUER: &str = "https://as.example";
const SECRET: &str = "a-high-entropy-registered-client-secret";
fn audiences() -> Vec<&'static str> {
vec![TOKEN_ENDPOINT, ISSUER]
}
fn claims() -> serde_json::Value {
json!({
"iss": CLIENT,
"sub": CLIENT,
"aud": TOKEN_ENDPOINT,
"exp": secs(now()) + 120,
"iat": secs(now()),
"jti": "assertion-0001",
})
}
fn hs256(secret: &str, header: &serde_json::Value, claims: &serde_json::Value) -> String {
compact_jws(
&serde_json::to_vec(header).unwrap(),
&serde_json::to_vec(claims).unwrap(),
|input| hmac_sha256(secret.as_bytes(), input.as_bytes()).to_vec(),
)
}
fn es256(key: &EcdsaP256Key, header: &serde_json::Value, claims: &serde_json::Value) -> String {
compact_jws(
&serde_json::to_vec(header).unwrap(),
&serde_json::to_vec(claims).unwrap(),
|input| key.sign_signing_input(input).unwrap(),
)
}
fn secret_keys() -> AssertionKeys {
AssertionKeys::ClientSecret {
secret: ClientSecretKey::new(SECRET).expect("fixture secret clears the floor"),
}
}
fn key_pair() -> (EcdsaP256Key, AssertionKeys) {
let key = EcdsaP256Key::generate("client-key-1");
let keys = AssertionKeys::PublicKeys {
keys: vec![key.to_public_jwk()],
};
(key, keys)
}
fn verify(keys: &AssertionKeys, assertion: &str) -> Result<VerifiedAssertion, AssertionFailure> {
verify_assertion(Some(VERIFIER), keys, assertion, CLIENT, &audiences(), now())
}
fn base64_url(bytes: &[u8]) -> String {
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine as _;
URL_SAFE_NO_PAD.encode(bytes)
}
#[test]
fn a_client_secret_jwt_assertion_verifies() {
let assertion = hs256(SECRET, &json!({"alg": "HS256", "typ": "JWT"}), &claims());
let verified = verify(&secret_keys(), &assertion).expect("a conforming assertion verifies");
assert_eq!(verified.jti, "assertion-0001");
assert_eq!(verified.expires_at, now() + Duration::from_secs(120));
}
#[test]
fn a_private_key_jwt_assertion_verifies() {
let (key, keys) = key_pair();
let assertion = es256(&key, &json!({"alg": "ES256", "typ": "JWT"}), &claims());
let verified = verify(&keys, &assertion).expect("a conforming assertion verifies");
assert_eq!(verified.jti, "assertion-0001");
}
#[test]
fn a_header_with_no_typ_at_all_is_accepted() {
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &claims());
assert!(verify(&secret_keys(), &assertion).is_ok());
}
#[test]
fn the_issuer_is_an_acceptable_audience_as_well_as_the_token_endpoint() {
let mut c = claims();
c["aud"] = json!(ISSUER);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert!(verify(&secret_keys(), &assertion).is_ok());
}
#[test]
fn an_array_valued_audience_is_accepted_when_one_element_matches() {
let mut c = claims();
c["aud"] = json!(["https://other.example/token", TOKEN_ENDPOINT]);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert!(verify(&secret_keys(), &assertion).is_ok());
}
#[test]
fn an_hmac_assertion_from_a_public_key_client_is_refused() {
let (key, keys) = key_pair();
let public = serde_json::to_string(&key.to_public_jwk()).unwrap();
let assertion = hs256(&public, &json!({"alg": "HS256", "typ": "JWT"}), &claims());
assert_eq!(
verify(&keys, &assertion),
Err(AssertionFailure::AlgorithmMismatch)
);
}
#[test]
fn an_ecdsa_assertion_from_a_client_secret_client_is_refused() {
let (key, _) = key_pair();
let assertion = es256(&key, &json!({"alg": "ES256", "typ": "JWT"}), &claims());
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::AlgorithmMismatch)
);
}
#[test]
fn alg_none_is_refused() {
let header = serde_json::to_vec(&json!({"alg": "none", "typ": "JWT"})).unwrap();
let payload = serde_json::to_vec(&claims()).unwrap();
let assertion = compact_jws(&header, &payload, |_| Vec::new());
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::AlgorithmMismatch)
);
}
#[test]
fn a_header_with_no_alg_at_all_is_refused() {
let assertion = hs256(SECRET, &json!({"typ": "JWT"}), &claims());
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::AlgorithmMismatch)
);
}
#[test]
fn a_dpop_proof_is_not_a_client_assertion() {
let assertion = hs256(
SECRET,
&json!({"alg": "HS256", "typ": "dpop+jwt"}),
&claims(),
);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::Malformed)
);
}
#[test]
fn an_access_token_is_not_a_client_assertion() {
let assertion = hs256(SECRET, &json!({"alg": "HS256", "typ": "at+jwt"}), &claims());
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::Malformed)
);
}
#[test]
fn an_assertion_signed_by_a_key_the_registration_does_not_name_is_refused() {
let (_, keys) = key_pair();
let attacker = EcdsaP256Key::generate("attacker-key");
let assertion = es256(&attacker, &json!({"alg": "ES256"}), &claims());
assert_eq!(
verify(&keys, &assertion),
Err(AssertionFailure::BadSignature)
);
}
#[test]
fn a_payload_edited_after_signing_is_refused() {
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &claims());
let mut parts: Vec<&str> = assertion.split('.').collect();
let forged = base64_url(
&serde_json::to_vec(&json!({
"iss": CLIENT, "sub": CLIENT, "aud": TOKEN_ENDPOINT,
"exp": secs(now()) + 120, "iat": secs(now()), "jti": "forged-0001",
}))
.unwrap(),
);
parts[1] = &forged;
let assertion = parts.join(".");
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::BadSignature)
);
}
#[test]
fn a_wrong_client_secret_is_refused() {
let assertion = hs256("not-the-secret", &json!({"alg": "HS256"}), &claims());
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::BadSignature)
);
}
#[test]
fn one_of_several_registered_keys_is_enough() {
let old = EcdsaP256Key::generate("old");
let new = EcdsaP256Key::generate("new");
let keys = AssertionKeys::PublicKeys {
keys: vec![old.to_public_jwk(), new.to_public_jwk()],
};
let assertion = es256(&new, &json!({"alg": "ES256"}), &claims());
assert!(verify(&keys, &assertion).is_ok());
}
#[test]
fn an_assertion_naming_another_client_as_issuer_is_refused() {
let mut c = claims();
c["iss"] = json!("some-other-client");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::WrongPrincipal)
);
}
#[test]
fn an_assertion_naming_another_client_as_subject_is_refused() {
let mut c = claims();
c["sub"] = json!("some-other-client");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::WrongPrincipal)
);
}
#[test]
fn an_assertion_missing_iss_or_sub_is_refused() {
for missing in ["iss", "sub"] {
let mut c = claims();
c.as_object_mut().unwrap().remove(missing);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::WrongPrincipal),
"an assertion with no {missing} must be refused"
);
}
}
#[test]
fn an_assertion_addressed_to_another_server_is_refused() {
let mut c = claims();
c["aud"] = json!("https://evil.example/token");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::WrongAudience)
);
}
#[test]
fn an_assertion_with_no_audience_is_refused() {
let mut c = claims();
c.as_object_mut().unwrap().remove("aud");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::WrongAudience)
);
}
#[test]
fn an_expired_assertion_is_refused() {
let mut c = claims();
c["exp"] = json!(secs(now()) - 1);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::Expired)
);
}
#[test]
fn an_assertion_with_no_exp_is_refused() {
let mut c = claims();
c.as_object_mut().unwrap().remove("exp");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::Expired)
);
}
#[test]
fn an_assertion_valid_for_longer_than_this_server_will_track_is_refused() {
let mut c = claims();
c["exp"] = json!(secs(now() + MAX_ASSERTION_LIFETIME) + 1);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::Expired)
);
}
#[test]
fn an_assertion_that_is_not_yet_valid_is_refused() {
let mut c = claims();
c["nbf"] = json!(secs(now() + CLOCK_SKEW_LEEWAY) + 1);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::NotYetValid)
);
}
#[test]
fn an_assertion_issued_in_the_future_is_refused() {
let mut c = claims();
c["iat"] = json!(secs(now() + CLOCK_SKEW_LEEWAY) + 1);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::NotYetValid)
);
}
#[test]
fn small_clock_skew_is_tolerated_in_the_one_safe_direction() {
let mut c = claims();
c["iat"] = json!(secs(now()) + 5);
c["nbf"] = json!(secs(now()) + 5);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert!(verify(&secret_keys(), &assertion).is_ok());
}
#[test]
fn an_assertion_with_no_jti_is_refused() {
let mut c = claims();
c.as_object_mut().unwrap().remove("jti");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::MissingJti)
);
}
#[test]
fn an_assertion_with_an_empty_jti_is_refused() {
let mut c = claims();
c["jti"] = json!("");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::MissingJti)
);
}
#[test]
fn a_non_string_claim_where_a_string_is_required_is_refused_not_coerced() {
let mut c = claims();
c["iss"] = json!(7);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::WrongPrincipal)
);
}
#[test]
fn a_fractional_or_negative_exp_reads_as_absent_rather_than_as_the_epoch() {
for bad in [json!(-1), json!(1.5), json!("soon")] {
let mut c = claims();
c["exp"] = bad.clone();
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::Expired),
"exp {bad} must not be coerced"
);
}
}
#[test]
fn garbage_is_refused_rather_than_panicking() {
for input in [
"",
".",
"..",
"a.b",
"a.b.c.d",
"not base64.at all.here",
"eyJhbGciOiJIUzI1NiJ9",
"e30.e30.e30.e30.e30",
] {
assert!(
verify(&secret_keys(), input).is_err(),
"{input:?} must be refused"
);
}
}
#[test]
fn hmac_sha256_matches_the_rfc_4231_test_case_2_vector() {
let tag = hmac_sha256(b"Jefe", b"what do ya want for nothing?");
let actual: String = tag.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(
actual,
"5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843"
);
}
#[test]
fn hmac_sha256_matches_the_rfc_4231_test_case_6_vector() {
let tag = hmac_sha256(
&[0xaau8; 131],
b"Test Using Larger Than Block-Size Key - Hash Key First",
);
let actual: String = tag.iter().map(|b| format!("{b:02x}")).collect();
assert_eq!(
actual,
"60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
);
}
#[test]
fn a_jwk_carrying_a_private_parameter_is_refused() {
let key = EcdsaP256Key::generate("k");
for member in ["d", "k", "p", "q"] {
let mut value = serde_json::to_value(key.to_public_jwk()).unwrap();
value[member] = json!("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA");
assert!(
PublicJwk::from_json(&value).is_err(),
"a JWK carrying {member} must be refused"
);
}
}
#[test]
fn a_jwk_with_a_trimmed_coordinate_is_refused() {
let key = EcdsaP256Key::generate("k");
let mut value = serde_json::to_value(key.to_public_jwk()).unwrap();
value["x"] = json!("AAAA");
assert!(PublicJwk::from_json(&value).is_err());
}
#[test]
fn an_assertion_expiring_exactly_at_the_tracking_horizon_is_accepted() {
let mut c = claims();
c["exp"] = json!(secs(now() + MAX_ASSERTION_LIFETIME));
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
let verified = verify(&secret_keys(), &assertion).expect("exp at the horizon is trackable");
assert_eq!(verified.expires_at, now() + MAX_ASSERTION_LIFETIME);
}
#[test]
fn an_assertion_issued_exactly_at_the_skew_horizon_is_accepted() {
for claim in ["iat", "nbf"] {
let mut c = claims();
c[claim] = json!(secs(now() + CLOCK_SKEW_LEEWAY));
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert!(
verify(&secret_keys(), &assertion).is_ok(),
"{claim} exactly at the skew horizon must be inside the tolerance"
);
}
}
#[test]
fn the_unverified_subject_is_the_assertions_own_sub_verbatim() {
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &claims());
assert_eq!(unverified_subject(&assertion).as_deref(), Some(CLIENT));
let mut other = claims();
other["sub"] = json!("some-other-client");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &other);
assert_eq!(
unverified_subject(&assertion).as_deref(),
Some("some-other-client")
);
}
#[test]
fn an_assertion_with_no_readable_subject_yields_no_lookup_key() {
let mut c = claims();
c.as_object_mut().unwrap().remove("sub");
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(unverified_subject(&assertion), None);
assert_eq!(unverified_subject("not-a-jws"), None);
assert_eq!(unverified_subject(""), None);
let mut c = claims();
c["sub"] = json!(7);
let assertion = hs256(SECRET, &json!({"alg": "HS256"}), &c);
assert_eq!(unverified_subject(&assertion), None);
}
fn padded_assertion(target: usize) -> String {
let build = |filler: usize| {
let mut c = claims();
c["pad"] = json!("p".repeat(filler));
hs256(SECRET, &json!({"alg": "HS256", "typ": "JWT"}), &c)
};
let mut filler = 0;
while build(filler + 1).len() <= target {
filler += 1;
}
build(filler)
}
#[test]
fn an_assertion_larger_than_the_cap_is_refused_before_it_is_parsed() {
let assertion = padded_assertion(MAX_ASSERTION_BYTES * 4);
assert!(assertion.len() > MAX_ASSERTION_BYTES);
assert_eq!(
verify(&secret_keys(), &assertion),
Err(AssertionFailure::Malformed),
"an assertion past the cap is refused on size, whatever it would have verified as"
);
assert_eq!(
unverified_subject(&assertion),
None,
"the pre-authentication lookup must not parse a string past the cap either"
);
}
#[test]
fn an_assertion_at_the_cap_is_accepted() {
let assertion = padded_assertion(MAX_ASSERTION_BYTES);
assert!(
assertion.len() <= MAX_ASSERTION_BYTES && assertion.len() + 4 > MAX_ASSERTION_BYTES,
"the fixture must sit ON the boundary, not comfortably inside it; it is {} bytes",
assertion.len()
);
let verified =
verify(&secret_keys(), &assertion).expect("an assertion at the cap is inside it");
assert_eq!(verified.jti, "assertion-0001");
assert_eq!(unverified_subject(&assertion).as_deref(), Some(CLIENT));
}
#[test]
fn an_ordinary_assertion_is_far_inside_the_cap() {
let hs = hs256(SECRET, &json!({"alg": "HS256", "typ": "JWT"}), &claims());
let (key, _) = key_pair();
let es = es256(&key, &json!({"alg": "ES256", "typ": "JWT"}), &claims());
for assertion in [hs, es] {
assert!(
assertion.len() * 4 < MAX_ASSERTION_BYTES,
"a conforming assertion is {} bytes; a cap of {MAX_ASSERTION_BYTES} must leave room",
assertion.len()
);
}
}