use core::fmt;
use zeroize::ZeroizeOnDrop;
use super::{
Jwk, KeyAlg, KeyMaterial, KeyOp, KeyUse, MemberNames, is_unsupported_key_kind, member,
};
use crate::error::{Error, Result};
use crate::json::{self, Object, Value};
use crate::jwa::{KeyMgmtAlg, SigningAlg};
use crate::jwe::key_ops_either_direction;
const NOT_AN_OBJECT: &str = "a JWK Set must be a JSON object";
const NO_KEYS_ARRAY: &str = "a JWK Set must have a keys member that is an array";
#[derive(Clone)]
pub struct JwkSet {
keys: Vec<Jwk>,
skipped: usize,
other: Object,
}
impl JwkSet {
pub fn from_json(bytes: &[u8]) -> Result<JwkSet> {
let object = json::parse(bytes)?
.into_object()
.ok_or(Error::InvalidJson(NOT_AN_OBJECT))?;
JwkSet::from_object(&object)
}
pub fn from_object(object: &Object) -> Result<JwkSet> {
let members = object
.get("keys")
.and_then(Value::as_array)
.ok_or(Error::InvalidKey(NO_KEYS_ARRAY))?;
let mut keys = Vec::with_capacity(members.len());
let mut skipped = 0;
for member in members {
match import_member(member)? {
Some(jwk) => keys.push(jwk),
None => skipped += 1,
}
}
let other = Object::from_unique_members(
object
.iter()
.filter(|(name, _)| *name != "keys")
.map(|(name, value)| member(name, value.clone()))
.collect(),
);
Ok(JwkSet {
keys,
skipped,
other,
})
}
pub fn keys(&self) -> &[Jwk] {
&self.keys
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}
pub fn skipped(&self) -> usize {
self.skipped
}
pub fn other_params(&self) -> &Object {
&self.other
}
pub fn verification_candidates<'a>(
&'a self,
alg: &'a SigningAlg,
kid: Option<&'a str>,
) -> impl Iterator<Item = (usize, &'a Jwk)> + 'a {
let criteria = Criteria::new(
Some(SelectorAlg::Signing(alg)),
None,
Some(KeyOp::Verify),
kid,
);
self.filter_keys(move |key| criteria.matches(key))
}
pub fn select(&self, selector: &JwkSelector) -> Result<(usize, &Jwk)> {
let criteria = Criteria::of(selector);
exactly_one(self.filter_keys(|key| criteria.matches(key)))
}
pub fn matching<'a>(
&'a self,
selector: &'a JwkSelector,
) -> impl Iterator<Item = (usize, &'a Jwk)> + 'a {
let criteria = Criteria::of(selector);
self.filter_keys(move |key| criteria.matches(key))
}
fn filter_keys(
&self,
mut keep: impl FnMut(&Jwk) -> bool,
) -> impl Iterator<Item = (usize, &Jwk)> {
self.keys
.iter()
.enumerate()
.filter(move |(_, key)| keep(key))
}
}
fn exactly_one<'a>(mut found: impl Iterator<Item = (usize, &'a Jwk)>) -> Result<(usize, &'a Jwk)> {
let first = found.next().ok_or(Error::NoMatchingKey)?;
match found.next() {
None => Ok(first),
Some(_) => Err(Error::MultipleMatchingKeys),
}
}
#[derive(Clone, Copy, Debug)]
enum SelectorAlg<'a> {
Signing(&'a SigningAlg),
KeyMgmt(&'a KeyMgmtAlg),
Unservable,
}
impl SelectorAlg<'_> {
fn served_by(self, material: &KeyMaterial) -> bool {
match self {
SelectorAlg::Signing(alg) => material.serves_signing_alg(alg),
SelectorAlg::KeyMgmt(alg) => material.serves_key_mgmt_alg(alg),
SelectorAlg::Unservable => false,
}
}
fn named_by(self, key_alg: &KeyAlg) -> bool {
match (self, key_alg) {
(SelectorAlg::Signing(alg), KeyAlg::Signing(key_alg)) => alg == key_alg,
(SelectorAlg::KeyMgmt(KeyMgmtAlg::Dir), KeyAlg::ContentEnc(_)) => true,
(SelectorAlg::KeyMgmt(alg), KeyAlg::KeyMgmt(key_alg)) => alg == key_alg,
_ => false,
}
}
fn key_use(self) -> Option<KeyUse> {
match self {
SelectorAlg::Signing(_) => Some(KeyUse::Sig),
SelectorAlg::KeyMgmt(_) => Some(KeyUse::Enc),
SelectorAlg::Unservable => None,
}
}
fn performs(self, op: &KeyOp) -> bool {
match self {
SelectorAlg::Signing(_) => matches!(op, KeyOp::Sign | KeyOp::Verify),
SelectorAlg::KeyMgmt(alg) => key_ops_either_direction(alg)
.iter()
.flatten()
.any(|performed| performed == op),
SelectorAlg::Unservable => false,
}
}
fn performed_by_any(self, key_ops: &[KeyOp]) -> bool {
key_ops.iter().any(|op| self.performs(op))
}
}
#[derive(Clone, Debug)]
enum Purpose {
Unchecked,
Use(KeyUse),
Conflict,
}
impl Purpose {
fn of(key_use: Option<&KeyUse>, key_op: Option<&KeyOp>, alg: Option<SelectorAlg<'_>>) -> Self {
let parts = [
key_use.cloned(),
key_op.and_then(KeyOp::key_use),
alg.and_then(SelectorAlg::key_use),
];
let mut purpose = Purpose::Unchecked;
for part in parts.into_iter().flatten() {
purpose = match purpose {
Purpose::Unchecked => Purpose::Use(part),
Purpose::Use(u) if u == part => Purpose::Use(u),
Purpose::Use(_) | Purpose::Conflict => return Purpose::Conflict,
};
}
purpose
}
}
#[derive(Clone, Debug)]
struct Criteria<'a> {
alg: Option<SelectorAlg<'a>>,
key_op: Option<KeyOp>,
kid: Option<&'a str>,
purpose: Purpose,
}
impl<'a> Criteria<'a> {
fn new(
alg: Option<SelectorAlg<'a>>,
key_use: Option<&KeyUse>,
key_op: Option<KeyOp>,
kid: Option<&'a str>,
) -> Self {
let purpose = Purpose::of(key_use, key_op.as_ref(), alg);
Criteria {
alg,
key_op,
kid,
purpose,
}
}
fn of(selector: &'a JwkSelector) -> Self {
let alg = selector.alg.as_ref().map(|alg| match alg {
KeyAlg::Signing(alg) => SelectorAlg::Signing(alg),
KeyAlg::KeyMgmt(alg) => SelectorAlg::KeyMgmt(alg),
KeyAlg::ContentEnc(_) | KeyAlg::Unknown(_) => SelectorAlg::Unservable,
});
Criteria::new(
alg,
selector.key_use.as_ref(),
selector.key_op.clone(),
selector.kid.as_deref(),
)
}
fn matches(&self, key: &Jwk) -> bool {
self.alg.is_none_or(|alg| alg.served_by(key.material()))
&& self
.alg
.is_none_or(|alg| key.alg().is_none_or(|key_alg| alg.named_by(key_alg)))
&& match &self.purpose {
Purpose::Unchecked => true,
Purpose::Use(u) => key.key_use().is_none_or(|key_use| key_use == u),
Purpose::Conflict => false,
}
&& self.key_ops_allow(key)
&& self.kid.is_none_or(|kid| key.kid() == Some(kid))
}
fn key_ops_allow(&self, key: &Jwk) -> bool {
if let Some(op) = &self.key_op {
return self.alg.is_none_or(|alg| alg.performs(op)) && key.permits(op);
}
let Some(key_ops) = key.key_ops() else {
return true;
};
if let Some(alg) = self.alg {
return alg.performed_by_any(key_ops);
}
match &self.purpose {
Purpose::Use(u) => key_ops.iter().any(|op| op.key_use().as_ref() == Some(u)),
Purpose::Unchecked | Purpose::Conflict => true,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct JwkSelector {
kid: Option<String>,
alg: Option<KeyAlg>,
key_use: Option<KeyUse>,
key_op: Option<KeyOp>,
}
impl JwkSelector {
pub fn new() -> JwkSelector {
JwkSelector::default()
}
pub fn kid(mut self, kid: &str) -> JwkSelector {
self.kid = Some(kid.to_owned());
self
}
pub fn alg(mut self, alg: KeyAlg) -> Result<JwkSelector> {
let implemented = match &alg {
KeyAlg::Signing(a) => a.is_known() && *a != SigningAlg::None,
KeyAlg::KeyMgmt(a) => a.is_known(),
KeyAlg::ContentEnc(_) | KeyAlg::Unknown(_) => false,
};
if !implemented {
return Err(Error::UnsupportedAlgorithm);
}
self.alg = Some(alg);
Ok(self)
}
pub fn key_use(mut self, key_use: KeyUse) -> Result<JwkSelector> {
if !matches!(key_use, KeyUse::Sig | KeyUse::Enc) {
return Err(Error::UnsupportedAlgorithm);
}
self.key_use = Some(key_use);
Ok(self)
}
pub fn key_op(mut self, key_op: KeyOp) -> Result<JwkSelector> {
if matches!(key_op, KeyOp::Unknown(_)) {
return Err(Error::UnsupportedAlgorithm);
}
self.key_op = Some(key_op);
Ok(self)
}
}
fn import_member(member: &Value) -> Result<Option<Jwk>> {
let Some(object) = member.as_object() else {
return Ok(None);
};
if is_unsupported_key_kind(object) {
return Ok(None);
}
Ok(Jwk::from_object(object).ok())
}
impl fmt::Debug for JwkSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("JwkSet")
.field("keys", &self.keys)
.field("skipped", &self.skipped)
.field("other_names", &MemberNames(&self.other))
.finish()
}
}
impl ZeroizeOnDrop for JwkSet {}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use crate::Error;
use crate::json;
use crate::jwa::{KeyMgmtAlg, SigningAlg};
use crate::jwk::{Jwk, JwkSet, KeyAlg, KeyMaterial, KeyType};
const EC_P256: &str = r#"{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"}"#;
const RSA_2048: &str = r#"{"kty":"RSA","n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw","e":"AQAB"}"#;
const OCT: &str = r#"{"kty":"oct","k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"}"#;
const ED25519: &str =
r#"{"kty":"OKP","crv":"Ed25519","x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"}"#;
const X25519: &str =
r#"{"kty":"OKP","crv":"X25519","x":"3p7bfXt9wbTTW2HC7OQ1Nz-DQ8hbeGdNrfx-FG-IK08"}"#;
fn ec(crv: &str, len: usize) -> Jwk {
let (x, y) = match crv {
"P-384" => (
"qofKIr6LBTeOscce8yCtdG4dO2KLp5uYWfdB4IJUKjhVAvJdv1UpbDpUXjhydgq3",
"NhfeSpYmLG9dnpi_kpLcKfj0Hb0omhR86doxE7XwuMAKYLHOHX6BnXpDHXyQ6g5f",
),
"P-521" => (
"AMaFjga3BATpzZ4-y2YjlbRCnGSBOQU_tSH4KK9ga009uqFLXnfv51ko_h3BJ6L_qN4zSLPBhWpCm_l-fjHC5b1m",
"ARg5KWp4mjvABFyKX7QsfRvZmPVESVebRGgXr70XJz5mLJfucple9CZAxVC5AT-tB2E1PHCGonLCQIi-lHaf0WZQ",
),
"secp256k1" => (
"eb5mfvncu6xVoGKVzocLBwKb_NstzijZWfKBWxb4F5g",
"SDradyajxGVdpPv8DhEIqP0XtEimhVQZnEfQj_sQ1Lg",
),
other => panic!("no EC fixture for {other}"),
};
let json = format!(r#"{{"kty":"EC","crv":"{crv}","x":"{x}","y":"{y}"}}"#);
let key = Jwk::from_json(json.as_bytes()).unwrap();
let KeyMaterial::Ec(ec) = key.material() else {
unreachable!()
};
assert_eq!(ec.x().len(), len, "{crv}");
key
}
fn jwk(json: &str) -> Jwk {
Jwk::from_json(json.as_bytes()).unwrap()
}
#[test]
fn import_with_10_000_members_per_object_is_n_log_n() {
const PER_OBJECT: usize = 10_000;
const OCT_K: &str = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow";
let mut key = format!(r#"{{"kty":"oct","k":"{OCT_K}""#);
for i in 0..PER_OBJECT - 2 {
key.push_str(&format!(r#","u{i}":{i}"#));
}
key.push('}');
let mut set = format!(r#"{{"keys":[{key}]"#);
for i in 0..PER_OBJECT - 1 {
set.push_str(&format!(r#","s{i}":{i}"#));
}
set.push('}');
let (set, count) =
json::Object::count_name_comparisons(|| JwkSet::from_json(set.as_bytes()));
let set = set.expect("the set imports");
assert_eq!((set.len(), set.skipped()), (1, 0));
assert_eq!(set.keys()[0].other_params().len(), PER_OBJECT - 2);
assert_eq!(set.other_params().len(), PER_OBJECT - 1);
let n = (2 * PER_OBJECT) as u64;
let bound = 4 * n * u64::from(u64::BITS - n.leading_zeros());
assert!(count <= bound, "{count} comparisons");
}
#[test]
fn family_table_matches_adr_0026_step_8() {
let keys = [
("oct", jwk(OCT)),
("RSA", jwk(RSA_2048)),
("P-256", jwk(EC_P256)),
("P-384", ec("P-384", 48)),
("P-521", ec("P-521", 66)),
("secp256k1", ec("secp256k1", 32)),
("Ed25519", jwk(ED25519)),
("X25519", jwk(X25519)),
];
let table: &[(SigningAlg, &str)] = &[
(SigningAlg::HS256, "oct"),
(SigningAlg::HS384, "oct"),
(SigningAlg::HS512, "oct"),
(SigningAlg::RS256, "RSA"),
(SigningAlg::RS384, "RSA"),
(SigningAlg::RS512, "RSA"),
(SigningAlg::PS256, "RSA"),
(SigningAlg::PS384, "RSA"),
(SigningAlg::PS512, "RSA"),
(SigningAlg::ES256, "P-256"),
(SigningAlg::ES384, "P-384"),
(SigningAlg::ES512, "P-521"),
(SigningAlg::ES256K, "secp256k1"),
(SigningAlg::EdDSA, "Ed25519"),
(SigningAlg::Ed25519, "Ed25519"),
];
assert_eq!(table.len() + 1, SigningAlg::KNOWN.len());
for (alg, family) in table {
for (kind, key) in &keys {
assert_eq!(
key.material().serves_signing_alg(alg),
kind == family,
"{alg} with a {kind} key"
);
}
}
for alg in [
SigningAlg::None,
SigningAlg::from("Ed448"),
SigningAlg::from("hs256"),
] {
for (kind, key) in &keys {
assert!(
!key.material().serves_signing_alg(&alg),
"{alg} with {kind}"
);
}
}
}
const A1: &str = r#"{"keys":
[
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"use":"enc",
"kid":"1"},
{"kty":"RSA",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"alg":"RS256",
"kid":"2011-04-29"}
]
}"#;
const A2: &str = r#"{"keys":
[
{"kty":"EC",
"crv":"P-256",
"x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4",
"y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM",
"d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE",
"use":"enc",
"kid":"1"},
{"kty":"RSA",
"n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
"e":"AQAB",
"d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q",
"p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs",
"q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk",
"dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0",
"dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk",
"qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU",
"alg":"RS256",
"kid":"2011-04-29"}
]
}"#;
const A3: &str = r#"{"keys":
[
{"kty":"oct",
"alg":"A128KW",
"k":"GawgguFyGrWKav7AX4VKUg"},
{"kty":"oct",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow",
"kid":"HMAC key used in JWS spec Appendix A.1 example"}
]
}"#;
fn set_of(members: &[&str]) -> String {
format!(r#"{{"keys":[{}]}}"#, members.join(","))
}
fn parse(json: &str) -> Result<JwkSet, Error> {
JwkSet::from_json(json.as_bytes())
}
fn kids(set: &JwkSet) -> Vec<Option<&str>> {
set.keys().iter().map(Jwk::kid).collect()
}
#[test]
fn rfc7517_a1_public_keys_parse_in_order() {
let set = parse(A1).unwrap();
assert_eq!(set.len(), 2);
assert!(!set.is_empty());
assert_eq!(set.skipped(), 0);
assert_eq!(kids(&set), [Some("1"), Some("2011-04-29")]);
assert_eq!(set.keys()[0].key_type(), KeyType::Ec);
assert_eq!(set.keys()[1].key_type(), KeyType::Rsa);
assert!(set.other_params().is_empty());
}
#[test]
fn rfc7517_a3_symmetric_keys_parse_in_order() {
let set = parse(A3).unwrap();
assert_eq!(set.len(), 2);
assert_eq!(set.skipped(), 0);
assert!(set.keys().iter().all(|k| k.key_type() == KeyType::Oct));
assert_eq!(
set.keys()[0].alg(),
Some(&KeyAlg::KeyMgmt(KeyMgmtAlg::A128KW))
);
assert_eq!(
kids(&set),
[None, Some("HMAC key used in JWS spec Appendix A.1 example")]
);
}
#[test]
fn rfc7517_a2_rsa_private_key_is_imported_as_is() {
let set = parse(A2).unwrap();
assert_eq!((set.len(), set.skipped()), (2, 0));
assert_eq!(kids(&set), [Some("1"), Some("2011-04-29")]);
let rsa = &set.keys()[1];
let KeyMaterial::Rsa(key) = rsa.material() else {
panic!("RSA");
};
assert!(key.is_private() && !rsa.is_public());
assert_eq!(key.modulus_bits(), 2048);
let public = rsa.to_public().unwrap();
assert!(public.is_public());
assert!(!public.to_json().contains(r#""d""#));
assert_eq!(rsa.alg(), Some(&KeyAlg::Signing(SigningAlg::RS256)));
}
#[test]
fn unsupported_key_kinds_are_skipped() {
let unsupported = [
r#"{"kty":"XYZ","kid":"made-up"}"#,
r#"{"kty":"OKP","crv":"Ed448","x":"AAAA","kid":"ed448"}"#,
r#"{"kty":"OKP","crv":"X448","x":"AAAA"}"#,
r#"{"kty":"EC","crv":"P-192","x":"AAAA","y":"AAAA"}"#,
r#"{"kty":"EC","crv":"Ed25519","x":"AAAA"}"#,
r#"{"kty":"ec","crv":"P-256"}"#,
];
for member in unsupported {
let json = set_of(&[ED25519, member, EC_P256]);
let set = parse(&json).unwrap_or_else(|e| panic!("{member}: {e:?}"));
assert_eq!(set.len(), 2, "{member}");
assert_eq!(set.skipped(), 1, "{member}");
assert_eq!(set.keys()[0].key_type(), KeyType::Okp);
assert_eq!(set.keys()[1].key_type(), KeyType::Ec);
}
}
#[test]
fn invalid_members_are_skipped() {
let invalid = [
r#""a string""#,
"42",
"null",
"true",
"[]",
"{}",
r#"{"crv":"P-256"}"#,
r#"{"kty":7}"#,
r#"{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4"}"#,
r#"{"kty":"EC","x":"AAAA"}"#,
r#"{"kty":"oct"}"#,
r#"{"kty":"RSA","e":"AQAB"}"#,
r#"{"kty":"OKP","crv":"Ed25519","x":"AAAA"}"#,
r#"{"kty":"oct","k":""}"#,
r#"{"kty":"RSA","n":"AQAB","e":"AQAB"}"#,
r#"{"kty":"oct","k":"AA=="}"#,
r#"{"kty":"oct","k":"!!!!"}"#,
r#"{"kty":"oct","k":"AAEC","kid":1}"#,
r#"{"kty":"oct","k":"AAEC","use":["sig"]}"#,
r#"{"kty":"oct","k":"AAEC","key_ops":["sign","sign"]}"#,
r#"{"kty":"oct","k":"AAEC","use":"sig","key_ops":["encrypt"]}"#,
];
for member in invalid {
let json = set_of(&[OCT, member, RSA_2048]);
let set = parse(&json).unwrap_or_else(|e| panic!("{member}: {e:?}"));
assert_eq!(set.skipped(), 1, "{member}");
assert_eq!(
set.keys().iter().map(Jwk::key_type).collect::<Vec<_>>(),
[KeyType::Oct, KeyType::Rsa],
"{member}"
);
}
}
#[test]
fn unusable_rsa_private_members_are_skipped() {
let unusable = [
r#"{"kty":"RSA","d":"AQAB"}"#,
r#"{"kty":"RSA","n":"AQAB","e":"AQAB","p":"AQAB"}"#,
r#"{"kty":"RSA","oth":[]}"#,
r#"{"kty":"RSA","qi":7,"kid":"broken"}"#,
];
for member in unusable {
for members in [
[member, OCT, EC_P256],
[OCT, member, EC_P256],
[r#"{"kty":"XYZ"}"#, r#"{"kty":"oct"}"#, member],
] {
let set = parse(&set_of(&members)).unwrap();
assert_eq!(set.len() + set.skipped(), 3, "{members:?}");
assert!(
set.keys().iter().all(|key| key.key_type() != KeyType::Rsa),
"{members:?}"
);
}
}
}
#[test]
fn json_level_errors_fail_the_whole_set() {
let deep = format!(
r#"{{"keys":[{{"kty":"XYZ","x":{}1{}}}]}}"#,
"[".repeat(40),
"]".repeat(40)
);
let documents = [
String::new(),
"{".to_owned(),
r#"{"keys":[]"#.to_owned(),
r#"{"keys":[],}"#.to_owned(),
r#"{"keys":[],"keys":[]}"#.to_owned(),
set_of(&[r#"{"kty":"oct","k":"AAEC","kty":"oct"}"#]),
set_of(&[r#"{"kty":"XYZ","a":{"b":1,"b":2}}"#]),
r#"{"keys":[],"x":{"y":1,"y":1}}"#.to_owned(),
deep,
"[]".to_owned(),
r#""keys""#.to_owned(),
"null".to_owned(),
];
for json in &documents {
assert!(
matches!(parse(json), Err(Error::InvalidJson(_))),
"{json}: {:?}",
parse(json)
);
}
let invalid_utf8 = b"{\"keys\":[],\"x\":\"\xff\"}";
assert!(matches!(
JwkSet::from_json(invalid_utf8),
Err(Error::InvalidJson(_))
));
}
#[test]
fn missing_or_non_array_keys_is_invalid_key() {
for json in [
"{}",
r#"{"Keys":[]}"#,
r#"{"keys":null}"#,
r#"{"keys":{}}"#,
r#"{"keys":"[]"}"#,
r#"{"keys":1}"#,
&format!(r#"{{"keys":{OCT}}}"#),
] {
assert!(
matches!(parse(json), Err(Error::InvalidKey(_))),
"{json}: {:?}",
parse(json)
);
}
}
#[test]
fn an_empty_or_all_skipped_set_is_empty() {
let set = parse(r#"{"keys":[]}"#).unwrap();
assert!(set.is_empty());
assert_eq!((set.len(), set.skipped()), (0, 0));
let set = parse(&set_of(&[r#"{"kty":"XYZ"}"#, "{}"])).unwrap();
assert!(set.is_empty());
assert_eq!(set.skipped(), 2);
}
#[test]
fn other_top_level_members_are_preserved_in_order() {
let set = parse(r#"{"z":1,"keys":[],"a":{"b":[true]},"m":"s"}"#).unwrap();
let names: Vec<&str> = set.other_params().iter().map(|(n, _)| n).collect();
assert_eq!(names, ["z", "a", "m"]);
assert_eq!(
set.other_params().get("m").and_then(json::Value::as_str),
Some("s")
);
}
#[test]
fn from_object_matches_from_json() {
let object = json::parse(A1.as_bytes()).unwrap().into_object().unwrap();
let from_object = JwkSet::from_object(&object).unwrap();
let from_json = parse(A1).unwrap();
assert_eq!(kids(&from_object), kids(&from_json));
let object = json::parse(A2.as_bytes()).unwrap().into_object().unwrap();
assert_eq!(
kids(&JwkSet::from_object(&object).unwrap()),
kids(&parse(A2).unwrap())
);
}
#[test]
fn debug_never_shows_secrets() {
let secret_k = "AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow";
let ec_d = "870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE";
let ec_private = format!(
r#"{{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","d":"{ec_d}"}}"#
);
let json = format!(r#"{{"keys":[{OCT},{ec_private}],"secret_value_member":"hunter2"}}"#);
let set = parse(&json).unwrap();
let debug = format!("{set:?}");
assert!(!debug.contains(secret_k), "{debug}");
assert!(!debug.contains(ec_d), "{debug}");
assert!(!debug.contains("hunter2"), "{debug}");
assert!(debug.contains("secret_value_member"), "{debug}");
assert!(debug.contains("<redacted>"), "{debug}");
assert!(debug.contains("skipped"), "{debug}");
}
fn with(member: &str, extra: &str) -> String {
match &member[1..] {
"}" => format!("{{{extra}}}"),
rest => format!("{{{extra},{rest}"),
}
}
fn candidates(set: &JwkSet, alg: &SigningAlg, kid: Option<&str>) -> Vec<usize> {
let found: Vec<(usize, &Jwk)> = set.verification_candidates(alg, kid).collect();
for (index, key) in &found {
assert!(core::ptr::eq(*key, &set.keys()[*index]));
}
found.into_iter().map(|(index, _)| index).collect()
}
fn set_from(members: &[String]) -> JwkSet {
let members: Vec<&str> = members.iter().map(String::as_str).collect();
parse(&set_of(&members)).unwrap()
}
#[test]
fn rfc7517_a1_selection() {
let set = parse(A1).unwrap();
assert_eq!(
candidates(&set, &SigningAlg::RS256, Some("2011-04-29")),
[1]
);
assert_eq!(candidates(&set, &SigningAlg::RS256, None), [1]);
assert!(candidates(&set, &SigningAlg::PS256, None).is_empty());
assert!(candidates(&set, &SigningAlg::ES256, Some("1")).is_empty());
assert!(candidates(&set, &SigningAlg::ES256, None).is_empty());
}
#[test]
fn header_kid_selects_by_exact_kid() {
let set = set_from(&[
with(OCT, r#""kid":"a""#),
with(OCT, r#""kid":"b""#),
OCT.to_owned(),
with(OCT, r#""kid":"A""#),
with(OCT, r#""kid":"a""#),
]);
let hs = SigningAlg::HS256;
assert_eq!(candidates(&set, &hs, Some("a")), [0, 4]);
assert_eq!(candidates(&set, &hs, Some("A")), [3]);
assert_eq!(candidates(&set, &hs, Some("b")), [1]);
for kid in ["c", "", "a ", " a", "\u{0430}"] {
assert!(candidates(&set, &hs, Some(kid)).is_empty(), "{kid:?}");
}
}
#[test]
fn without_header_kid_all_compatible_keys_are_candidates() {
let set = set_from(&[
OCT.to_owned(),
EC_P256.to_owned(),
with(OCT, r#""use":"sig""#),
with(OCT, r#""kid":"k1""#),
OCT.to_owned(),
with(OCT, r#""use":"enc""#),
]);
assert_eq!(candidates(&set, &SigningAlg::HS256, None), [0, 2, 3, 4]);
assert_eq!(candidates(&set, &SigningAlg::HS512, None), [0, 2, 3, 4]);
assert_eq!(candidates(&set, &SigningAlg::ES256, None), [1]);
}
#[test]
fn jwk_alg_must_equal_header_alg() {
let set = set_from(&[
with(RSA_2048, r#""alg":"RS256""#),
with(RSA_2048, r#""alg":"RS384""#),
RSA_2048.to_owned(),
with(RSA_2048, r#""alg":"PS256""#),
with(RSA_2048, r#""alg":"rs256""#),
with(RSA_2048, r#""alg":"RSA-OAEP-256""#),
]);
assert_eq!(candidates(&set, &SigningAlg::RS256, None), [0, 2]);
assert_eq!(candidates(&set, &SigningAlg::RS384, None), [1, 2]);
assert_eq!(candidates(&set, &SigningAlg::PS256, None), [2, 3]);
assert_eq!(candidates(&set, &SigningAlg::PS512, None), [2]);
let set = set_from(&[
with(OCT, r#""alg":"A128KW""#),
with(OCT, r#""alg":"HS256""#),
]);
assert_eq!(candidates(&set, &SigningAlg::HS256, None), [1]);
assert!(candidates(&set, &SigningAlg::HS384, None).is_empty());
}
#[test]
fn eddsa_is_not_ed25519() {
let set = set_from(&[
with(ED25519, r#""alg":"EdDSA""#),
with(ED25519, r#""alg":"Ed25519""#),
ED25519.to_owned(),
X25519.to_owned(),
]);
assert_eq!(candidates(&set, &SigningAlg::EdDSA, None), [0, 2]);
assert_eq!(candidates(&set, &SigningAlg::Ed25519, None), [1, 2]);
}
#[test]
fn use_and_key_ops_must_allow_verify() {
let set = set_from(&[
with(EC_P256, r#""use":"sig""#),
with(EC_P256, r#""use":"enc""#),
with(EC_P256, r#""use":"x-custom""#),
with(EC_P256, r#""key_ops":["verify"]"#),
with(EC_P256, r#""key_ops":["sign"]"#),
with(EC_P256, r#""key_ops":["sign","verify"]"#),
with(EC_P256, r#""key_ops":[]"#),
with(EC_P256, r#""key_ops":["encrypt","verify"]"#),
with(EC_P256, r#""use":"sig","key_ops":["sign"]"#),
with(EC_P256, r#""use":"sig","key_ops":["verify"]"#),
with(EC_P256, r#""key_ops":["Verify"]"#),
]);
assert_eq!(set.skipped(), 0);
assert_eq!(candidates(&set, &SigningAlg::ES256, None), [0, 3, 5, 7, 9]);
}
#[test]
fn family_must_match() {
let set = set_from(&[
EC_P256.to_owned(),
OCT.to_owned(),
ec("P-384", 48).to_json().to_string(),
RSA_2048.to_owned(),
ED25519.to_owned(),
]);
assert_eq!(candidates(&set, &SigningAlg::HS256, None), [1]);
assert_eq!(candidates(&set, &SigningAlg::ES256, None), [0]);
assert_eq!(candidates(&set, &SigningAlg::ES384, None), [2]);
assert!(candidates(&set, &SigningAlg::ES512, None).is_empty());
assert!(candidates(&set, &SigningAlg::ES256K, None).is_empty());
assert_eq!(candidates(&set, &SigningAlg::RS256, None), [3]);
assert_eq!(candidates(&set, &SigningAlg::EdDSA, None), [4]);
let set = set_from(&[with(EC_P256, r#""kid":"k""#)]);
assert!(candidates(&set, &SigningAlg::HS256, Some("k")).is_empty());
}
#[test]
fn none_and_unknown_algorithms_select_nothing() {
let set = set_from(&[
OCT.to_owned(),
with(OCT, r#""alg":"none""#),
with(OCT, r#""alg":"HS999""#),
RSA_2048.to_owned(),
]);
for alg in [
SigningAlg::None,
SigningAlg::from("HS999"),
SigningAlg::from(""),
] {
assert!(candidates(&set, &alg, None).is_empty(), "{alg}");
}
}
#[test]
fn ec_private_members_are_candidates() {
let set = parse(&set_of(&[
r#"{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE","kid":"1"}"#,
]))
.unwrap();
assert_eq!(candidates(&set, &SigningAlg::ES256, Some("1")), [0]);
}
#[test]
fn indices_count_kept_keys_only() {
let set = set_from(&[
r#"{"kty":"XYZ"}"#.to_owned(),
with(OCT, r#""kid":"a""#),
"{}".to_owned(),
with(OCT, r#""kid":"b""#),
]);
assert_eq!(set.skipped(), 2);
assert_eq!(candidates(&set, &SigningAlg::HS256, Some("b")), [1]);
assert_eq!(candidates(&set, &SigningAlg::HS256, None), [0, 1]);
}
mod properties {
use proptest::prelude::*;
use super::*;
#[derive(Clone, Copy, Debug)]
enum Expect {
Keep(
&'static str,
Option<&'static str>,
Option<&'static str>,
Option<&'static [&'static str]>,
),
Skip,
}
fn pool() -> Vec<(String, Expect)> {
use Expect::{Keep, Skip};
vec![
(OCT.to_owned(), Keep("oct", None, None, None)),
(
with(OCT, r#""alg":"HS384""#),
Keep("oct", Some("HS384"), None, None),
),
(
with(OCT, r#""use":"enc""#),
Keep("oct", None, Some("enc"), None),
),
(
with(OCT, r#""alg":"A128KW""#),
Keep("oct", Some("A128KW"), None, None),
),
(EC_P256.to_owned(), Keep("P-256", None, None, None)),
(
with(EC_P256, r#""use":"sig""#),
Keep("P-256", None, Some("sig"), None),
),
(
with(EC_P256, r#""key_ops":["sign"]"#),
Keep("P-256", None, None, Some(&["sign"])),
),
(
with(EC_P256, r#""key_ops":["verify"]"#),
Keep("P-256", None, None, Some(&["verify"])),
),
(
ec("P-384", 48).to_json().to_string(),
Keep("P-384", None, None, None),
),
(
ec("secp256k1", 32).to_json().to_string(),
Keep("secp256k1", None, None, None),
),
(RSA_2048.to_owned(), Keep("RSA", None, None, None)),
(
with(RSA_2048, r#""alg":"PS256""#),
Keep("RSA", Some("PS256"), None, None),
),
(ED25519.to_owned(), Keep("Ed25519", None, None, None)),
(
with(ED25519, r#""alg":"Ed25519""#),
Keep("Ed25519", Some("Ed25519"), None, None),
),
(
with(ED25519, r#""alg":"EdDSA""#),
Keep("Ed25519", Some("EdDSA"), None, None),
),
(
with(ED25519, r#""use":"x-other""#),
Keep("Ed25519", None, Some("x-other"), None),
),
(X25519.to_owned(), Keep("X25519", None, None, None)),
(r#"{"kty":"XYZ"}"#.to_owned(), Skip),
(r#"{"kty":"OKP","crv":"Ed448","x":"AA"}"#.to_owned(), Skip),
(
r#"{"kty":"EC","crv":"P-256","x":"AA","y":"AA"}"#.to_owned(),
Skip,
),
(r#"{"kty":"RSA","n":"AQAB","e":"AQAB"}"#.to_owned(), Skip),
(r#"{"kty":"oct","k":"AA=="}"#.to_owned(), Skip),
("{}".to_owned(), Skip),
(r#"{"kty":"RSA","d":"AQAB"}"#.to_owned(), Skip),
]
}
fn family_of(alg: &str) -> Option<&'static str> {
Some(match alg {
"HS256" | "HS384" | "HS512" => "oct",
"RS256" | "RS384" | "RS512" | "PS256" | "PS384" | "PS512" => "RSA",
"ES256" => "P-256",
"ES384" => "P-384",
"ES512" => "P-521",
"ES256K" => "secp256k1",
"EdDSA" | "Ed25519" => "Ed25519",
_ => return None,
})
}
const KIDS: &[Option<&str>] = &[None, Some("a"), Some("b"), Some("A")];
fn header_alg() -> impl Strategy<Value = SigningAlg> {
let mut algs: Vec<SigningAlg> = SigningAlg::KNOWN.to_vec();
algs.push(SigningAlg::from("HS1"));
algs.push(SigningAlg::from("A128KW"));
prop::sample::select(algs)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(512))]
#[test]
fn arbitrary_bytes_never_panic(bytes in prop::collection::vec(any::<u8>(), 0..256)) {
match JwkSet::from_json(&bytes) {
Ok(set) => prop_assert!(set.len() + set.skipped() <= bytes.len()),
Err(e) => prop_assert!(
matches!(e, Error::InvalidJson(_) | Error::InvalidKey(_) | Error::UnsupportedAlgorithm),
"{e:?}"
),
}
}
#[test]
fn mutated_a1_never_panics(
edits in prop::collection::vec((any::<prop::sample::Index>(), any::<u8>()), 1..4)
) {
let mut bytes = A1.as_bytes().to_vec();
for (index, byte) in edits {
let at = index.index(bytes.len());
bytes[at] = byte;
}
if let Err(e) = JwkSet::from_json(&bytes) {
prop_assert!(
matches!(e, Error::InvalidJson(_) | Error::InvalidKey(_) | Error::UnsupportedAlgorithm),
"{e:?}"
);
}
}
#[test]
fn generated_sets_and_selection_match_the_oracle(
picks in prop::collection::vec(
(any::<prop::sample::Index>(), prop::sample::select(KIDS)),
0..12,
),
alg in header_alg(),
header_kid in prop::sample::select(KIDS),
) {
let pool = pool();
let mut members = Vec::new();
let mut expected = Vec::new();
for (index, kid) in &picks {
let (json, expect) = &pool[index.index(pool.len())];
let json = match kid {
Some(kid) => with(json, &format!(r#""kid":"{kid}""#)),
None => json.clone(),
};
members.push(json);
match expect {
Expect::Keep(..) => expected.push((*expect, *kid)),
Expect::Skip => {}
}
}
let members: Vec<&str> = members.iter().map(String::as_str).collect();
let set = parse(&set_of(&members)).unwrap();
prop_assert_eq!(set.len(), expected.len());
prop_assert_eq!(set.skipped(), picks.len() - expected.len());
let expected_kids: Vec<Option<&str>> = expected.iter().map(|(_, kid)| *kid).collect();
prop_assert_eq!(kids(&set), expected_kids);
let oracle: Vec<usize> = expected
.iter()
.enumerate()
.filter(|(_, (expect, kid))| {
let Expect::Keep(family, key_alg, key_use, key_ops) = *expect else {
unreachable!()
};
family_of(alg.as_str()) == Some(family)
&& key_alg.is_none_or(|a| a == alg.as_str())
&& key_use.is_none_or(|u| u == "sig")
&& key_ops.is_none_or(|ops| ops.contains(&"verify"))
&& header_kid.is_none_or(|h| *kid == Some(h))
})
.map(|(index, _)| index)
.collect();
prop_assert_eq!(candidates(&set, &alg, header_kid), oracle);
}
}
}
mod rule {
use super::*;
use crate::jwa::{KeyMgmtAlg, SigningAlg};
use crate::jwe::key_ops_either_direction;
use crate::jwk::{JwkSelector, KeyOp, KeyUse};
fn by_alg(alg: &str) -> JwkSelector {
JwkSelector::new().alg(KeyAlg::from(alg)).unwrap()
}
fn matched(set: &JwkSet, selector: &JwkSelector) -> Vec<usize> {
let found: Vec<(usize, &Jwk)> = set.matching(selector).collect();
for (index, key) in &found {
assert!(core::ptr::eq(*key, &set.keys()[*index]));
}
found.into_iter().map(|(index, _)| index).collect()
}
fn families() -> JwkSet {
let set = set_from(&[
OCT.to_owned(),
RSA_2048.to_owned(),
EC_P256.to_owned(),
ec("P-384", 48).to_json().to_string(),
ec("P-521", 66).to_json().to_string(),
ec("secp256k1", 32).to_json().to_string(),
ED25519.to_owned(),
X25519.to_owned(),
]);
assert_eq!(set.len(), 8);
set
}
const OCT_I: usize = 0;
const RSA_I: usize = 1;
const P256_I: usize = 2;
const P384_I: usize = 3;
const P521_I: usize = 4;
const K256_I: usize = 5;
const ED25519_I: usize = 6;
const X25519_I: usize = 7;
#[test]
fn family_follows_the_core_tables_for_every_alg() {
let set = families();
let jws: &[(&str, &[usize])] = &[
("HS256", &[OCT_I]),
("HS384", &[OCT_I]),
("HS512", &[OCT_I]),
("RS256", &[RSA_I]),
("RS384", &[RSA_I]),
("RS512", &[RSA_I]),
("PS256", &[RSA_I]),
("PS384", &[RSA_I]),
("PS512", &[RSA_I]),
("ES256", &[P256_I]),
("ES384", &[P384_I]),
("ES512", &[P521_I]),
("ES256K", &[K256_I]),
("EdDSA", &[ED25519_I]),
("Ed25519", &[ED25519_I]),
];
assert_eq!(
jws.len() + 1,
SigningAlg::KNOWN.len(),
"every JWS alg but none"
);
let ecdh: &[usize] = &[P256_I, P384_I, P521_I, X25519_I];
let jwe: &[(&str, &[usize])] = &[
("RSA-OAEP-256", &[RSA_I]),
("A128KW", &[OCT_I]),
("A192KW", &[OCT_I]),
("A256KW", &[OCT_I]),
("dir", &[OCT_I]),
("A128GCMKW", &[OCT_I]),
("A192GCMKW", &[OCT_I]),
("A256GCMKW", &[OCT_I]),
("PBES2-HS256+A128KW", &[OCT_I]),
("PBES2-HS384+A192KW", &[OCT_I]),
("PBES2-HS512+A256KW", &[OCT_I]),
("ECDH-ES", ecdh),
("ECDH-ES+A128KW", ecdh),
("ECDH-ES+A192KW", ecdh),
("ECDH-ES+A256KW", ecdh),
];
assert_eq!(
jwe.len(),
KeyMgmtAlg::KNOWN.len(),
"every JWE key management alg"
);
for (alg, expected) in jws.iter().chain(jwe) {
assert_eq!(matched(&set, &by_alg(alg)), *expected, "{alg}");
}
}
#[test]
fn family_never_crosses_for_the_named_pairs() {
let set = families();
let never: &[(usize, &[&str])] = &[
(
OCT_I,
&["RS256", "ES256", "EdDSA", "ECDH-ES", "RSA-OAEP-256"],
),
(RSA_I, &["HS256", "A128KW", "dir", "PBES2-HS256+A128KW"]),
(P256_I, &["HS256", "A128KW", "dir", "PBES2-HS256+A128KW"]),
(X25519_I, &["EdDSA"]),
(ED25519_I, &["ECDH-ES"]),
(K256_I, &["ECDH-ES"]),
(P384_I, &["ES256"]),
];
for (member, algs) in never {
for alg in *algs {
assert!(
!matched(&set, &by_alg(alg)).contains(member),
"{member} {alg}"
);
}
}
let set = set_from(&[with(OCT, r#""kid":"k""#)]);
assert!(matched(&set, &by_alg("ES256").kid("k")).is_empty());
}
#[test]
fn jwk_alg_is_absent_or_exact() {
let set = set_from(&[
with(ED25519, r#""alg":"EdDSA""#),
with(ED25519, r#""alg":"Ed25519""#),
ED25519.to_owned(),
]);
assert_eq!(matched(&set, &by_alg("EdDSA")), [0, 2]);
assert_eq!(matched(&set, &by_alg("Ed25519")), [1, 2]);
let set = set_from(&[
with(OCT, r#""alg":"A128KW""#),
with(OCT, r#""alg":"A256KW""#),
OCT.to_owned(),
with(OCT, r#""alg":"x-unregistered""#),
with(OCT, r#""alg":"HS256""#),
]);
assert_eq!(set.len(), 5);
assert_eq!(matched(&set, &by_alg("A128KW")), [0, 2]);
assert_eq!(matched(&set, &by_alg("A256KW")), [1, 2]);
assert_eq!(matched(&set, &by_alg("HS256")), [2, 4]);
assert_eq!(matched(&set, &by_alg("HS384")), [2]);
assert_eq!(matched(&set, &by_alg("A192KW")), [2]);
assert_eq!(matched(&set, &JwkSelector::new()), [0, 1, 2, 3, 4]);
}
#[test]
fn dir_alone_matches_a_member_named_by_its_enc() {
let set = set_from(&[
with(OCT, r#""alg":"A256GCM""#),
with(OCT, r#""alg":"A128CBC-HS256""#),
with(OCT, r#""alg":"dir""#),
with(OCT, r#""alg":"A256KW""#),
]);
assert_eq!(matched(&set, &by_alg("dir")), [0, 1, 2]);
assert_eq!(matched(&set, &by_alg("A256KW")), [3]);
assert!(matched(&set, &by_alg("A256GCMKW")).is_empty());
assert!(matched(&set, &by_alg("PBES2-HS256+A128KW")).is_empty());
assert!(matched(&set, &by_alg("HS256")).is_empty());
}
#[test]
fn purpose_comes_from_key_use_then_key_op_then_alg() {
let set = set_from(&[
with(EC_P256, r#""use":"sig""#),
with(EC_P256, r#""use":"enc""#),
EC_P256.to_owned(),
with(EC_P256, r#""use":"x-other""#),
]);
assert_eq!(matched(&set, &by_alg("ES256")), [0, 2]);
assert_eq!(matched(&set, &by_alg("ECDH-ES")), [1, 2]);
let op = |op: KeyOp| JwkSelector::new().key_op(op).unwrap();
assert_eq!(matched(&set, &op(KeyOp::Sign)), [0, 2]);
assert_eq!(matched(&set, &op(KeyOp::Verify)), [0, 2]);
for enc_op in [
KeyOp::Encrypt,
KeyOp::Decrypt,
KeyOp::WrapKey,
KeyOp::UnwrapKey,
KeyOp::DeriveKey,
KeyOp::DeriveBits,
] {
assert_eq!(matched(&set, &op(enc_op.clone())), [1, 2], "{enc_op}");
}
let key_use = |u: KeyUse| JwkSelector::new().key_use(u).unwrap();
assert_eq!(matched(&set, &key_use(KeyUse::Sig)), [0, 2]);
assert_eq!(matched(&set, &key_use(KeyUse::Enc)), [1, 2]);
let agree = by_alg("ES256")
.key_use(KeyUse::Sig)
.unwrap()
.key_op(KeyOp::Verify);
assert_eq!(matched(&set, &agree.unwrap()), [0, 2]);
let set = set_from(&[
with(RSA_2048, r#""use":"sig""#),
with(RSA_2048, r#""use":"enc""#),
RSA_2048.to_owned(),
]);
assert_eq!(matched(&set, &by_alg("RSA-OAEP-256")), [1, 2]);
assert_eq!(matched(&set, &by_alg("RS256")), [0, 2]);
}
#[test]
fn disagreeing_parts_match_nothing() {
let set = set_from(&[
EC_P256.to_owned(),
with(EC_P256, r#""use":"enc""#),
with(EC_P256, r#""use":"sig""#),
OCT.to_owned(),
]);
let conflicts = [
by_alg("ES256").key_use(KeyUse::Enc).unwrap(),
JwkSelector::new()
.key_op(KeyOp::Verify)
.unwrap()
.key_use(KeyUse::Enc)
.unwrap(),
by_alg("ES256").key_op(KeyOp::WrapKey).unwrap(),
by_alg("ECDH-ES").key_use(KeyUse::Sig).unwrap(),
by_alg("ECDH-ES").key_op(KeyOp::Sign).unwrap(),
by_alg("A128KW").key_op(KeyOp::Verify).unwrap(),
JwkSelector::new()
.key_use(KeyUse::Sig)
.unwrap()
.key_op(KeyOp::DeriveKey)
.unwrap(),
];
for selector in &conflicts {
assert!(matched(&set, selector).is_empty(), "{selector:?}");
assert_eq!(
set.select(selector).map(|(i, _)| i),
Err(Error::NoMatchingKey),
"{selector:?}"
);
}
}
#[test]
fn without_a_purpose_use_is_not_checked() {
let set = set_from(&[
with(EC_P256, r#""use":"enc","kid":"e""#),
with(EC_P256, r#""use":"x-other","kid":"x""#),
with(EC_P256, r#""use":"sig","kid":"s""#),
]);
for (index, kid) in ["e", "x", "s"].into_iter().enumerate() {
assert_eq!(matched(&set, &JwkSelector::new().kid(kid)), [index]);
}
assert_eq!(matched(&set, &JwkSelector::new()), [0, 1, 2]);
}
#[test]
fn key_ops_with_a_key_op_is_permits() {
let set = set_from(&[
with(EC_P256, r#""key_ops":["sign"]"#),
with(EC_P256, r#""key_ops":["verify"]"#),
with(EC_P256, r#""key_ops":["sign","verify"]"#),
with(EC_P256, r#""key_ops":[]"#),
EC_P256.to_owned(),
with(EC_P256, r#""key_ops":["Verify"]"#),
with(EC_P256, r#""key_ops":["deriveKey"]"#),
]);
for op in [KeyOp::Sign, KeyOp::Verify, KeyOp::DeriveKey, KeyOp::Encrypt] {
let selector = JwkSelector::new().key_op(op.clone()).unwrap();
let permits: Vec<usize> = (0..set.len())
.filter(|i| set.keys()[*i].permits(&op))
.collect();
assert_eq!(matched(&set, &selector), permits, "{op}");
}
let verify = JwkSelector::new().key_op(KeyOp::Verify).unwrap();
assert_eq!(matched(&set, &verify), [1, 2, 4]);
}
#[test]
fn key_ops_with_an_alg_needs_an_op_of_either_direction() {
let ops = [
"wrapKey",
"unwrapKey",
"encrypt",
"decrypt",
"deriveKey",
"deriveBits",
"sign",
"verify",
];
let cases: &[(&str, &str, &[&str])] = &[
(OCT, "A128KW", &["wrapKey", "unwrapKey"]),
(OCT, "A128GCMKW", &["wrapKey", "unwrapKey"]),
(OCT, "PBES2-HS256+A128KW", &["wrapKey", "unwrapKey"]),
(RSA_2048, "RSA-OAEP-256", &["wrapKey", "unwrapKey"]),
(OCT, "dir", &["encrypt", "decrypt"]),
(EC_P256, "ECDH-ES", &["deriveKey"]),
(X25519, "ECDH-ES+A128KW", &["deriveKey"]),
(EC_P256, "ES256", &["sign", "verify"]),
(OCT, "HS256", &["sign", "verify"]),
];
for (material, alg, allowed) in cases {
let members: Vec<String> = ops
.iter()
.map(|op| with(material, &format!(r#""key_ops":["{op}"]"#)))
.chain([with(material, r#""key_ops":[]"#), material.to_string()])
.collect();
let set = set_from(&members);
assert_eq!(set.len(), ops.len() + 2);
let mut expected: Vec<usize> = ops
.iter()
.enumerate()
.filter(|(_, op)| allowed.contains(op))
.map(|(i, _)| i)
.collect();
expected.push(ops.len() + 1);
assert_eq!(matched(&set, &by_alg(alg)), expected, "{alg}");
}
}
#[test]
fn a_key_op_with_an_alg_must_be_an_op_of_the_alg() {
let ops = [
KeyOp::Sign,
KeyOp::Verify,
KeyOp::Encrypt,
KeyOp::Decrypt,
KeyOp::WrapKey,
KeyOp::UnwrapKey,
KeyOp::DeriveKey,
KeyOp::DeriveBits,
];
let cases: &[(&str, &str, &[KeyOp])] = &[
(EC_P256, "ES256", &[KeyOp::Sign, KeyOp::Verify]),
(OCT, "HS256", &[KeyOp::Sign, KeyOp::Verify]),
(OCT, "A128KW", &[KeyOp::WrapKey, KeyOp::UnwrapKey]),
(OCT, "A256GCMKW", &[KeyOp::WrapKey, KeyOp::UnwrapKey]),
(
OCT,
"PBES2-HS256+A128KW",
&[KeyOp::WrapKey, KeyOp::UnwrapKey],
),
(
RSA_2048,
"RSA-OAEP-256",
&[KeyOp::WrapKey, KeyOp::UnwrapKey],
),
(OCT, "dir", &[KeyOp::Encrypt, KeyOp::Decrypt]),
(EC_P256, "ECDH-ES", &[KeyOp::DeriveKey]),
(X25519, "ECDH-ES+A128KW", &[KeyOp::DeriveKey]),
];
for (material, alg, performed) in cases {
let mut members: Vec<String> = ops
.iter()
.map(|op| with(material, &format!(r#""key_ops":["{op}"]"#)))
.collect();
members.push(with(
material,
r#""key_ops":["sign","verify","encrypt","decrypt","wrapKey","unwrapKey","deriveKey","deriveBits"]"#,
));
members.push(material.to_string());
let set = set_from(&members);
assert_eq!(set.len(), ops.len() + 2);
for (i, op) in ops.iter().enumerate() {
let selector = by_alg(alg).key_op(op.clone()).unwrap();
let expected: Vec<usize> = if performed.contains(op) {
vec![i, ops.len(), ops.len() + 1]
} else {
Vec::new()
};
assert_eq!(matched(&set, &selector), expected, "{alg} + {op}");
if expected.is_empty() {
assert_eq!(
set.select(&selector).map(|(i, _)| i),
Err(Error::NoMatchingKey),
"{alg} + {op}"
);
}
}
}
}
#[test]
fn an_incoherent_key_op_matches_nothing() {
let set = set_from(&[with(OCT, r#""key_ops":["encrypt"]"#)]);
let a128kw_encrypt = by_alg("A128KW").key_op(KeyOp::Encrypt).unwrap();
assert!(matched(&set, &a128kw_encrypt).is_empty());
assert_eq!(
set.select(&a128kw_encrypt).map(|(i, _)| i),
Err(Error::NoMatchingKey)
);
let encrypt = JwkSelector::new().key_op(KeyOp::Encrypt).unwrap();
assert_eq!(matched(&set, &encrypt), [0]);
let dir_encrypt = by_alg("dir").key_op(KeyOp::Encrypt).unwrap();
assert_eq!(matched(&set, &dir_encrypt), [0]);
let bare_oct = set_from(&[OCT.to_owned()]);
let bare_p256 = set_from(&[EC_P256.to_owned()]);
let bare_x25519 = set_from(&[X25519.to_owned()]);
let pair = |alg: &str, op: KeyOp| by_alg(alg).key_op(op).unwrap();
let cases: &[(&JwkSet, JwkSelector, bool)] = &[
(&bare_oct, pair("A128KW", KeyOp::Encrypt), false),
(&bare_oct, pair("A128KW", KeyOp::WrapKey), true),
(&bare_oct, pair("A128KW", KeyOp::UnwrapKey), true),
(&bare_oct, pair("dir", KeyOp::Encrypt), true),
(&bare_oct, pair("dir", KeyOp::Decrypt), true),
(&bare_oct, pair("dir", KeyOp::WrapKey), false),
(&bare_p256, pair("ECDH-ES", KeyOp::DeriveKey), true),
(&bare_x25519, pair("ECDH-ES+A128KW", KeyOp::DeriveKey), true),
(&bare_x25519, pair("ECDH-ES+A128KW", KeyOp::WrapKey), false),
(&bare_p256, pair("ECDH-ES", KeyOp::DeriveBits), false),
(&bare_p256, pair("ES256", KeyOp::Verify), true),
(&bare_p256, pair("ES256", KeyOp::Encrypt), false),
];
for (set, selector, matches) in cases {
let expected: &[usize] = if *matches { &[0] } else { &[] };
assert_eq!(matched(set, selector), expected, "{selector:?}");
}
}
#[test]
fn the_key_op_rule_does_not_depend_on_setter_order() {
let set = set_from(&[
OCT.to_owned(),
with(OCT, r#""key_ops":["encrypt"]"#),
with(OCT, r#""key_ops":["wrapKey"]"#),
EC_P256.to_owned(),
]);
for alg in ["A128KW", "dir", "ES256", "HS256", "ECDH-ES"] {
for op in [
KeyOp::Encrypt,
KeyOp::WrapKey,
KeyOp::Verify,
KeyOp::DeriveKey,
] {
let alg_first = by_alg(alg).key_op(op.clone()).unwrap();
let op_first = JwkSelector::new()
.key_op(op.clone())
.unwrap()
.alg(KeyAlg::from(alg))
.unwrap();
assert_eq!(alg_first, op_first, "{alg} + {op}");
assert_eq!(
matched(&set, &alg_first),
matched(&set, &op_first),
"{alg} + {op}"
);
}
}
}
#[test]
fn the_jwe_key_ops_of_every_alg_come_from_key_op() {
for alg in KeyMgmtAlg::KNOWN {
let ops = key_ops_either_direction(alg);
assert!(ops.iter().all(Option::is_some), "{alg}");
}
assert_eq!(
key_ops_either_direction(&KeyMgmtAlg::from("RSA1_5")),
[None, None]
);
}
#[test]
fn key_ops_with_only_a_use_needs_an_op_of_that_use() {
let set = set_from(&[
with(OCT, r#""key_ops":["sign"]"#),
with(OCT, r#""key_ops":["encrypt"]"#),
with(OCT, r#""key_ops":["x-op"]"#),
with(OCT, r#""key_ops":[]"#),
with(OCT, r#""key_ops":["deriveBits"]"#),
OCT.to_owned(),
with(OCT, r#""key_ops":["x-op","verify"]"#),
]);
let key_use = |u: KeyUse| JwkSelector::new().key_use(u).unwrap();
assert_eq!(matched(&set, &key_use(KeyUse::Sig)), [0, 5, 6]);
assert_eq!(matched(&set, &key_use(KeyUse::Enc)), [1, 4, 5]);
assert_eq!(matched(&set, &JwkSelector::new()), [0, 1, 2, 3, 4, 5, 6]);
}
#[test]
fn kid_is_exact_equality_only() {
let set = set_from(&[
with(OCT, r#""kid":"a""#),
with(OCT, r#""kid":"A""#),
OCT.to_owned(),
with(OCT, r#""kid":"a ""#),
with(OCT, r#""kid":"../keys/a""#),
with(OCT, r#""kid":"""#),
]);
let kid = |kid: &str| matched(&set, &JwkSelector::new().kid(kid));
assert_eq!(kid("a"), [0]);
assert_eq!(kid("A"), [1]);
assert_eq!(kid("a "), [3]);
assert_eq!(kid("../keys/a"), [4]);
assert_eq!(kid(""), [5]);
for absent in ["*", "a*", "%61", "\u{0430}", "keys/a", "b"] {
assert!(kid(absent).is_empty(), "{absent:?}");
}
}
#[test]
fn an_alg_the_setter_refuses_matches_nothing() {
let set = families();
for alg in ["A256GCM", "none", "RSA1_5"] {
let selector = JwkSelector {
alg: Some(KeyAlg::from(alg)),
..JwkSelector::default()
};
assert!(matched(&set, &selector).is_empty(), "{alg}");
}
let set = set_from(&[with(OCT, r#""alg":"A256GCM""#)]);
let selector = JwkSelector {
alg: Some(KeyAlg::from("A256GCM")),
..JwkSelector::default()
};
assert!(matched(&set, &selector).is_empty());
}
}
mod results {
use core::cell::Cell;
use super::*;
use crate::jwa::SigningAlg;
use crate::jwk::set::{Criteria, exactly_one};
use crate::jwk::{JwkSelector, KeyUse};
use crate::jws::{VerifyOptions, verify_compact};
fn selected(set: &JwkSet, selector: &JwkSelector) -> Result<usize, Error> {
set.select(selector).map(|(index, key)| {
assert!(core::ptr::eq(key, &set.keys()[index]));
index
})
}
fn by_alg(alg: &str) -> JwkSelector {
JwkSelector::new().alg(KeyAlg::from(alg)).unwrap()
}
#[test]
fn select_returns_the_one_match_with_its_key_index() {
let set = set_from(&[
r#"{"kty":"XYZ","kid":"b"}"#.to_owned(),
with(OCT, r#""kid":"a""#),
"{}".to_owned(),
with(OCT, r#""kid":"b""#),
]);
assert_eq!(set.skipped(), 2);
assert_eq!(selected(&set, &JwkSelector::new().kid("b")), Ok(1));
assert_eq!(selected(&set, &JwkSelector::new().kid("a")), Ok(0));
let rfc_a1 = parse(A1).unwrap();
assert_eq!(
selected(&rfc_a1, &by_alg("ES256")),
Err(Error::NoMatchingKey)
);
assert_eq!(selected(&rfc_a1, &by_alg("ECDH-ES")), Ok(0));
assert_eq!(selected(&rfc_a1, &by_alg("RS256")), Ok(1));
}
#[test]
fn no_match_is_no_matching_key() {
let empty = parse(r#"{"keys":[]}"#).unwrap();
let all_skipped = set_from(&[r#"{"kty":"XYZ"}"#.to_owned(), "{}".to_owned()]);
assert!(all_skipped.is_empty());
let one = set_from(&[with(OCT, r#""kid":"a""#)]);
for (set, selector) in [
(&empty, JwkSelector::new()),
(&empty, by_alg("HS256")),
(&all_skipped, JwkSelector::new()),
(&one, JwkSelector::new().kid("b")),
(&one, by_alg("ES256")),
] {
assert_eq!(
selected(set, &selector),
Err(Error::NoMatchingKey),
"{selector:?}"
);
assert_eq!(set.matching(&selector).count(), 0);
}
}
#[test]
fn several_matches_is_multiple_matching_keys() {
let set = parse(A3).unwrap();
assert_eq!(
selected(&set, &JwkSelector::new()),
Err(Error::MultipleMatchingKeys)
);
let all: Vec<usize> = set.matching(&JwkSelector::new()).map(|(i, _)| i).collect();
assert_eq!(all, [0, 1]);
assert_eq!(selected(&set, &by_alg("HS256")), Ok(1));
let set = set_from(&[
with(OCT, r#""kid":"k""#),
EC_P256.to_owned(),
with(OCT, r#""kid":"k""#),
with(OCT, r#""kid":"k""#),
]);
let selector = JwkSelector::new().kid("k");
assert_eq!(selected(&set, &selector), Err(Error::MultipleMatchingKeys));
let all: Vec<usize> = set.matching(&selector).map(|(i, _)| i).collect();
assert_eq!(all, [0, 2, 3]);
}
#[test]
fn the_empty_selector_returns_the_only_key() {
for member in [OCT, RSA_2048, EC_P256, ED25519, X25519] {
let set = set_from(&[member.to_owned()]);
assert_eq!(selected(&set, &JwkSelector::new()), Ok(0), "{member}");
}
}
#[test]
fn the_scan_stops_at_the_second_match() {
let set = set_from(&[
OCT.to_owned(),
EC_P256.to_owned(),
OCT.to_owned(),
OCT.to_owned(),
OCT.to_owned(),
OCT.to_owned(),
]);
let selector = by_alg("HS256");
let criteria = Criteria::of(&selector);
let scanned = Cell::new(0);
let counting = |key: &Jwk| {
scanned.set(scanned.get() + 1);
criteria.matches(key)
};
let result = exactly_one(set.filter_keys(counting)).map(|(i, _)| i);
assert_eq!(result, Err(Error::MultipleMatchingKeys));
assert_eq!(scanned.get(), 3);
let selector = by_alg("ES256");
let criteria = Criteria::of(&selector);
scanned.set(0);
let counting = |key: &Jwk| {
scanned.set(scanned.get() + 1);
criteria.matches(key)
};
assert_eq!(
exactly_one(set.filter_keys(counting)).map(|(i, _)| i),
Ok(1)
);
assert_eq!(scanned.get(), set.len());
assert_eq!(
selected(&set, &by_alg("HS256")),
Err(Error::MultipleMatchingKeys)
);
assert_eq!(selected(&set, &by_alg("ES256")), Ok(1));
}
#[test]
fn select_agrees_with_matching() {
let set = set_from(&[
with(EC_P256, r#""kid":"1","use":"enc""#),
with(OCT, r#""kid":"1""#),
with(X25519, r#""kid":"2""#),
RSA_2048.to_owned(),
]);
let selectors = [
JwkSelector::new(),
JwkSelector::new().kid("1"),
JwkSelector::new().kid("2"),
JwkSelector::new().kid("3"),
by_alg("ECDH-ES"),
by_alg("ECDH-ES").kid("1"),
by_alg("RSA-OAEP-256"),
by_alg("RS256"),
JwkSelector::new().key_use(KeyUse::Enc).unwrap(),
];
for selector in &selectors {
let all: Vec<usize> = set.matching(selector).map(|(i, _)| i).collect();
let expected = match all.as_slice() {
[] => Err(Error::NoMatchingKey),
[one] => Ok(*one),
_ => Err(Error::MultipleMatchingKeys),
};
assert_eq!(selected(&set, selector), expected, "{selector:?}");
}
}
#[test]
fn without_alg_any_family_may_be_selected() {
let set = set_from(&[
with(OCT, r#""kid":"o""#),
with(EC_P256, r#""kid":"e""#),
with(RSA_2048, r#""kid":"r""#),
with(X25519, r#""kid":"x""#),
]);
for (index, kid) in ["o", "e", "r", "x"].into_iter().enumerate() {
assert_eq!(selected(&set, &JwkSelector::new().kid(kid)), Ok(index));
}
let enc = JwkSelector::new().key_use(KeyUse::Enc).unwrap();
assert_eq!(set.matching(&enc).count(), set.len());
}
#[test]
fn selection_never_authorizes_an_alg() {
const RFC7515_A1: &str = concat!(
"eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9",
".eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ",
".dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
);
let set = parse(A3).unwrap();
let (_, key) = set.select(&by_alg("HS256")).unwrap();
let only_hs384 = [SigningAlg::HS384];
assert_eq!(
verify_compact(RFC7515_A1, key, &VerifyOptions::new(&only_hs384).unwrap())
.map(drop),
Err(Error::AlgorithmNotAllowed)
);
let hs256 = [SigningAlg::HS256];
assert!(verify_compact(RFC7515_A1, key, &VerifyOptions::new(&hs256).unwrap()).is_ok());
}
#[test]
fn the_selected_key_keeps_its_private_members() {
let ec_private = r#"{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM","d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE","kid":"ec"}"#;
let set = set_from(&[ec_private.to_owned(), with(OCT, r#""kid":"oct""#)]);
let (_, ec) = set.select(&JwkSelector::new().kid("ec")).unwrap();
assert!(!ec.is_public());
let KeyMaterial::Ec(material) = ec.material() else {
unreachable!()
};
assert!(material.is_private());
assert!(ec.to_public().unwrap().is_public());
let (_, oct) = set.select(&JwkSelector::new().kid("oct")).unwrap();
assert!(!oct.is_public());
assert!(oct.to_public().is_none());
}
}
mod equivalence {
use proptest::prelude::*;
use super::*;
use crate::jwk::{JwkSelector, KeyOp};
fn reference_verification_candidate(
key: &Jwk,
alg: &SigningAlg,
kid: Option<&str>,
) -> bool {
key.material().serves_signing_alg(alg)
&& key
.alg()
.is_none_or(|key_alg| matches!(key_alg, KeyAlg::Signing(a) if a == alg))
&& key.permits(&KeyOp::Verify)
&& kid.is_none_or(|kid| key.kid() == Some(kid))
}
fn reference<'a>(
set: &'a JwkSet,
alg: &SigningAlg,
kid: Option<&str>,
) -> Vec<(usize, &'a Jwk)> {
set.keys()
.iter()
.enumerate()
.filter(|(_, key)| reference_verification_candidate(key, alg, kid))
.collect()
}
fn same(a: &[(usize, &Jwk)], b: &[(usize, &Jwk)]) -> bool {
a.len() == b.len()
&& a.iter()
.zip(b)
.all(|((i, x), (j, y))| i == j && core::ptr::eq(*x, *y))
}
fn materials() -> Vec<(String, &'static [Option<&'static str>])> {
const UNREGISTERED: Option<&str> = Some("x-unregistered");
vec![
(
OCT.to_owned(),
&[
None,
None,
Some("HS256"),
Some("HS384"),
Some("A128KW"),
Some("dir"),
Some("A256GCM"),
UNREGISTERED,
Some("ES256"),
],
),
(
RSA_2048.to_owned(),
&[
None,
None,
Some("RS256"),
Some("PS256"),
Some("RSA-OAEP-256"),
UNREGISTERED,
Some("HS256"),
],
),
(
EC_P256.to_owned(),
&[
None,
None,
Some("ES256"),
Some("ECDH-ES"),
UNREGISTERED,
Some("ES384"),
],
),
(
with(
EC_P256,
r#""d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE""#,
),
&[None, Some("ES256"), Some("ECDH-ES+A128KW")],
),
(
ec("P-384", 48).to_json().to_string(),
&[None, Some("ES384"), Some("ES256")],
),
(
ec("P-521", 66).to_json().to_string(),
&[None, Some("ES512")],
),
(
ec("secp256k1", 32).to_json().to_string(),
&[None, Some("ES256K"), Some("ECDH-ES")],
),
(
ED25519.to_owned(),
&[None, None, Some("EdDSA"), Some("Ed25519"), UNREGISTERED],
),
(X25519.to_owned(), &[None, Some("ECDH-ES"), Some("EdDSA")]),
]
}
const USES: &[Option<&str>] = &[
None,
None,
None,
None,
Some("sig"),
Some("enc"),
Some("x-other"),
];
const KEY_OPS: &[Option<&str>] = &[
None,
None,
None,
None,
Some("[]"),
Some(r#"["verify"]"#),
Some(r#"["sign"]"#),
Some(r#"["sign","verify"]"#),
Some(r#"["encrypt"]"#),
Some(r#"["wrapKey","unwrapKey"]"#),
Some(r#"["deriveKey"]"#),
Some(r#"["verify","x-op"]"#),
];
const MEMBER_KIDS: &[Option<&str>] = &[None, Some("a"), Some("b"), Some("A")];
const ABSENT_KID: &str = "absent-from-every-set";
type Pick = (
prop::sample::Index,
prop::sample::Index,
prop::sample::Index,
prop::sample::Index,
prop::sample::Index,
);
fn member(materials: &[(String, &[Option<&str>])], pick: &Pick) -> String {
let (material, alg, key_use, key_ops, kid) = pick;
let (material, algs) = &materials[material.index(materials.len())];
let mut extra = Vec::new();
if let Some(alg) = algs[alg.index(algs.len())] {
extra.push(format!(r#""alg":"{alg}""#));
}
if let Some(key_use) = USES[key_use.index(USES.len())] {
extra.push(format!(r#""use":"{key_use}""#));
}
if let Some(key_ops) = KEY_OPS[key_ops.index(KEY_OPS.len())] {
extra.push(format!(r#""key_ops":{key_ops}"#));
}
if let Some(kid) = MEMBER_KIDS[kid.index(MEMBER_KIDS.len())] {
extra.push(format!(r#""kid":"{kid}""#));
}
if extra.is_empty() {
material.clone()
} else {
with(material, &extra.join(","))
}
}
fn any_alg() -> impl Strategy<Value = SigningAlg> {
let mut algs: Vec<SigningAlg> = SigningAlg::KNOWN.to_vec();
algs.push(SigningAlg::from("HS1"));
algs.push(SigningAlg::from("A128KW"));
prop::sample::select(algs)
}
#[derive(Clone, Debug)]
enum HeaderAlg {
Any(SigningAlg),
ServedBy(prop::sample::Index, prop::sample::Index),
}
fn header_alg() -> impl Strategy<Value = HeaderAlg> {
prop_oneof![
1 => any_alg().prop_map(HeaderAlg::Any),
3 => any::<(prop::sample::Index, prop::sample::Index)>()
.prop_map(|(key, alg)| HeaderAlg::ServedBy(key, alg)),
]
}
impl HeaderAlg {
fn resolve(&self, set: &JwkSet) -> SigningAlg {
match self {
HeaderAlg::Any(alg) => alg.clone(),
HeaderAlg::ServedBy(key, alg) => {
let served: Vec<&SigningAlg> = if set.is_empty() {
Vec::new()
} else {
let key = &set.keys()[key.index(set.len())];
SigningAlg::KNOWN
.iter()
.filter(|a| key.material().serves_signing_alg(a))
.collect()
};
match served.as_slice() {
[] => SigningAlg::ES256,
served => served[alg.index(served.len())].clone(),
}
}
}
}
}
#[derive(Clone, Debug)]
enum HeaderKid {
None,
Present(prop::sample::Index),
Absent,
}
fn header_kid() -> impl Strategy<Value = HeaderKid> {
prop_oneof![
3 => Just(HeaderKid::None),
2 => any::<prop::sample::Index>().prop_map(HeaderKid::Present),
1 => Just(HeaderKid::Absent),
]
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]
#[test]
fn verification_candidates_equals_the_reference_filter(
picks in prop::collection::vec(any::<Pick>(), 0..=8),
alg in header_alg(),
kid in header_kid(),
) {
let materials = materials();
let members: Vec<String> = picks.iter().map(|p| member(&materials, p)).collect();
let set = set_from(&members);
let alg = alg.resolve(&set);
let set_kids: Vec<&str> = set.keys().iter().filter_map(Jwk::kid).collect();
let kid: Option<&str> = match &kid {
HeaderKid::None => None,
HeaderKid::Present(i) if !set_kids.is_empty() => Some(set_kids[i.index(set_kids.len())]),
HeaderKid::Present(_) | HeaderKid::Absent => Some(ABSENT_KID),
};
let expected = reference(&set, &alg, kid);
let actual: Vec<(usize, &Jwk)> = set.verification_candidates(&alg, kid).collect();
prop_assert!(same(&actual, &expected), "{alg} {kid:?}: {actual:?} vs {expected:?}");
match JwkSelector::new().alg(KeyAlg::Signing(alg.clone())) {
Ok(selector) => {
let mut selector = selector.key_op(KeyOp::Verify).unwrap();
if let Some(kid) = kid {
selector = selector.kid(kid);
}
let matched: Vec<(usize, &Jwk)> = set.matching(&selector).collect();
prop_assert!(same(&matched, &expected), "{selector:?}: {matched:?} vs {expected:?}");
}
Err(e) => {
prop_assert_eq!(e, Error::UnsupportedAlgorithm);
prop_assert!(alg == SigningAlg::None || !alg.is_known(), "{alg}");
prop_assert!(expected.is_empty());
}
}
}
}
}
}
#[cfg(test)]
mod selector_tests {
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use crate::Error;
use crate::jwa::{ContentEncAlg, KeyMgmtAlg, SigningAlg};
use crate::jwk::{JwkSelector, KeyAlg, KeyOp, KeyUse};
#[test]
fn new_is_the_empty_default_selector() {
assert_eq!(JwkSelector::new(), JwkSelector::default());
assert_eq!(
format!("{:?}", JwkSelector::new()),
"JwkSelector { kid: None, alg: None, key_use: None, key_op: None }"
);
}
#[test]
fn alg_accepts_every_implemented_jws_alg_but_none() {
for alg in SigningAlg::KNOWN {
let result = JwkSelector::new().alg(KeyAlg::Signing(alg.clone()));
if *alg == SigningAlg::None {
assert_eq!(result, Err(Error::UnsupportedAlgorithm));
} else {
assert!(result.is_ok(), "{alg}");
}
}
}
#[test]
fn alg_accepts_every_implemented_jwe_key_management_alg() {
for alg in KeyMgmtAlg::KNOWN {
assert!(
JwkSelector::new().alg(KeyAlg::KeyMgmt(alg.clone())).is_ok(),
"{alg}"
);
}
}
#[test]
fn alg_rejects_none_unimplemented_unknown_and_enc_names() {
let mut rejected: Vec<KeyAlg> = ["none", "RSA1_5", "RSA-OAEP", "Ed448", "XYZ", "", "hs256"]
.into_iter()
.map(KeyAlg::from)
.collect();
rejected.extend(ContentEncAlg::KNOWN.iter().cloned().map(KeyAlg::ContentEnc));
rejected.push(KeyAlg::Signing(SigningAlg::Unknown("HS256".into())));
rejected.push(KeyAlg::KeyMgmt(KeyMgmtAlg::Unknown("A128KW".into())));
rejected.push(KeyAlg::ContentEnc(ContentEncAlg::Unknown("A128GCM".into())));
rejected.push(KeyAlg::Unknown("ES256".into()));
for alg in rejected {
assert_eq!(
JwkSelector::new().alg(alg.clone()),
Err(Error::UnsupportedAlgorithm),
"{alg:?}"
);
}
}
#[test]
fn key_use_accepts_sig_and_enc_only() {
assert!(JwkSelector::new().key_use(KeyUse::Sig).is_ok());
assert!(JwkSelector::new().key_use(KeyUse::Enc).is_ok());
for other in ["", "SIG", "signature", "x"] {
assert_eq!(
JwkSelector::new().key_use(KeyUse::from(other)),
Err(Error::UnsupportedAlgorithm),
"{other:?}"
);
}
}
#[test]
fn key_op_accepts_the_registered_ops_only() {
for op in [
KeyOp::Sign,
KeyOp::Verify,
KeyOp::Encrypt,
KeyOp::Decrypt,
KeyOp::WrapKey,
KeyOp::UnwrapKey,
KeyOp::DeriveKey,
KeyOp::DeriveBits,
] {
assert!(JwkSelector::new().key_op(op.clone()).is_ok(), "{op}");
}
for other in ["", "Sign", "VERIFY", "wrapkey", "x"] {
assert_eq!(
JwkSelector::new().key_op(KeyOp::from(other)),
Err(Error::UnsupportedAlgorithm),
"{other:?}"
);
}
}
#[test]
fn setters_record_their_criteria_and_compare_by_value() {
let selector = JwkSelector::new()
.kid("2011-04-29")
.alg(KeyAlg::from("ES256"))
.unwrap()
.key_use(KeyUse::Sig)
.unwrap()
.key_op(KeyOp::Verify)
.unwrap();
assert_eq!(
format!("{selector:?}"),
"JwkSelector { kid: Some(\"2011-04-29\"), alg: Some(Signing(ES256)), \
key_use: Some(Sig), key_op: Some(Verify) }"
);
let reordered = JwkSelector::new()
.key_op(KeyOp::Verify)
.unwrap()
.key_use(KeyUse::Sig)
.unwrap()
.alg(KeyAlg::from("HS256"))
.unwrap()
.alg(KeyAlg::from("ES256"))
.unwrap()
.kid("other")
.kid("2011-04-29");
assert_eq!(reordered, selector);
assert_eq!(selector.clone(), selector);
assert_ne!(JwkSelector::new().kid("a"), JwkSelector::new().kid("A"));
}
}