Skip to main content

bh_sd_jwt/
verifier.rs

1// Copyright (C) 2020-2026  The Blockhouse Technology Limited (TBTL).
2//
3// This program is free software: you can redistribute it and/or modify it
4// under the terms of the GNU Affero General Public License as published by
5// the Free Software Foundation, either version 3 of the License, or (at your
6// option) any later version.
7//
8// This program is distributed in the hope that it will be useful, but
9// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10// or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
11// License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! This module provides the [`Verifier`] type for verifying SD-JWT+KB presentations.
17
18use 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
30/// Verifier of SD-JWT+KB verifiable presentation.
31///
32/// This verifier requires Key Binding. Note that the decision whether to
33/// require Key Binding for a particular use case **MUST NOT** be based on
34/// whether a Key Binding JWT is provided by the Holder or not, according
35/// to [official documentation].
36///
37/// # Lifecycle
38///
39/// A fresh instance must be constructed for every presentation exchange
40/// session.  The instance should live for the entire session, as it contains
41/// the nonce value used for ensuring freshness of the presentation that needs
42/// to be both communicated to the [Holder](crate::holder::Holder) and used in
43/// verification of the [SdJwtKB].
44///
45/// NB: Does **NOT** implement [Clone] to prevent nonce reuse!
46///
47/// [official documentation]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-13#section-7.3-4.1
48pub struct Verifier {
49    challenge: KeyBindingChallenge,
50}
51
52/// Error type for errors related to the SD-JWT verifier.
53#[derive(strum_macros::Display, Debug, PartialEq)]
54pub enum VerifierError {
55    /// Error indicating that the nonce generation failed.
56    #[strum(to_string = "Nonce generation failed")]
57    NonceGenerationFailed,
58
59    /// Error with Key Binding JWT.
60    #[strum(to_string = "{0}")]
61    KeyBinding(KBError),
62
63    /// Error indicating that the provided SD-JWT format is invalid.
64    #[strum(to_string = "Format error: {0}")]
65    Format(FormatError),
66
67    /// Error indicating that the signature verification failed.
68    #[strum(to_string = "Signature error: {0}")]
69    Signature(SignatureError),
70
71    /// Error indicating that the decoding of the SD-JWT failed.
72    #[strum(to_string = "Decoding error: {0}")]
73    Decoding(DecodingError),
74
75    /// Error indicating that the JWT is not yet valid, i.e. the `nbf` (not
76    /// before) claim is set to a future time.
77    #[strum(to_string = "Jwt not yet valid: current time is {0}, nbf is {1}")]
78    JwtNotYetValid(u64, u64),
79
80    /// Error indicating that the JWT has expired, i.e. the `exp` (expiration)
81    /// claim is set to a time in the past.
82    #[strum(to_string = "Jwt expired, current time is {0}, expiration is {1}")]
83    JwtExpired(u64, u64),
84}
85
86impl bherror::BhError for VerifierError {}
87
88/// Result type used by the [`verifier`][crate::verifier] module.
89pub type Result<T> = bherror::Result<T, VerifierError>;
90
91impl Verifier {
92    /// Construct a verifier for a new presentation exchange session.
93    ///
94    /// # Key Binding
95    ///
96    /// This verifier will require Key Binding. The challenge parameters include
97    /// the `aud` parameter which represents the identifier of the verifier
98    /// entity for the purpose of proving key binding, and the nonce to be used
99    /// for replay prevention that will be sampled from the provided
100    /// `nonce_rng`.
101    ///
102    /// # Lifecycle
103    ///
104    /// The verifier instance needs to be persisted for the duration of the
105    /// presentation exchange session, as it holds the aforementioned
106    /// challenge-related parameters as its state, since they will be needed for
107    /// verification once the presentation arrives.
108    ///
109    /// Note that no implementation is provided for (de)serialization, cloning,
110    /// nor construction from an explicit value of the nonce, in order to
111    /// prevent accidental reuse of the nonce. Callers should carefully consider
112    /// how to store the verifier instance until presentation verification.
113    ///
114    /// # Errors
115    ///
116    /// This constructor will only fail if sampling of the nonce fails.
117    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    /// Constructs a [`Verifier`] for an existing presentation exchange session.
124    ///
125    /// The provided [`KeyBindingChallenge`] is under complete control of the
126    /// caller, which might be a security risk. If this is not desired, take a
127    /// look at [`Verifier::new`] associated function.
128    ///
129    /// # Note
130    /// The caller of this function needs to ensure that the `nonce` value
131    /// provided within the [`KeyBindingChallenge`] **WILL NOT** be reused.
132    pub fn from_challenge(challenge: KeyBindingChallenge) -> Self {
133        Self { challenge }
134    }
135
136    /// Return the challenge to be sent to the holder. The purpose of the
137    /// challenge is to ensure the freshness of the key binding signature, as
138    /// well as the proper audience, in order to prevent credential replay attacks.
139    pub fn key_binding_challenge(&self) -> &KeyBindingChallenge {
140        &self.challenge
141    }
142
143    /// Verify the provided SD-JWT+KB presentation, returning the reconstructed
144    /// payload, an algorithm used to sign the JWT, and the resolved public key
145    /// of the SD-JWT Issuer in the JWK format.
146    ///
147    /// # Key Binding
148    ///
149    /// This function will verify key binding, using the public JWK contained in
150    /// the issuer-signed JWT's `cnf` claim, and comparing the `aud` and `nonce`
151    /// claims in the KB JWT against the challenge values created on
152    /// construction.
153    ///
154    /// The validation of the `iat` claim in the KB JWT will be done against
155    /// `current_time`, accepting only values of `iat` within the previous 5 min
156    /// (this is currently chosen arbitrarily).
157    ///
158    /// # Lifecycle
159    ///
160    /// This method must take ownership of the [`Verifier`] to destroy the
161    /// nonce value used in this presentation exchange session, in order to prevent
162    /// accidental reuse.
163    ///
164    /// # Cryptography
165    ///
166    /// The caller needs to provide an implementation of a [`Hasher`] for every
167    /// algorithm they want to support, using the `get_hasher` argument. If the
168    /// received payload uses an algorithm whose [`Hasher`] the caller did not
169    /// provide, an error will be returned.
170    ///
171    /// # Arguments
172    /// - `sd_jwt_kb`: SD-JWT+KB presentation to verify,
173    ///
174    /// - `issuer_public_key_lookup`: an implementation of the interface
175    ///   capable of resolving the issuer's public key based on the `iss`
176    ///   claim of the `JWT` and its header (see [`IssuerPublicKeyLookup`]),
177    ///
178    /// - `get_hasher`: a function that returns an instance of a [`Hasher`]
179    ///   based on the provided [`HashingAlgorithm`], or `None` if it is not
180    ///   supported,
181    ///
182    /// - `get_signature_verifier`: a function that returns an implementation
183    ///   of a [`SignatureVerifier`] based on the provided [`SigningAlgorithm`],
184    ///   or `None` if it is not supported.
185    ///
186    /// # Notes
187    /// - The caller needs to support at least `SHA-256` hashing algorithm, as
188    ///   specified [here].
189    ///
190    /// [here]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-selective-disclosure-jwt-07#section-5.1.1-3
191    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        // Validate the additional `SD-JWT-VC` claims
220        claims.validate_claims_verifier(current_time)?;
221
222        Ok((claims, signing_algorithm, issuer_public_key))
223    }
224}
225
226/// Generates a `nonce` value.
227///
228/// The `nonce` is generated as a random, `base64-url` encoded `String` with 256
229/// bits of entropy.
230///
231/// # Error
232/// If the `nonce` generation fails, [`VerifierError::NonceGenerationFailed`] is
233/// returned.
234pub 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, // this is the line causing InvalidPresentation
286            )
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" // this is set to ES512, the signature algorithm should be ES256
310        }));
311        let signature_verifier = StubVerifier::new(public_jwk_wrong); // uses different algorithm than signer
312        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        // signature is last part of jwt, change it to be invalid
339        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, // missing hasher
378                |_| 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; // should cause expiration of key binding because KB_JWT_EXPIRATION_OFFSET is 5 * 60
432        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'); // presentation uses different challenge than the verifier provided
461
462        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'); // presentation uses different challenge than the verifier provided
496
497        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); // set nbf (not before) to future
535
536        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                        // non-selectively disclosable claim in the root object
634                        &["baz".into()],
635                    ],
636                },
637                TestCase {
638                    requested_claims: TEST_PATHS,
639                    not_to_be_disclosed_claims: &[],
640                    implied_paths: &[
641                        // non-selectively disclosable claim in the root object
642                        &["baz".into()],
643                    ],
644                },
645                TestCase {
646                    requested_claims: &[&[Key("foo")]],
647                    not_to_be_disclosed_claims: &[
648                        // the only other disclosure at this level + ancestor of
649                        // all other disclosures
650                        &[Key("parent")],
651                    ],
652                    implied_paths: &[
653                        // non-selectively disclosable claim in the root object
654                        &["baz".into()],
655                    ],
656                },
657                TestCase {
658                    requested_claims: &[&[Key("parent")]],
659                    not_to_be_disclosed_claims: &[
660                        &[Key("foo")],
661                        // NB: difficult to test for absence of `$.parent.child1[1]`
662                        // form the original as the subsequent array entry from the
663                        // original will exist at that path in the reconstruction
664                        // &[Key("parent"), Key("child1"), Index(1)],
665                        &[Key("parent"), Key("child2"), Key("leaf")],
666                        &[Key("parent"), Key("child2"), Key("foo")],
667                        &[Key("parent"), Key("child3")],
668                    ],
669                    implied_paths: &[
670                        // non-selectively disclosable claim in the root object
671                        &["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                        // non-selectively disclosable claim in the root object
684                        &["baz".into()],
685                        // ancestor of &[Key("parent"), Key("child1"), Index(1)]
686                        &[Key("parent")],
687                        // non-selectively-disclosable siblings within the same
688                        // array as &[Key("parent"), Key("child1"), Index(1)]
689                        &["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                        // NB: difficult to test for absence of `$.parent.child1[1]`
699                        // form the original as the subsequent array entry from the
700                        // original will exist at that path in the reconstruction
701                        // &[Key("parent"), Key("child1"), Index(1)],
702                        &[Key("parent"), Key("child2"), Key("foo")],
703                        &[Key("parent"), Key("child3")],
704                    ],
705                    implied_paths: &[
706                        // non-selectively disclosable claim in the root object
707                        &["baz".into()],
708                        // ancestor of &[Key("parent"), Key("child2"), Key("leaf")]
709                        &[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                        // non-selectively disclosable claim in the root object
724                        &["baz".into()],
725                        // ancestor of &[Key("parent"), Key("child1"), Index(1)] and &[Key("parent"), Key("child2"), Key("foo")],
726                        &[Key("parent")],
727                        // non-selectively-disclosable siblings within the same
728                        // array as &[Key("parent"), Key("child1"), Index(1)]
729                        &["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                        // non-selectively disclosable claim in the root object
737                        &["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                        // non-selectively disclosable claim in the root object
754                        &["baz".into()],
755                        // ancestor of &[Key("parent"), Key("child1"), Index(1)] and &[Key("parent"), Key("child2"), Key("foo")],
756                        &[Key("parent")],
757                        // non-selectively-disclosable siblings within the same
758                        // array as &[Key("parent"), Key("child1"), Index(3)]
759                        &[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}