1use bh_jws_utils::{base64_url_encode, JwkPublic, SignatureVerifier, SigningAlgorithm};
19use bherror::traits::PropagateError;
20use rand_core::CryptoRngCore;
21
22use crate::{
23 error::{FormatError, SignatureError},
24 key_binding::{KBError, KeyBindingChallenge},
25 sd_jwt::SdJwtKB,
26 traits::IssuerPublicKeyLookup,
27 DecodingError, Hasher, HashingAlgorithm, IssuerJwt, SecondsSinceEpoch,
28};
29
30pub struct Verifier {
49 challenge: KeyBindingChallenge,
50}
51
52#[derive(strum_macros::Display, Debug, PartialEq)]
54pub enum VerifierError {
55 #[strum(to_string = "Nonce generation failed")]
57 NonceGenerationFailed,
58
59 #[strum(to_string = "{0}")]
61 KeyBinding(KBError),
62
63 #[strum(to_string = "Format error: {0}")]
65 Format(FormatError),
66
67 #[strum(to_string = "Signature error: {0}")]
69 Signature(SignatureError),
70
71 #[strum(to_string = "Decoding error: {0}")]
73 Decoding(DecodingError),
74
75 #[strum(to_string = "Jwt not yet valid: current time is {0}, nbf is {1}")]
78 JwtNotYetValid(u64, u64),
79
80 #[strum(to_string = "Jwt expired, current time is {0}, expiration is {1}")]
83 JwtExpired(u64, u64),
84}
85
86impl bherror::BhError for VerifierError {}
87
88pub type Result<T> = bherror::Result<T, VerifierError>;
90
91impl Verifier {
92 pub fn new<R: CryptoRngCore + ?Sized>(aud: String, nonce_rng: &mut R) -> Result<Self> {
118 let nonce = generate_nonce(nonce_rng)?;
119
120 Ok(Self::from_challenge(KeyBindingChallenge { aud, nonce }))
121 }
122
123 pub fn from_challenge(challenge: KeyBindingChallenge) -> Self {
133 Self { challenge }
134 }
135
136 pub fn key_binding_challenge(&self) -> &KeyBindingChallenge {
140 &self.challenge
141 }
142
143 pub async fn verify<'a>(
192 self,
193 sd_jwt_kb: SdJwtKB,
194 issuer_public_key_lookup: &impl IssuerPublicKeyLookup,
195 current_time: SecondsSinceEpoch,
196 get_hasher: impl Fn(HashingAlgorithm) -> Option<Box<dyn Hasher>>,
197 get_signature_verifier: impl Fn(SigningAlgorithm) -> Option<&'a dyn SignatureVerifier>,
198 ) -> Result<(IssuerJwt, SigningAlgorithm, JwkPublic)> {
199 let (verified_sd_jwt, signing_algorithm, issuer_public_key) = sd_jwt_kb
200 .sd_jwt
201 .to_signature_verified_sd_jwt(issuer_public_key_lookup, &get_signature_verifier)
202 .await
203 .match_err(|crate_error| crate_error.to_verifier_error())?;
204
205 let decoded_sd_jwt = verified_sd_jwt
206 .into_decoded(get_hasher)
207 .match_err(|crate_error| crate_error.to_verifier_error())?;
208
209 sd_jwt_kb.verify_key_binding_jwt(
210 decoded_sd_jwt.hasher(),
211 decoded_sd_jwt.key_binding_public_key(),
212 &self.challenge,
213 current_time,
214 get_signature_verifier,
215 )?;
216
217 let claims = decoded_sd_jwt.into_claims();
218
219 claims.validate_claims_verifier(current_time)?;
221
222 Ok((claims, signing_algorithm, issuer_public_key))
223 }
224}
225
226pub fn generate_nonce<R: CryptoRngCore + ?Sized>(nonce_rng: &mut R) -> Result<String> {
235 let mut nonce_bytes = [0u8; 32];
236 nonce_rng
237 .try_fill_bytes(&mut nonce_bytes)
238 .map_err(|err| bherror::Error::root(VerifierError::NonceGenerationFailed).ctx(err))?;
239 Ok(base64_url_encode(nonce_bytes))
240}
241
242#[cfg(test)]
243mod tests {
244
245 use rand::thread_rng;
246
247 use super::*;
248 use crate::{
249 holder::tests::test_holder, key_binding::KB_JWT_EXPIRATION_OFFSET,
250 test_utils::dummy_key_binding_audience, SHA_256_ALG_NAME,
251 };
252
253 fn test_verifier() -> Verifier {
254 Verifier::new(dummy_key_binding_audience(), &mut thread_rng()).unwrap()
255 }
256
257 use serde_json::json;
258
259 use crate::{
260 into_object,
261 issuer::tests::{dummy_claims, dummy_https_iss, test_issuer_jwt},
262 test_utils::{
263 dummy_hasher_factory, dummy_public_key_lookup, header_public_key_lookup,
264 symbolic_crypto::{dummy_public_jwk, StubSigner, StubVerifier},
265 },
266 };
267
268 #[tokio::test]
269 async fn invalid_presentation_missing_signature_verifier() {
270 let verifier = test_verifier();
271 let challenge = verifier.key_binding_challenge();
272 let iat = 100;
273
274 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
275 let presentation = holder
276 .present(&[], challenge.clone(), iat, &StubSigner::default())
277 .unwrap();
278
279 let invalid_verify = verifier
280 .verify(
281 presentation,
282 &dummy_public_key_lookup(),
283 iat,
284 dummy_hasher_factory,
285 |_| None, )
287 .await;
288 assert_eq!(
289 invalid_verify.unwrap_err().error,
290 VerifierError::Signature(SignatureError::MissingSignatureVerifier(
291 SigningAlgorithm::Es256
292 ))
293 );
294 }
295
296 #[tokio::test]
297 async fn invalid_presentation_mismatched_algorithm() {
298 let verifier = test_verifier();
299 let challenge = verifier.key_binding_challenge();
300 let iat = 100;
301
302 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
303 let presentation = holder
304 .present(&[], challenge.clone(), iat, &StubSigner::default())
305 .unwrap();
306
307 let public_jwk_wrong = into_object(json!({
308 "kid": "test key id",
309 "alg": "ES512" }));
311 let signature_verifier = StubVerifier::new(public_jwk_wrong); let invalid_verify = verifier
313 .verify(
314 presentation,
315 &dummy_public_key_lookup(),
316 iat,
317 dummy_hasher_factory,
318 |_| Some(&signature_verifier),
319 )
320 .await;
321 assert_eq!(
322 invalid_verify.unwrap_err().error,
323 VerifierError::Signature(SignatureError::InvalidJwtSignature)
324 );
325 }
326
327 #[tokio::test]
328 async fn invalid_presentation_invalid_signature() {
329 let verifier = test_verifier();
330 let challenge = verifier.key_binding_challenge();
331 let iat = 100;
332
333 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
334 let mut sd_jwk_kb = holder
335 .present(&[], challenge.clone(), iat, &StubSigner::default())
336 .unwrap();
337
338 let last_ch_signature = sd_jwk_kb.sd_jwt.jwt.pop().unwrap();
340 let wrong_ch = if last_ch_signature == '0' { '1' } else { '0' };
341
342 sd_jwk_kb.sd_jwt.jwt.push(wrong_ch);
343
344 let signature_verifier = StubVerifier::default();
345 let invalid_verify = verifier
346 .verify(
347 sd_jwk_kb,
348 &dummy_public_key_lookup(),
349 iat,
350 dummy_hasher_factory,
351 |_| Some(&signature_verifier),
352 )
353 .await;
354 assert_eq!(
355 invalid_verify.unwrap_err().error,
356 VerifierError::Signature(SignatureError::InvalidJwtSignature)
357 );
358 }
359
360 #[tokio::test]
361 async fn invalid_presentation_missing_hasher() {
362 let verifier = test_verifier();
363 let challenge = verifier.key_binding_challenge();
364 let iat = 100;
365
366 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
367 let presentation = holder
368 .present(&[], challenge.clone(), iat, &StubSigner::default())
369 .unwrap();
370
371 let signature_verifier = StubVerifier::default();
372 let invalid_verify = verifier
373 .verify(
374 presentation,
375 &dummy_public_key_lookup(),
376 iat,
377 |_| None, |_| Some(&signature_verifier),
379 )
380 .await;
381 assert_eq!(
382 invalid_verify.unwrap_err().error,
383 VerifierError::Decoding(DecodingError::MissingHasher(SHA_256_ALG_NAME.to_string()))
384 );
385 }
386
387 #[tokio::test]
388 async fn key_binding_invalid_kbjwt_signature() {
389 let iat = 100;
390 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
391
392 let verifier = test_verifier();
393 let challenge = verifier.key_binding_challenge();
394
395 let mut sd_jwt_kb = holder
396 .present(&[], challenge.clone(), iat, &StubSigner::default())
397 .unwrap();
398
399 sd_jwt_kb.key_binding_jwt.pop();
400 sd_jwt_kb.key_binding_jwt.push('1');
401
402 let signature_verifier = StubVerifier::default();
403 let invalid_verify = verifier
404 .verify(
405 sd_jwt_kb,
406 &header_public_key_lookup(),
407 iat,
408 dummy_hasher_factory,
409 |_| Some(&signature_verifier),
410 )
411 .await;
412 assert_eq!(
413 invalid_verify.unwrap_err().error,
414 VerifierError::KeyBinding(KBError::InvalidKBJwtSignature)
415 );
416 }
417
418 #[tokio::test]
419 async fn key_binding_expired() {
420 let iat = 100;
421 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
422
423 let verifier = test_verifier();
424 let challenge = verifier.key_binding_challenge();
425
426 let presentation = holder
427 .present(&[], challenge.clone(), iat, &StubSigner::default())
428 .unwrap();
429
430 let signature_verifier = StubVerifier::default();
431 let current_time = iat + 5 * 60 + 10; let invalid_verify = verifier
433 .verify(
434 presentation,
435 &dummy_public_key_lookup(),
436 current_time,
437 dummy_hasher_factory,
438 |_| Some(&signature_verifier),
439 )
440 .await;
441 assert_eq!(
442 invalid_verify.unwrap_err().error,
443 VerifierError::KeyBinding(KBError::KBJwtExpired(
444 iat,
445 KB_JWT_EXPIRATION_OFFSET,
446 current_time
447 ))
448 );
449 }
450
451 #[tokio::test]
452 async fn key_binding_invalid_kbjwt_nonce() {
453 let iat = 100;
454 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
455
456 let verifier = test_verifier();
457 let mut challenge = verifier.key_binding_challenge().clone();
458
459 challenge.nonce.pop();
460 challenge.nonce.push('1'); let presentation_challenge_nonce = challenge.nonce.clone();
463
464 let presentation = holder
465 .present(&[], challenge.clone(), iat, &StubSigner::default())
466 .unwrap();
467
468 let signature_verifier = StubVerifier::default();
469 let invalid_verify = verifier
470 .verify(
471 presentation,
472 &header_public_key_lookup(),
473 iat,
474 dummy_hasher_factory,
475 |_| Some(&signature_verifier),
476 )
477 .await;
478 assert_eq!(
479 invalid_verify.unwrap_err().error,
480 VerifierError::KeyBinding(KBError::InvalidKBJwtNonce(presentation_challenge_nonce))
481 );
482 }
483
484 #[tokio::test]
485 async fn key_binding_invalid_kbjwt_aud() {
486 let iat = 100;
487 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
488
489 let verifier = test_verifier();
490 let mut challenge = verifier.key_binding_challenge().clone();
491
492 let original_aud = challenge.aud.clone();
493
494 challenge.aud.pop();
495 challenge.aud.push('1'); let presentation_challenge_aud = challenge.aud.clone();
498
499 let presentation = holder
500 .present(&[], challenge, iat, &StubSigner::default())
501 .unwrap();
502
503 let signature_verifier = StubVerifier::default();
504 let invalid_verify = verifier
505 .verify(
506 presentation,
507 &header_public_key_lookup(),
508 iat,
509 dummy_hasher_factory,
510 |_| Some(&signature_verifier),
511 )
512 .await;
513 assert_eq!(
514 invalid_verify.unwrap_err().error,
515 VerifierError::KeyBinding(KBError::InvalidKBJwtAud(
516 presentation_challenge_aud,
517 original_aud,
518 ))
519 );
520 }
521
522 #[tokio::test]
523 async fn nbf_in_future() {
524 let mut issuer_jwt = IssuerJwt::new(
525 "TestCredential".into(),
526 dummy_https_iss(),
527 dummy_public_jwk(),
528 dummy_claims(),
529 )
530 .unwrap();
531
532 let iat = 100;
533 let nbf = iat + 50;
534 issuer_jwt.nbf = Some(nbf); let holder = test_holder(issuer_jwt, StubVerifier::default(), iat).await;
537
538 let verifier = test_verifier();
539 let challenge = verifier.key_binding_challenge();
540
541 let presentation = holder
542 .present(&[], challenge.clone(), iat, &StubSigner::default())
543 .unwrap();
544
545 let signature_verifier = StubVerifier::default();
546 let invalid_verify = verifier
547 .verify(
548 presentation,
549 &header_public_key_lookup(),
550 iat,
551 dummy_hasher_factory,
552 |_| Some(&signature_verifier),
553 )
554 .await;
555 assert_eq!(
556 invalid_verify.unwrap_err().error,
557 VerifierError::JwtNotYetValid(iat, nbf)
558 );
559 }
560
561 #[tokio::test]
562 async fn presentation_expired() {
563 let mut issuer_jwt = IssuerJwt::new(
564 "TestCredential".into(),
565 dummy_https_iss(),
566 dummy_public_jwk(),
567 dummy_claims(),
568 )
569 .unwrap();
570
571 let iat = 100;
572 let expiration_time = iat + 15;
573 let verify_time = iat + 20;
574
575 issuer_jwt.exp = Some(expiration_time);
576
577 let holder = test_holder(issuer_jwt, StubVerifier::default(), iat).await;
578
579 let verifier = test_verifier();
580 let challenge = verifier.key_binding_challenge();
581
582 let presentation = holder
583 .present(&[], challenge.clone(), iat, &StubSigner::default())
584 .unwrap();
585
586 let signature_verifier = StubVerifier::default();
587 let invalid_verify = verifier
588 .verify(
589 presentation,
590 &header_public_key_lookup(),
591 verify_time,
592 dummy_hasher_factory,
593 |_| Some(&signature_verifier),
594 )
595 .await;
596 assert_eq!(
597 invalid_verify.unwrap_err().error,
598 VerifierError::JwtExpired(verify_time, expiration_time)
599 );
600 }
601
602 mod integration {
603
604 use JsonNodePathSegment::*;
605
606 use super::*;
607 use crate::{
608 issuer::tests::{test_issuer_jwt, TEST_DISCLOSURE_PATHS as TEST_PATHS},
609 paths_exist,
610 test_utils::{
611 dummy_hasher_factory, dummy_public_key_lookup,
612 symbolic_crypto::{StubSigner, StubVerifier},
613 },
614 JsonNodePath, JsonNodePathSegment,
615 };
616
617 #[tokio::test]
618 async fn holder_verifier_happy_path() {
619 let iat = 100;
620 let holder = test_holder(test_issuer_jwt(), StubVerifier::default(), iat).await;
621
622 struct TestCase<'a> {
623 requested_claims: &'a [&'a JsonNodePath<'a>],
624 not_to_be_disclosed_claims: &'a [&'a JsonNodePath<'a>],
625 implied_paths: &'a [&'a JsonNodePath<'a>],
626 }
627
628 let test_cases = &[
629 TestCase {
630 requested_claims: &[],
631 not_to_be_disclosed_claims: TEST_PATHS,
632 implied_paths: &[
633 &["baz".into()],
635 ],
636 },
637 TestCase {
638 requested_claims: TEST_PATHS,
639 not_to_be_disclosed_claims: &[],
640 implied_paths: &[
641 &["baz".into()],
643 ],
644 },
645 TestCase {
646 requested_claims: &[&[Key("foo")]],
647 not_to_be_disclosed_claims: &[
648 &[Key("parent")],
651 ],
652 implied_paths: &[
653 &["baz".into()],
655 ],
656 },
657 TestCase {
658 requested_claims: &[&[Key("parent")]],
659 not_to_be_disclosed_claims: &[
660 &[Key("foo")],
661 &[Key("parent"), Key("child2"), Key("leaf")],
666 &[Key("parent"), Key("child2"), Key("foo")],
667 &[Key("parent"), Key("child3")],
668 ],
669 implied_paths: &[
670 &["baz".into()],
672 ],
673 },
674 TestCase {
675 requested_claims: &[&[Key("parent"), Key("child1"), Index(1)]],
676 not_to_be_disclosed_claims: &[
677 &[Key("foo")],
678 &[Key("parent"), Key("child2"), Key("leaf")],
679 &[Key("parent"), Key("child2"), Key("foo")],
680 &[Key("parent"), Key("child3")],
681 ],
682 implied_paths: &[
683 &["baz".into()],
685 &[Key("parent")],
687 &["parent".into(), "child1".into(), 0.into()],
690 &["parent".into(), "child1".into(), 2.into()],
691 &["parent".into(), "child1".into(), 3.into()],
692 ],
693 },
694 TestCase {
695 requested_claims: &[&[Key("parent"), Key("child2"), Key("leaf")]],
696 not_to_be_disclosed_claims: &[
697 &[Key("foo")],
698 &[Key("parent"), Key("child2"), Key("foo")],
703 &[Key("parent"), Key("child3")],
704 ],
705 implied_paths: &[
706 &["baz".into()],
708 &[Key("parent")],
710 ],
711 },
712 TestCase {
713 requested_claims: &[
714 &[Key("foo")],
715 &[Key("parent"), Key("child1"), Index(1)],
716 &[Key("parent"), Key("child2"), Key("foo")],
717 ],
718 not_to_be_disclosed_claims: &[
719 &[Key("parent"), Key("child2"), Key("leaf")],
720 &[Key("parent"), Key("child3")],
721 ],
722 implied_paths: &[
723 &["baz".into()],
725 &[Key("parent")],
727 &["parent".into(), "child1".into(), 0.into()],
730 &["parent".into(), "child1".into(), 2.into()],
731 &["parent".into(), "child1".into(), 3.into()],
732 ],
733 },
734 TestCase {
735 requested_claims: &[
736 &["baz".into()],
738 ],
739 not_to_be_disclosed_claims: TEST_PATHS,
740 implied_paths: &[],
741 },
742 TestCase {
743 requested_claims: &[
744 &[Key("parent"), Key("child1"), Index(1)],
745 &[Key("parent"), Key("child1"), Index(3), Key("nested")],
746 ],
747 not_to_be_disclosed_claims: &[
748 &[Key("foo")],
749 &[Key("parent"), Key("child2"), Key("leaf")],
750 &[Key("parent"), Key("child3")],
751 ],
752 implied_paths: &[
753 &["baz".into()],
755 &[Key("parent")],
757 &[Key("parent"), Key("child1"), Index(2)],
760 &[Key("parent"), Key("child1"), Index(3)],
761 ],
762 },
763 ];
764
765 for TestCase {
766 requested_claims,
767 not_to_be_disclosed_claims,
768 implied_paths,
769 } in test_cases
770 {
771 let verifier = test_verifier();
772 let challenge = verifier.key_binding_challenge();
773
774 let presentation = holder
775 .present(
776 requested_claims,
777 challenge.clone(),
778 iat,
779 &StubSigner::default(),
780 )
781 .unwrap();
782
783 let signature_verifier = StubVerifier::default();
784 let reconstructed = verifier
785 .verify(
786 presentation,
787 &dummy_public_key_lookup(),
788 iat,
789 dummy_hasher_factory,
790 |_| Some(&signature_verifier),
791 )
792 .await
793 .unwrap()
794 .0;
795
796 let reconstructed = reconstructed.to_object();
797
798 paths_exist(&reconstructed, requested_claims).expect("Requested path(s) missing");
799 paths_exist(&reconstructed, implied_paths)
800 .expect("Indirectly requested path(s) missing");
801
802 for not_to_be_disclosed_path in *not_to_be_disclosed_claims {
803 paths_exist(&reconstructed, &[not_to_be_disclosed_path]).expect_err(
804 "Some non-requested selectively disclosable paths \
805 (and not indirectly implied by the request) are present",
806 );
807 }
808 }
809 }
810 }
811}