use bh_jws_utils::{base64_url_encode, JwkPublic, SignatureVerifier, SigningAlgorithm};
use bherror::traits::PropagateError;
use rand_core::CryptoRngCore;
use crate::{
error::{FormatError, SignatureError},
key_binding::{KBError, KeyBindingChallenge},
sd_jwt::SdJwtKB,
traits::IssuerPublicKeyLookup,
DecodingError, Hasher, HashingAlgorithm, IssuerJwt, SecondsSinceEpoch,
};
pub struct Verifier {
challenge: KeyBindingChallenge,
}
#[derive(strum_macros::Display, Debug, PartialEq)]
pub enum VerifierError {
#[strum(to_string = "Nonce generation failed")]
NonceGenerationFailed,
#[strum(to_string = "{0}")]
KeyBinding(KBError),
#[strum(to_string = "Format error: {0}")]
Format(FormatError),
#[strum(to_string = "Signature error: {0}")]
Signature(SignatureError),
#[strum(to_string = "Decoding error: {0}")]
Decoding(DecodingError),
#[strum(to_string = "Jwt not yet valid: current time is {0}, nbf is {1}")]
JwtNotYetValid(u64, u64),
#[strum(to_string = "Jwt expired, current time is {0}, expiration is {1}")]
JwtExpired(u64, u64),
}
impl bherror::BhError for VerifierError {}
pub type Result<T> = bherror::Result<T, VerifierError>;
impl Verifier {
pub fn new<R: CryptoRngCore + ?Sized>(aud: String, nonce_rng: &mut R) -> Result<Self> {
let nonce = generate_nonce(nonce_rng)?;
Ok(Self::from_challenge(KeyBindingChallenge { aud, nonce }))
}
pub fn from_challenge(challenge: KeyBindingChallenge) -> Self {
Self { challenge }
}
pub fn key_binding_challenge(&self) -> &KeyBindingChallenge {
&self.challenge
}
pub async fn verify<'a>(
self,
sd_jwt_kb: SdJwtKB,
issuer_public_key_lookup: &impl IssuerPublicKeyLookup,
current_time: SecondsSinceEpoch,
get_hasher: impl Fn(HashingAlgorithm) -> Option<Box<dyn Hasher>>,
get_signature_verifier: impl Fn(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
) -> Result<(IssuerJwt, SigningAlgorithm, JwkPublic)> {
let (verified_sd_jwt, signing_algorithm, issuer_public_key) = sd_jwt_kb
.sd_jwt
.to_signature_verified_sd_jwt(issuer_public_key_lookup, &get_signature_verifier)
.await
.match_err(|crate_error| crate_error.to_verifier_error())?;
let decoded_sd_jwt = verified_sd_jwt
.into_decoded(get_hasher)
.match_err(|crate_error| crate_error.to_verifier_error())?;
sd_jwt_kb.verify_key_binding_jwt(
decoded_sd_jwt.hasher(),
decoded_sd_jwt.key_binding_public_key(),
&self.challenge,
current_time,
get_signature_verifier,
)?;
let claims = decoded_sd_jwt.into_claims();
claims.validate_claims_verifier(current_time)?;
Ok((claims, signing_algorithm, issuer_public_key))
}
}
pub fn generate_nonce<R: CryptoRngCore + ?Sized>(nonce_rng: &mut R) -> Result<String> {
let mut nonce_bytes = [0u8; 32];
nonce_rng
.try_fill_bytes(&mut nonce_bytes)
.map_err(|err| bherror::Error::root(VerifierError::NonceGenerationFailed).ctx(err))?;
Ok(base64_url_encode(nonce_bytes))
}
#[cfg(test)]
mod tests {
use rand::thread_rng;
use super::*;
use crate::{
holder::tests::test_holder, key_binding::KB_JWT_EXPIRATION_OFFSET,
test_utils::dummy_key_binding_audience, SHA_256_ALG_NAME,
};
fn test_verifier() -> Verifier {
Verifier::new(dummy_key_binding_audience(), &mut thread_rng()).unwrap()
}
use serde_json::json;
use crate::{
into_object,
issuer::tests::{dummy_claims, dummy_https_iss, test_issuer_jwt},
test_utils::{
dummy_hasher_factory, dummy_public_key_lookup, header_public_key_lookup,
symbolic_crypto::{dummy_public_jwk, StubSigner, StubVerifier},
},
};
#[tokio::test]
async fn invalid_presentation_missing_signature_verifier() {
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
let presentation = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
let invalid_verify = verifier
.verify(
presentation,
&dummy_public_key_lookup(),
iat,
dummy_hasher_factory,
|_| None, )
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::Signature(SignatureError::MissingSignatureVerifier(
SigningAlgorithm::Es256
))
);
}
#[tokio::test]
async fn invalid_presentation_mismatched_algorithm() {
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
let presentation = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
let public_jwk_wrong = into_object(json!({
"kid": "test key id",
"alg": "ES512" }));
let signature_verifier = StubVerifier::new(public_jwk_wrong); let invalid_verify = verifier
.verify(
presentation,
&dummy_public_key_lookup(),
iat,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::Signature(SignatureError::InvalidJwtSignature)
);
}
#[tokio::test]
async fn invalid_presentation_invalid_signature() {
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
let mut sd_jwk_kb = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
let last_ch_signature = sd_jwk_kb.sd_jwt.jwt.pop().unwrap();
let wrong_ch = if last_ch_signature == '0' { '1' } else { '0' };
sd_jwk_kb.sd_jwt.jwt.push(wrong_ch);
let signature_verifier = StubVerifier::default();
let invalid_verify = verifier
.verify(
sd_jwk_kb,
&dummy_public_key_lookup(),
iat,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::Signature(SignatureError::InvalidJwtSignature)
);
}
#[tokio::test]
async fn invalid_presentation_missing_hasher() {
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
let presentation = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
let signature_verifier = StubVerifier::default();
let invalid_verify = verifier
.verify(
presentation,
&dummy_public_key_lookup(),
iat,
|_| None, |_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::Decoding(DecodingError::MissingHasher(SHA_256_ALG_NAME.to_string()))
);
}
#[tokio::test]
async fn key_binding_invalid_kbjwt_signature() {
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let mut sd_jwt_kb = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
sd_jwt_kb.key_binding_jwt.pop();
sd_jwt_kb.key_binding_jwt.push('1');
let signature_verifier = StubVerifier::default();
let invalid_verify = verifier
.verify(
sd_jwt_kb,
&header_public_key_lookup(),
iat,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::KeyBinding(KBError::InvalidKBJwtSignature)
);
}
#[tokio::test]
async fn key_binding_expired() {
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let presentation = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
let signature_verifier = StubVerifier::default();
let current_time = iat + 5 * 60 + 10; let invalid_verify = verifier
.verify(
presentation,
&dummy_public_key_lookup(),
current_time,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::KeyBinding(KBError::KBJwtExpired(
iat,
KB_JWT_EXPIRATION_OFFSET,
current_time
))
);
}
#[tokio::test]
async fn key_binding_invalid_kbjwt_nonce() {
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
let verifier = test_verifier();
let mut challenge = verifier.key_binding_challenge().clone();
challenge.nonce.pop();
challenge.nonce.push('1');
let presentation_challenge_nonce = challenge.nonce.clone();
let presentation = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
let signature_verifier = StubVerifier::default();
let invalid_verify = verifier
.verify(
presentation,
&header_public_key_lookup(),
iat,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::KeyBinding(KBError::InvalidKBJwtNonce(presentation_challenge_nonce))
);
}
#[tokio::test]
async fn key_binding_invalid_kbjwt_aud() {
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
let verifier = test_verifier();
let mut challenge = verifier.key_binding_challenge().clone();
let original_aud = challenge.aud.clone();
challenge.aud.pop();
challenge.aud.push('1');
let presentation_challenge_aud = challenge.aud.clone();
let presentation = holder
.present(&[], challenge, iat, &StubSigner::default())
.unwrap();
let signature_verifier = StubVerifier::default();
let invalid_verify = verifier
.verify(
presentation,
&header_public_key_lookup(),
iat,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::KeyBinding(KBError::InvalidKBJwtAud(
presentation_challenge_aud,
original_aud,
))
);
}
#[tokio::test]
async fn nbf_in_future() {
let mut issuer_jwt = IssuerJwt::new(
"TestCredential".into(),
dummy_https_iss(),
dummy_public_jwk(),
dummy_claims(),
)
.unwrap();
let iat = 100;
let nbf = iat + 50;
issuer_jwt.nbf = Some(nbf);
let holder = test_holder(issuer_jwt, StubVerifier::default(), iat).await;
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let presentation = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
let signature_verifier = StubVerifier::default();
let invalid_verify = verifier
.verify(
presentation,
&header_public_key_lookup(),
iat,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::JwtNotYetValid(iat, nbf)
);
}
#[tokio::test]
async fn presentation_expired() {
let mut issuer_jwt = IssuerJwt::new(
"TestCredential".into(),
dummy_https_iss(),
dummy_public_jwk(),
dummy_claims(),
)
.unwrap();
let iat = 100;
let expiration_time = iat + 15;
let verify_time = iat + 20;
issuer_jwt.exp = Some(expiration_time);
let holder = test_holder(issuer_jwt, StubVerifier::default(), iat).await;
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let presentation = holder
.present(&[], challenge.clone(), iat, &StubSigner::default())
.unwrap();
let signature_verifier = StubVerifier::default();
let invalid_verify = verifier
.verify(
presentation,
&header_public_key_lookup(),
verify_time,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await;
assert_eq!(
invalid_verify.unwrap_err().error,
VerifierError::JwtExpired(verify_time, expiration_time)
);
}
mod integration {
use JsonNodePathSegment::*;
use super::*;
use crate::{
issuer::tests::{test_issuer_jwt, TEST_DISCLOSURE_PATHS as TEST_PATHS},
paths_exist,
test_utils::{
dummy_hasher_factory, dummy_public_key_lookup,
symbolic_crypto::{StubSigner, StubVerifier},
},
JsonNodePath, JsonNodePathSegment,
};
#[tokio::test]
async fn holder_verifier_happy_path() {
let iat = 100;
let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
struct TestCase<'a> {
requested_claims: &'a [&'a JsonNodePath<'a>],
not_to_be_disclosed_claims: &'a [&'a JsonNodePath<'a>],
implied_paths: &'a [&'a JsonNodePath<'a>],
}
let test_cases = &[
TestCase {
requested_claims: &[],
not_to_be_disclosed_claims: TEST_PATHS,
implied_paths: &[
&["baz".into()],
],
},
TestCase {
requested_claims: TEST_PATHS,
not_to_be_disclosed_claims: &[],
implied_paths: &[
&["baz".into()],
],
},
TestCase {
requested_claims: &[&[Key("foo")]],
not_to_be_disclosed_claims: &[
&[Key("parent")],
],
implied_paths: &[
&["baz".into()],
],
},
TestCase {
requested_claims: &[&[Key("parent")]],
not_to_be_disclosed_claims: &[
&[Key("foo")],
&[Key("parent"), Key("child2"), Key("leaf")],
&[Key("parent"), Key("child2"), Key("foo")],
&[Key("parent"), Key("child3")],
],
implied_paths: &[
&["baz".into()],
],
},
TestCase {
requested_claims: &[&[Key("parent"), Key("child1"), Index(1)]],
not_to_be_disclosed_claims: &[
&[Key("foo")],
&[Key("parent"), Key("child2"), Key("leaf")],
&[Key("parent"), Key("child2"), Key("foo")],
&[Key("parent"), Key("child3")],
],
implied_paths: &[
&["baz".into()],
&[Key("parent")],
&["parent".into(), "child1".into(), 0.into()],
&["parent".into(), "child1".into(), 2.into()],
&["parent".into(), "child1".into(), 3.into()],
],
},
TestCase {
requested_claims: &[&[Key("parent"), Key("child2"), Key("leaf")]],
not_to_be_disclosed_claims: &[
&[Key("foo")],
&[Key("parent"), Key("child2"), Key("foo")],
&[Key("parent"), Key("child3")],
],
implied_paths: &[
&["baz".into()],
&[Key("parent")],
],
},
TestCase {
requested_claims: &[
&[Key("foo")],
&[Key("parent"), Key("child1"), Index(1)],
&[Key("parent"), Key("child2"), Key("foo")],
],
not_to_be_disclosed_claims: &[
&[Key("parent"), Key("child2"), Key("leaf")],
&[Key("parent"), Key("child3")],
],
implied_paths: &[
&["baz".into()],
&[Key("parent")],
&["parent".into(), "child1".into(), 0.into()],
&["parent".into(), "child1".into(), 2.into()],
&["parent".into(), "child1".into(), 3.into()],
],
},
TestCase {
requested_claims: &[
&["baz".into()],
],
not_to_be_disclosed_claims: TEST_PATHS,
implied_paths: &[],
},
TestCase {
requested_claims: &[
&[Key("parent"), Key("child1"), Index(1)],
&[Key("parent"), Key("child1"), Index(3), Key("nested")],
],
not_to_be_disclosed_claims: &[
&[Key("foo")],
&[Key("parent"), Key("child2"), Key("leaf")],
&[Key("parent"), Key("child3")],
],
implied_paths: &[
&["baz".into()],
&[Key("parent")],
&[Key("parent"), Key("child1"), Index(2)],
&[Key("parent"), Key("child1"), Index(3)],
],
},
];
for TestCase {
requested_claims,
not_to_be_disclosed_claims,
implied_paths,
} in test_cases
{
let verifier = test_verifier();
let challenge = verifier.key_binding_challenge();
let presentation = holder
.present(
requested_claims,
challenge.clone(),
iat,
&StubSigner::default(),
)
.unwrap();
let signature_verifier = StubVerifier::default();
let reconstructed = verifier
.verify(
presentation,
&dummy_public_key_lookup(),
iat,
dummy_hasher_factory,
|_| Some(&signature_verifier),
)
.await
.unwrap()
.0;
let reconstructed = reconstructed.to_object();
paths_exist(&reconstructed, requested_claims).expect("Requested path(s) missing");
paths_exist(&reconstructed, implied_paths)
.expect("Indirectly requested path(s) missing");
for not_to_be_disclosed_path in *not_to_be_disclosed_claims {
paths_exist(&reconstructed, &[not_to_be_disclosed_path]).expect_err(
"Some non-requested selectively disclosable paths \
(and not indirectly implied by the request) are present",
);
}
}
}
}
}