Skip to main content

bh_sd_jwt/
lookup.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//! Contains implementations for public key lookup strategies.
17//!
18//! * [`HttpsIssuerPublicKeyLookup`] for lookup via HTTPS.
19//! * [`X5ChainIssuerPublicKeyLookup`] for lookup from a X.509 certificate chain.
20//!
21//! <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-09#name-issuer-signed-jwt-verificat>
22
23use std::future::Future;
24
25use bh_jws_utils::public_jwk_from_x5chain_leaf;
26pub use bh_jws_utils::{JwkPublic, JwkSet};
27use bh_uri_utils::UriPathExtensions;
28use bherror::{
29    traits::{ForeignError, PropagateError},
30    BhError, Error,
31};
32use bhx5chain::X509Trust;
33use iref::{Uri, UriBuf};
34use reqwest::{Client, ClientBuilder, StatusCode};
35use serde::{Deserialize, Serialize};
36
37use crate::{IssuerJwtHeader, IssuerPublicKeyLookup, JsonObject};
38
39/// Models JWT Issuer Metadata, specified [here], contains:
40///
41/// - `issuer` : The Issuer identifier, which MUST be identical to the `iss` value in the JWT
42///
43/// exactly one of:
44/// - `jwks`   : Issuer's JSON Web Key Set which contains the Issuer's public keys.
45/// - `jwks_uri` : HTTPS URL to an endpoint serving the former.
46///
47/// and:
48/// - `params` : All other additional configuration parameters which MAY be used [2].
49///
50/// [here]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#name-jwt-vc-issuer-metadata
51/// [2]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-11
52#[derive(Serialize, Deserialize, Debug, PartialEq)]
53#[serde(try_from = "JwtVcIssuerMetadataUnverified")]
54pub struct JwtVcIssuerMetadata {
55    /// The Issuer Identifier (`iss`).
56    pub issuer: String,
57    /// Either `jwks` or `jwks_uri`.
58    #[serde(flatten)]
59    pub jwks_param: JwksParam,
60    /// Other optional configuration parameters.
61    #[serde(flatten)]
62    pub params: JsonObject,
63}
64
65/// Represents either the `jwks` or `jwks_uri`.
66#[derive(Debug, PartialEq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum JwksParam {
69    /// `jwks` variant.
70    Jwks(JwkSet),
71    /// `jwks_uri` variant.
72    JwksUri(UriBuf),
73}
74
75impl JwksParam {
76    async fn resolve(self, client: &impl HttpGetClient) -> Result<JwkSet, Error<LookupError>> {
77        match self {
78            JwksParam::Jwks(jwk_set) => Ok(jwk_set),
79            JwksParam::JwksUri(jwks_uri) => {
80                let jwk_set = client
81                    .get(&jwks_uri)
82                    .await
83                    .foreign_err(|| {
84                        LookupError(format!("failed to resolve `jwks_uri`: {}", jwks_uri))
85                    })?
86                    .json()
87                    .await
88                    .foreign_err(|| {
89                        LookupError("failed to parse response from `jwks_uri` as a JWKS".to_owned())
90                    })?;
91                Ok(jwk_set)
92            }
93        }
94    }
95
96    #[cfg(test)]
97    fn to_jwks(&self) -> &JwkSet {
98        if let Self::Jwks(jwk_set) = self {
99            jwk_set
100        } else {
101            panic!("`JwksParam` was `JwksUri`");
102        }
103    }
104
105    #[cfg(test)]
106    fn to_jwks_uri(&self) -> &Uri {
107        if let Self::JwksUri(jwks_uri) = self {
108            jwks_uri
109        } else {
110            panic!("`JwksParam` was `Jwks`");
111        }
112    }
113}
114
115/// This is a "shadow" type whose sole purpose of existence is to be able to verify validity of deserialized
116/// [JwtVcIssuerMetadata] without writing deserialization manually. This is achieved with misusage of
117/// `TryFrom` trait. For more info see this [github issue].
118///
119/// [github issue]: https://github.com/serde-rs/serde/issues/642
120#[derive(Deserialize, Debug)]
121struct JwtVcIssuerMetadataUnverified {
122    issuer: String,
123    #[serde(flatten)]
124    jwks_param: JwksParam,
125    #[serde(flatten)]
126    params: JsonObject,
127}
128
129// Rejects a deserialization when deserialized metadata would jwks_uri parameter
130impl TryFrom<JwtVcIssuerMetadataUnverified> for JwtVcIssuerMetadata {
131    type Error = &'static str;
132
133    fn try_from(value: JwtVcIssuerMetadataUnverified) -> Result<Self, Self::Error> {
134        // Due to how serde implements Deserialize, the other (2nd in the
135        // processing order) variant will (if present) actually end up in `params`
136        if value.params.contains_key("jwks_uri") || value.params.contains_key("jwks") {
137            return Err("`jwks` and `jwks_uri` are mutually exclusive");
138        }
139
140        Ok(JwtVcIssuerMetadata {
141            issuer: value.issuer,
142            jwks_param: value.jwks_param,
143            params: value.params,
144        })
145    }
146}
147
148/// Interface providing functionality of sending HTTP GET request.
149/// Motivation for introducing this abstraction is to allow an implementation of a more secure
150/// HTTP Client which could prevent malicious URL injections (e.g. by whitelisting hosts).
151///
152// See TODO(issues/53)
153pub trait HttpGetClient: Sync {
154    /// Error type used by this trait.
155    type Err: std::error::Error + Send + Sync + 'static;
156    /// Performs a HTTP GET request with provided `url`.
157    ///
158    /// Note: Return type of request is currently hardcoded. This is subject to change in the future.
159    // TODO(issues/53)
160    fn get(
161        &self,
162        url: &str,
163    ) -> impl Future<Output = std::result::Result<reqwest::Response, Self::Err>> + Send;
164}
165
166/// [`HttpGetClient`] implementation using the [`reqwest`] crate.
167pub struct ReqwestGetClient(Client);
168
169impl ReqwestGetClient {
170    /// Construct [`ReqwestGetClient`] from [`Client`].
171    pub fn new(client: Client) -> Self {
172        Self(client)
173    }
174    /// Construct [`ReqwestGetClient`] from [`ClientBuilder`].
175    pub fn from_builder(builder: ClientBuilder) -> reqwest::Result<Self> {
176        Ok(ReqwestGetClient(builder.build()?))
177    }
178}
179
180impl HttpGetClient for ReqwestGetClient {
181    type Err = reqwest::Error;
182
183    fn get(&self, url: &str) -> impl Future<Output = reqwest::Result<reqwest::Response>> {
184        self.0.get(url).send()
185    }
186}
187
188/// Implementation of Issuer Public Key lookup using HTTPS according to [spec]. Lookup requires
189/// a [HttpGetClient] that provides the functionality of sending HTTP GET request to retrieve the key.
190///
191/// [spec]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#name-jwt-vc-issuer-metadata
192pub struct HttpsIssuerPublicKeyLookup<C: HttpGetClient> {
193    // TODO(issues/53)
194    client: C,
195}
196
197impl<C: HttpGetClient> HttpsIssuerPublicKeyLookup<C> {
198    /// Construct [`HttpsIssuerPublicKeyLookup`] from a [`HttpGetClient`].
199    pub fn new(client: C) -> Self {
200        HttpsIssuerPublicKeyLookup { client }
201    }
202}
203
204/// <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5-2>
205pub const ISSUER_METADATA_URL_SUFFIX: &str = "/.well-known/jwt-vc-issuer";
206
207/// Error type for reporting issues during the lookup of the Issuer Public Key.
208#[derive(PartialEq, Debug)]
209pub struct LookupError(pub String);
210
211impl std::fmt::Display for LookupError {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        write!(f, "Lookup error: {}", self.0)
214    }
215}
216
217impl BhError for LookupError {}
218
219impl<C: HttpGetClient> IssuerPublicKeyLookup for HttpsIssuerPublicKeyLookup<C>
220where
221    <C as HttpGetClient>::Err: 'static, // because lookup receives &self
222{
223    type Err = LookupError;
224    /// Retrieves the Issuer Public Key from the provided `alleged_iss` and a `kid` value
225    /// as provided in the [IssuerJwtHeader] `header`.
226    ///
227    /// Arguments:
228    ///
229    /// - `alleged_iss` : `URI` which represents the Issuer of the Verifiable Credential.
230    ///   URL for HTTP GET request is made by inserting the `WELL_KNOWN` string between
231    ///   host and the path component of this value.
232    ///   Should not contain query or fragment components.
233    ///
234    /// - `header`      : JWT header which contains a `kid` parameter that is used to look
235    ///   up the public key in the [JwkSet] from the retrieved metadata.
236    ///
237    async fn lookup(
238        &self,
239        alleged_iss: &str,
240        header: &IssuerJwtHeader,
241    ) -> Result<JwkPublic, Error<Self::Err>> {
242        let Ok(alleged_iss) = alleged_iss.try_into() else {
243            return Err(Error::root(LookupError(format!(
244                "Invalid `iss` URL: {}",
245                alleged_iss
246            ))));
247        };
248
249        // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.1-1
250        check_valid_iss(alleged_iss)?;
251        let url = url_from_iss(alleged_iss)?;
252
253        let Some(header_kid) = &header.kid else {
254            return Err(Error::root(LookupError(
255                "JWT header `kid` field missing".to_owned(),
256            )));
257        };
258
259        let response = self
260            .client
261            .get(url.as_str())
262            .await
263            .foreign_err(|| LookupError("could not get issuer metadata".to_string()))?;
264
265        // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-1
266        check_successful_response(&response)?;
267
268        let metadata: JwtVcIssuerMetadata = response
269            .json()
270            .await
271            .foreign_err(|| LookupError("response content invalid format".to_string()))?;
272
273        // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.3-1
274        if metadata.issuer != alleged_iss.as_str() {
275            return Err(Error::root(LookupError(
276                "response issuer value was not identical to iss value of JWT".to_string(),
277            )));
278        }
279
280        let jwks = metadata.jwks_param.resolve(&self.client).await?;
281
282        // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-6
283        for jwk in jwks.keys {
284            if jwk
285                .get("kid")
286                .and_then(|value| value.as_str())
287                .is_some_and(|kid| kid == header_kid)
288            {
289                return Ok(jwk);
290            };
291        }
292        Err(Error::root(LookupError(
293            "JWK with kid from JWT header was not found".to_string(),
294        )))
295    }
296}
297
298// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-1
299fn check_successful_response(response: &reqwest::Response) -> Result<(), Error<LookupError>> {
300    if response.status() != StatusCode::OK {
301        return Err(Error::root(LookupError(format!(
302            "response status code was {}, expected 200 OK",
303            response.status()
304        ))));
305    }
306    let content_type = response
307        .headers()
308        .get("Content-type")
309        .ok_or_else(|| Error::root(LookupError("response content type was empty".to_string())))?
310        .to_str()
311        .foreign_err(|| {
312            LookupError("response content type is not able to be represented as string".to_string())
313        })?;
314
315    if content_type != "application/json" {
316        return Err(Error::root(LookupError(format!(
317            "response content type was {}, expected application/json",
318            content_type
319        ))));
320    }
321    Ok(())
322}
323
324/// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5-2
325fn check_valid_iss(alleged_iss: &Uri) -> Result<(), Error<LookupError>> {
326    if alleged_iss.scheme().as_str() != "https" {
327        return Err(Error::root(LookupError(
328            "iss uri scheme should be https".to_string(),
329        )));
330    }
331
332    if alleged_iss.query().is_some() || alleged_iss.fragment().is_some() {
333        return Err(Error::root(LookupError(
334            "iss uri should not contain query or fragment parts".to_string(),
335        )));
336    }
337
338    Ok(())
339}
340
341/// <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5-2>
342pub fn metadata_uri_from_iss(alleged_iss: &Uri) -> iref::uri::UriBuf {
343    alleged_iss
344        .add_path_prefix(ISSUER_METADATA_URL_SUFFIX)
345        .expect("`ISSUER_METADATA_URL_SUFFIX` must be valid")
346}
347
348/// <https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5-2>
349pub fn url_from_iss(alleged_iss: &Uri) -> Result<reqwest::Url, Error<LookupError>> {
350    let url = metadata_uri_from_iss(alleged_iss);
351
352    reqwest::Url::parse(&url)
353        .foreign_err(|| LookupError("error while parsing iss as url".to_string()))
354}
355
356/// The [`IssuerPublicKeyLookup`] implementation that retrieves the Issuer's
357/// public key from the X.509 certificate chain.
358///
359/// It can be configured with trusted root certificates, in which case the
360/// authenticity of the X.509 certificate chain will be verified against those
361/// certificates, returning an error if the verification fails.
362pub struct X5ChainIssuerPublicKeyLookup {
363    trust: Option<X509Trust>,
364}
365
366impl X5ChainIssuerPublicKeyLookup {
367    /// Create a new instance of the [`X5ChainIssuerPublicKeyLookup`], that will
368    /// **TRUST ALL** Issuers, i.e. the authenticity of the X.509 certificate
369    /// chain will not be verified.
370    pub fn trust_all() -> Self {
371        tracing::warn!("Issuer's authenticity will not be verified");
372
373        Self { trust: None }
374    }
375
376    /// Create a new instance of the [`X5ChainIssuerPublicKeyLookup`], that will
377    /// verify the authenticity of the X.509 certificate chain against the
378    /// provided trusted roots.
379    pub fn with_trust(trust: X509Trust) -> Self {
380        Self { trust: Some(trust) }
381    }
382}
383
384impl IssuerPublicKeyLookup for X5ChainIssuerPublicKeyLookup {
385    type Err = LookupError;
386
387    /// Retrieve and check the Issuer public key from the x5chain field in the JWT header.
388    async fn lookup(
389        &self,
390        _alleged_iss: &str,
391        header: &IssuerJwtHeader,
392    ) -> Result<JwkPublic, Error<Self::Err>> {
393        let Some(jwt_x5chain) = &header.x5c else {
394            return Err(Error::root(LookupError(
395                "missing 'x5c' jwt header".to_owned(),
396            )));
397        };
398
399        let x5chain: bhx5chain::X5Chain = jwt_x5chain
400            .clone()
401            .try_into()
402            .with_err(|| LookupError("failed to convert `JwtX5Chain` to `X5Chain`".to_owned()))?;
403
404        if let Some(trust) = &self.trust {
405            x5chain.verify_against_trusted_roots(trust).with_err(|| {
406                LookupError("x5chain does not verify against trusted roots".to_owned())
407            })?;
408        }
409
410        let public_jwk = public_jwk_from_x5chain_leaf(&x5chain, &header.alg, header.kid.as_deref())
411            .foreign_err(|| LookupError("failed to get jwk from x5chain leaf".to_owned()))?;
412
413        Ok(public_jwk)
414    }
415}
416
417#[cfg(test)]
418pub(crate) mod tests {
419    use std::sync::Mutex;
420
421    use bh_jws_utils::{jwt::claims::SecondsSinceEpoch, Es256Verifier, SigningAlgorithm};
422    use openssl::x509::X509;
423    use serde_json::{json, Value};
424
425    use super::*;
426    use crate::{holder::Holder, issuer::TYP_VC_SD_JWT, HashingAlgorithm, Sha256};
427
428    const HOLDER_ACCEPT_TIME: SecondsSinceEpoch = 1783000000;
429
430    struct StubClient {
431        expected_url: Option<String>,
432        response: http::Response<String>,
433    }
434
435    impl HttpGetClient for StubClient {
436        type Err = reqwest::Error;
437
438        async fn get(&self, url: &str) -> reqwest::Result<reqwest::Response> {
439            if self.expected_url.is_none() || url != self.expected_url.as_ref().unwrap() {
440                panic!("Unexpected url: {}", url);
441            }
442            Ok(reqwest::Response::from(self.response.clone()))
443        }
444    }
445
446    struct StubClient2Step {
447        // The trait bounds + signature require this mutex
448        first: Mutex<Option<StubClient>>,
449        second: StubClient,
450    }
451
452    impl HttpGetClient for StubClient2Step {
453        type Err = reqwest::Error;
454
455        async fn get(&self, url: &str) -> reqwest::Result<reqwest::Response> {
456            let first = self.first.lock().unwrap().take();
457            if let Some(first) = first {
458                first.get(url).await
459            } else {
460                self.second.get(url).await
461            }
462        }
463    }
464
465    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-8
466    fn example_metadata() -> Value {
467        json!({
468           "issuer":"https://example.com",
469           "jwks":{
470              "keys":[
471                 {
472                    "kid":"doc-signer-05-25-2022",
473                    "e":"AQAB",
474                    "n":"nj3YJwsLUFl9BmpAbkOswCNVx17Eh9wMO-_AReZwBqfaWFcfG
475             HrZXsIV2VMCNVNU8Tpb4obUaSXcRcQ-VMsfQPJm9IzgtRdAY8NN8Xb7PEcYyk
476             lBjvTtuPbpzIaqyiUepzUXNDFuAOOkrIol3WmflPUUgMKULBN0EUd1fpOD70p
477             RM0rlp_gg_WNUKoW1V-3keYUJoXH9NztEDm_D2MQXj9eGOJJ8yPgGL8PAZMLe
478             2R7jb9TxOCPDED7tY_TU4nFPlxptw59A42mldEmViXsKQt60s1SLboazxFKve
479             qXC_jpLUt22OC6GUG63p-REw-ZOr3r845z50wMuzifQrMI9bQ",
480                    "kty":"RSA"
481                 }
482              ]
483           }
484        })
485    }
486
487    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-10
488    fn example_metadata_with_jwks_uri() -> Value {
489        json!({
490           "issuer": "https://example.com",
491           "jwks_uri": "https://jwt-vc-issuer.example.org/my_public_keys.jwks"
492        })
493    }
494
495    fn example_header() -> IssuerJwtHeader {
496        IssuerJwtHeader {
497            typ: TYP_VC_SD_JWT.to_string(),
498            alg: SigningAlgorithm::Es256,
499            kid: Some("doc-signer-05-25-2022".to_string()),
500            x5c: None,
501        }
502    }
503
504    fn example_header_with_x5c(jwt_x5chain: bhx5chain::JwtX5Chain) -> IssuerJwtHeader {
505        IssuerJwtHeader {
506            typ: TYP_VC_SD_JWT.to_string(),
507            alg: SigningAlgorithm::Es256,
508            kid: None,
509            x5c: Some(jwt_x5chain),
510        }
511    }
512
513    fn response_with_metadata(metadata: Value) -> http::Response<String> {
514        http::Response::builder()
515            .status(200)
516            .header("Content-type", "application/json")
517            .body(metadata.to_string())
518            .unwrap()
519    }
520
521    /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-7
522    #[tokio::test]
523    async fn public_key_example_from_specification() {
524        let alleged_iss = "https://example.com";
525
526        let client = StubClient {
527            expected_url: Some("https://example.com/.well-known/jwt-vc-issuer".to_string()),
528            response: response_with_metadata(example_metadata()),
529        };
530
531        let header = example_header();
532
533        let response = HttpsIssuerPublicKeyLookup { client }
534            .lookup(Uri::new(alleged_iss).unwrap(), &header)
535            .await
536            .unwrap();
537
538        assert_eq!(
539            response.get("kid").unwrap().as_str().unwrap(),
540            header.kid.unwrap()
541        );
542    }
543
544    /// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-10
545    #[tokio::test]
546    async fn public_key_example_with_jwks_uri_from_specification() {
547        let alleged_iss = "https://example.com";
548
549        let client = StubClient2Step {
550            first: Mutex::new(Some(StubClient {
551                expected_url: Some("https://example.com/.well-known/jwt-vc-issuer".to_owned()),
552                response: response_with_metadata(example_metadata_with_jwks_uri()),
553            })),
554            second: StubClient {
555                expected_url: Some(
556                    "https://jwt-vc-issuer.example.org/my_public_keys.jwks".to_owned(),
557                ),
558                response: response_with_metadata(example_metadata()["jwks"].take()),
559            },
560        };
561
562        let header = example_header();
563
564        let response = HttpsIssuerPublicKeyLookup { client }
565            .lookup(Uri::new(alleged_iss).unwrap(), &header)
566            .await
567            .unwrap();
568
569        assert_eq!(
570            response.get("kid").unwrap().as_str().unwrap(),
571            header.kid.unwrap()
572        );
573    }
574
575    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.1-5
576    #[tokio::test]
577    async fn iss_with_more_complex_path() {
578        let alleged_iss = "https://example.com/tenant/1234";
579
580        let metadata = json!({
581           "issuer": alleged_iss,
582           "jwks":{
583              "keys":[
584                 {"kid":"doc-signer-05-25-2022"}
585              ]
586           }
587        });
588
589        let client = StubClient {
590            expected_url: Some(
591                "https://example.com/.well-known/jwt-vc-issuer/tenant/1234".to_string(),
592            ),
593            response: response_with_metadata(metadata),
594        };
595
596        let header = example_header();
597
598        let response = HttpsIssuerPublicKeyLookup { client }
599            .lookup(Uri::new(alleged_iss).unwrap(), &header)
600            .await
601            .unwrap();
602
603        assert_eq!(
604            response.get("kid").unwrap().as_str().unwrap(),
605            header.kid.unwrap()
606        );
607    }
608
609    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.1-1
610    #[tokio::test]
611    async fn iss_scheme_not_http() {
612        let alleged_iss = "git://example.com";
613
614        let client = StubClient {
615            expected_url: None,
616            response: response_with_metadata(example_metadata()),
617        };
618
619        let response = HttpsIssuerPublicKeyLookup { client }
620            .lookup(Uri::new(alleged_iss).unwrap(), &example_header())
621            .await;
622
623        assert_eq!(
624            response.unwrap_err().error,
625            LookupError("iss uri scheme should be https".to_string())
626        );
627    }
628
629    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5-2
630    #[tokio::test]
631    async fn iss_contains_query() {
632        let alleged_iss = "https://example.com/issuer?name=issue";
633
634        let client = StubClient {
635            expected_url: None,
636            response: response_with_metadata(example_metadata()),
637        };
638
639        let response = HttpsIssuerPublicKeyLookup { client }
640            .lookup(Uri::new(alleged_iss).unwrap(), &example_header())
641            .await;
642
643        assert_eq!(
644            response.unwrap_err().error,
645            LookupError("iss uri should not contain query or fragment parts".to_string())
646        );
647    }
648
649    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5-2
650    #[tokio::test]
651    async fn iss_contains_fragment() {
652        let alleged_iss = "https://example.com/issuer#nose";
653
654        let client = StubClient {
655            expected_url: None,
656            response: response_with_metadata(example_metadata()),
657        };
658
659        let response = HttpsIssuerPublicKeyLookup { client }
660            .lookup(Uri::new(alleged_iss).unwrap(), &example_header())
661            .await;
662
663        assert_eq!(
664            response.unwrap_err().error,
665            LookupError("iss uri should not contain query or fragment parts".to_string())
666        );
667    }
668
669    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-1
670    #[tokio::test]
671    async fn response_wrong_status_code() {
672        let alleged_iss = "https://example.com";
673
674        let client = StubClient {
675            expected_url: Some("https://example.com/.well-known/jwt-vc-issuer".to_string()),
676            response: http::Response::builder()
677                .status(201)
678                .body("".to_string())
679                .unwrap(),
680        };
681
682        let response = HttpsIssuerPublicKeyLookup { client }
683            .lookup(Uri::new(alleged_iss).unwrap(), &example_header())
684            .await;
685
686        assert_eq!(
687            response.unwrap_err().error,
688            LookupError("response status code was 201 Created, expected 200 OK".to_string())
689        );
690    }
691
692    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-1
693    #[tokio::test]
694    async fn response_wrong_content_type() {
695        let alleged_iss = "https://example.com";
696
697        let client = StubClient {
698            expected_url: Some("https://example.com/.well-known/jwt-vc-issuer".to_string()),
699            response: http::Response::builder()
700                .status(200)
701                .header("Content-type", "text/html")
702                .body("".to_string())
703                .unwrap(),
704        };
705
706        let response = HttpsIssuerPublicKeyLookup { client }
707            .lookup(Uri::new(alleged_iss).unwrap(), &example_header())
708            .await;
709
710        assert_eq!(
711            response.unwrap_err().error,
712            LookupError(
713                "response content type was text/html, expected application/json".to_string()
714            )
715        );
716    }
717
718    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.3-1
719    #[tokio::test]
720    async fn metadata_issuer_value_not_identical_to_iss() {
721        let alleged_iss = "https://example.com/tenant";
722
723        let client = StubClient {
724            expected_url: Some("https://example.com/.well-known/jwt-vc-issuer/tenant".to_string()),
725            response: response_with_metadata(example_metadata()),
726        };
727
728        let response = HttpsIssuerPublicKeyLookup { client }
729            .lookup(Uri::new(alleged_iss).unwrap(), &example_header())
730            .await;
731
732        assert_eq!(
733            response.unwrap_err().error,
734            LookupError("response issuer value was not identical to iss value of JWT".to_string())
735        );
736    }
737
738    // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-sd-jwt-vc-03#section-5.2-6
739    #[tokio::test]
740    async fn multiple_jwks_in_metadata() {
741        let alleged_iss = "https://example.com";
742
743        let metadata = json!({
744           "issuer": alleged_iss,
745           "jwks":{
746              "keys":[
747                 {"kid": "kid1"},
748                 {"kid": "kid2"},
749                 {"kid": "kid3"}
750              ],
751           }
752        });
753
754        let client = StubClient {
755            expected_url: Some("https://example.com/.well-known/jwt-vc-issuer".to_string()),
756            response: response_with_metadata(metadata),
757        };
758        let mut header = example_header();
759        header.kid = Some("kid2".to_string());
760
761        let response = HttpsIssuerPublicKeyLookup { client }
762            .lookup(Uri::new(alleged_iss).unwrap(), &header)
763            .await
764            .unwrap();
765
766        assert_eq!(
767            response.get("kid").unwrap().as_str().unwrap(),
768            header.kid.unwrap()
769        );
770    }
771
772    #[test]
773    fn issuer_metadata_contains_both_jwks_and_jwks_uri() {
774        let metadata = json!({
775           "issuer": "issuer",
776           "jwks":{
777              "keys":[
778                 {"kid": "kid1"},
779              ],
780           },
781           "jwks_uri": "https://jwt-vc-issuer.example.org/my_public_keys.jwks"
782        });
783
784        let err = serde_json::from_str::<JwtVcIssuerMetadata>(metadata.to_string().as_str());
785
786        assert_eq!(
787            err.unwrap_err().to_string(),
788            "`jwks` and `jwks_uri` are mutually exclusive"
789        );
790    }
791
792    #[test]
793    fn issuer_metadata_serialization() {
794        let metadata = example_metadata();
795
796        let deserialized =
797            serde_json::from_str::<JwtVcIssuerMetadata>(metadata.to_string().as_str()).unwrap();
798
799        assert_eq!(deserialized.issuer, "https://example.com".to_string());
800        assert_eq!(
801            deserialized
802                .jwks_param
803                .to_jwks()
804                .keys
805                .first()
806                .unwrap()
807                .get("kid")
808                .unwrap(),
809            &Value::String("doc-signer-05-25-2022".to_string())
810        );
811        assert_eq!(
812            serde_json::to_string(&deserialized).unwrap(),
813            example_metadata().to_string()
814        );
815    }
816
817    #[test]
818    fn issuer_metadata_serialization_jwks_uri() {
819        let metadata = example_metadata_with_jwks_uri();
820
821        let deserialized =
822            serde_json::from_str::<JwtVcIssuerMetadata>(metadata.to_string().as_str()).unwrap();
823
824        assert_eq!(deserialized.issuer, "https://example.com".to_string());
825        assert_eq!(
826            deserialized.jwks_param.to_jwks_uri(),
827            "https://jwt-vc-issuer.example.org/my_public_keys.jwks"
828        );
829        assert_eq!(
830            serde_json::to_string(&deserialized).unwrap(),
831            example_metadata_with_jwks_uri().to_string()
832        );
833    }
834
835    #[test]
836    fn test_metadata_uri_from_iss() {
837        let iss = Uri::new("http://example.com/path").unwrap();
838
839        let url = metadata_uri_from_iss(iss);
840
841        assert_eq!(url, "http://example.com/.well-known/jwt-vc-issuer/path");
842    }
843
844    #[test]
845    fn test_metadata_uri_from_iss_trailing_slash() {
846        let iss = Uri::new("http://example.com/path/").unwrap();
847
848        let url = metadata_uri_from_iss(iss);
849
850        assert_eq!(url, "http://example.com/.well-known/jwt-vc-issuer/path/");
851    }
852
853    #[test]
854    fn test_metadata_uri_from_iss_no_path() {
855        let iss = Uri::new("http://example.com").unwrap();
856
857        let url = metadata_uri_from_iss(iss);
858
859        assert_eq!(url, "http://example.com/.well-known/jwt-vc-issuer");
860    }
861
862    #[test]
863    fn test_metadata_uri_from_iss_no_path_trailing_slash() {
864        let iss = Uri::new("http://example.com/").unwrap();
865
866        let url = metadata_uri_from_iss(iss);
867
868        assert_eq!(url, "http://example.com/.well-known/jwt-vc-issuer");
869    }
870
871    /// Returns a dummy [`bhx5chain::X5Chain`] for testing purposes.
872    fn dummy_x5chain() -> bhx5chain::X5Chain {
873        let cert = "-----BEGIN CERTIFICATE-----
874MIIBqDCCAU+gAwIBAgIUa3Ph3O2ChLkG2WGly2OlOA2oB/gwCgYIKoZIzj0EAwIw
875DzENMAsGA1UEAwwEdGJ0bDAeFw0yNTEwMTQxMTI3NDhaFw0zNTEwMTIxMTI3NDha
876MA8xDTALBgNVBAMMBHRidGwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARh62Ff
8772AR3vJQJFb1N/N5qQwUCIcYpc82B5wckfSlYTD9Z7or8/DHS8fl0Tx84QUSCq1j5
878EoXAsa6WyWwH/Oo9o4GIMIGFMB0GA1UdDgQWBBQcHDN+Jn8uxxWyWsvidMXsxux7
879ZTAfBgNVHSMEGDAWgBQcHDN+Jn8uxxWyWsvidMXsxux7ZTAPBgNVHRMBAf8EBTAD
880AQH/MDIGA1UdEQQrMCmCC2V4YW1wbGUuY29thwQKAAABhhRodHRwczovL3d3dy50
881YnRsLm5ldDAKBggqhkjOPQQDAgNHADBEAiAo1LhKVWisYrzCR02qweeOQaJOaEAZ
882UGok7hT15f+X6wIgHQ5uUck3v4W0PxZyVL1dd6tZM3gPcmD/yR25VcbrADY=
883-----END CERTIFICATE-----";
884
885        let cert = X509::from_pem(cert.as_bytes()).unwrap();
886        bhx5chain::X5Chain::new(vec![cert]).unwrap()
887    }
888
889    /// Returns a dummy [`bhx5chain::JwtX5Chain`] for testing purposes.
890    fn dummy_jwt_x5chain() -> bhx5chain::JwtX5Chain {
891        dummy_x5chain().try_into().unwrap()
892    }
893
894    /// Returns a root certificate for the dummy X.509 certificate chain.
895    ///
896    /// This will serve as a root for the certificate chains returned by the
897    /// [`dummy_x5chain`] and [`dummy_jwt_x5chain`] functions.
898    fn dummy_root_certificate() -> X509 {
899        dummy_x5chain().leaf_certificate().to_owned()
900    }
901
902    #[tokio::test]
903    async fn test_public_key_from_x5chain() {
904        let x5chain = dummy_x5chain();
905        let jwt_x5chain = dummy_jwt_x5chain();
906
907        // NB: `iss` does not matter for `x5c`-based public key lookup anymore
908        let alleged_iss = "https://example.com";
909        let header = example_header_with_x5c(jwt_x5chain.clone());
910
911        let public_jwk = X5ChainIssuerPublicKeyLookup::trust_all()
912            .lookup(alleged_iss, &header)
913            .await
914            .unwrap();
915
916        assert_eq!(
917            public_jwk_from_x5chain_leaf(&x5chain, &header.alg, header.kid.as_deref()).unwrap(),
918            public_jwk
919        );
920    }
921
922    #[tokio::test]
923    async fn test_public_key_from_x5chain_missing_header_field() {
924        let alleged_iss = "https://example.com";
925        let header = example_header();
926
927        let err = X5ChainIssuerPublicKeyLookup::trust_all()
928            .lookup(alleged_iss, &header)
929            .await
930            .unwrap_err()
931            .error;
932
933        assert!(matches!(err, LookupError(msg) if msg == "missing 'x5c' jwt header"));
934    }
935
936    #[tokio::test]
937    async fn test_public_key_from_x5chain_verify_authenticity() {
938        let x5chain = dummy_x5chain();
939        let jwt_x5chain = dummy_jwt_x5chain();
940
941        let alleged_iss = "https://example.com";
942        let header = example_header_with_x5c(jwt_x5chain.clone());
943
944        // Issuer authenticity verified
945        let trust = X509Trust::new(vec![dummy_root_certificate()]);
946        let public_jwk = X5ChainIssuerPublicKeyLookup::with_trust(trust)
947            .lookup(alleged_iss, &header)
948            .await
949            .unwrap();
950        assert_eq!(
951            public_jwk_from_x5chain_leaf(&x5chain, &header.alg, header.kid.as_deref()).unwrap(),
952            public_jwk
953        );
954
955        // no Issuer is trusted (empty `trust`)
956        let trust = X509Trust::new(vec![]);
957        let err = X5ChainIssuerPublicKeyLookup::with_trust(trust)
958            .lookup(alleged_iss, &header)
959            .await
960            .unwrap_err();
961        assert!(
962            matches!(err.error, LookupError(msg) if msg == "x5chain does not verify against trusted roots")
963        );
964
965        // every Issuer is trusted (`trust` not provided)
966        let public_jwk = X5ChainIssuerPublicKeyLookup::trust_all()
967            .lookup(alleged_iss, &header)
968            .await
969            .unwrap();
970        assert_eq!(
971            public_jwk_from_x5chain_leaf(&x5chain, &header.alg, header.kid.as_deref()).unwrap(),
972            public_jwk
973        );
974    }
975
976    #[tokio::test]
977    async fn test_verify_issued_sd_jwt_happy_path() {
978        let issued_sd_jwt = "eyJ0eXAiOiJ2YytzZC1qd3QiLCJhbGciOiJFUzI1NiIsImtpZCI6Imlzc3VlciBraWQiLCJ4NWMiOlsiTUlJQjlEQ0NBWnFnQXdJQkFnSVVXZmFXV0FtK2kvbWRQR2luY25RQjR4NHROb013Q2dZSUtvWkl6ajBFQXdJd2F6RUxNQWtHQTFVRUJoTUNTRkl4RkRBU0JnTlZCQWdNQzBkeVlXUWdXbUZuY21WaU1ROHdEUVlEVlFRSERBWmFZV2R5WldJeERUQUxCZ05WQkFvTUJGUkNWRXd4RVRBUEJnTlZCQXNNQ0ZSbFlXMGdRbVZsTVJNd0VRWURWUVFEREFwamIyTnZiblYwTFdOaE1CNFhEVEkxTURNd05ERXpNekV4TmxvWERUTTFNRE13TWpFek16RXhObG93RWpFUU1BNEdBMVVFQXd3SFkyOWpiMjUxZERCWk1CTUdCeXFHU000OUFnRUdDQ3FHU000OUF3RUhBMElBQkREamhLOVlHc1ZvWmpxZlRYbldYTnFneCt6NlZTUkJnU3RUb1dFZ3N4R2V3UWhVMkNaYXBqK0ZwempLY1phd0RIRlovaHY5NUMxQnEwTW02U2V3RitxamRUQnpNQWtHQTFVZEV3UUNNQUF3RGdZRFZSMFBBUUgvQkFRREFnYkFNQjBHQTFVZERnUVdCQlE5Z1NxNEdxekVISTlVbTRFbitndDVYNkYxdERBZkJnTlZIU01FR0RBV2dCVFZIYjlKV25iMmR4Q2ZDME16b21tckxndEdlVEFXQmdOVkhSRUVEekFOZ2d0bGVHRnRjR3hsTG1OdmJUQUtCZ2dxaGtqT1BRUURBZ05JQURCRkFpRUE5QXhoeTRTVFJQUmNSY2w1eVRxYXp6QU1WaU0wbUhHWVg0YWUvZjJTY3FvQ0lCTkR5R3lsMTJ1Z2hpaStZdkt5VUt2dTdYVUR1cllWQjJIY0pMUTNuem5YIiwiTUlJQ09qQ0NBZUNnQXdJQkFnSVVON295UHd4cUxlMnhETUxqRHZhMEhxcUtEWHd3Q2dZSUtvWkl6ajBFQXdJd1pURUxNQWtHQTFVRUJoTUNTRkl4RkRBU0JnTlZCQWdNQzBkeVlXUWdXbUZuY21WaU1ROHdEUVlEVlFRSERBWmFZV2R5WldJeERUQUxCZ05WQkFvTUJGUkNWRXd4RVRBUEJnTlZCQXNNQ0ZSbFlXMGdRbVZsTVEwd0N3WURWUVFEREFSeWIyOTBNQ0FYRFRJME1USXhNREV5TWpRek5Wb1lEekl4TWpReE1URTJNVEl5TkRNMVdqQnJNUXN3Q1FZRFZRUUdFd0pJVWpFVU1CSUdBMVVFQ0F3TFIzSmhaQ0JhWVdkeVpXSXhEekFOQmdOVkJBY01CbHBoWjNKbFlqRU5NQXNHQTFVRUNnd0VWRUpVVERFUk1BOEdBMVVFQ3d3SVZHVmhiU0JDWldVeEV6QVJCZ05WQkFNTUNtTnZZMjl1ZFhRdFkyRXdXVEFUQmdjcWhrak9QUUlCQmdncWhrak9QUU1CQndOQ0FBUWRSR2ErMk9IaWFXYVIzK1JKNjNoR0VZV0I0aWpVdTdGcGhpSUNETjBKL2Z4QkJOOEUzVS9jdFQzZU1TcTVTeVBZTXZFbXMyS01PeER5a3ZnNkloRmdvMll3WkRBZEJnTlZIUTRFRmdRVTFSMi9TVnAyOW5jUW53dERNNkpwcXk0TFJua3dId1lEVlIwakJCZ3dGb0FVSm5iemROUERKVFd3SURCLzJsYmw3ampYazkwd0VnWURWUjBUQVFIL0JBZ3dCZ0VCL3dJQkFEQU9CZ05WSFE4QkFmOEVCQU1DQVlZd0NnWUlLb1pJemowRUF3SURTQUF3UlFJaEFOYTJ1V3VhV0wvZGZ0QkZSM3ArWldKaDdYNWpXRUFhNlZ0TXBtRWE2ZlRiQWlCNU45Nk4yckNJYjRnaUdPODZZUUJQb1dTUkE2UWovYmFiS25pQVlzSkxxUT09Il19.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tL2lzc3VlciIsImV4cCI6MTg4MzAwMDAwMCwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImFsZyI6IkVTMjU2IiwidXNlIjoic2lnIiwiY3J2IjoiUC0yNTYiLCJ4IjoiT3d2dDhKUEpPRHFfRG9zVkRUQllsR2RGOUk1UGM0TENNOERvLVlCd0xjUSIsInkiOiIyM3V6VVlrZlh4RV95M3hybFQyM1ZCSUNyUmczOVQ3N1dHQUVvLXB5ZE1JIiwia2lkIjoiaG9sZGVyIGtpZCJ9fSwidmN0IjoiaHR0cHM6Ly9ibWkuYnVuZC5leGFtcGxlL2NyZWRlbnRpYWwvcGlkLzEuMCIsIl9zZF9hbGciOiJzaGEtMjU2IiwiaWF0IjoxNjgzMDAwMDAwLCJhZ2VfZXF1YWxfb3Jfb3ZlciI6eyJfc2QiOlsiem5VT0NOdUM1SDZJcWt1SnhJa2FIZ2JOU1NTVXhXczk4MmZFeW1iNGFtWSIsIldVb194ZUhpQTk1aE1jMHZSUXdLbThCM1Z3bHEyVUJkRHZBOTNpWDBLeDQiLCJOVXFjekZpYVdNbGtTWWNCVmNqSHNhR2Q1dTVRc3d4cWEwQjhiS0diMTZVIiwiUEhERWhucXZTQ3FvMlRITXoyS0pTLXdnYkRteWIwVHRYaUlfb0I0Y1ZwZyIsImRmbXdNTDJCdHVycDJBdmFXYmh1ME5hZ1E4eXJ2c3J1XzJpYXIzYVp5b1EiLCJaVlMtdDNHWm5ETVhJUm0yZmdWU1RaNDhlMGZ4OWxldVZvQTB5RElGcHcwIl19LCJfc2QiOlsiU1I3X0hvYi1zR216cFBlMDAtSXFSclYzUC1OeWVHaTl4d3ZTU2JnQ2RDSSIsImE3cmdNVHRSaWVvT0xYWGVGbWdnT25XSS1VQnhCUDVKVXlHTW85ZW14eUUiLCJ6MjAtckZIVmpvOXEySWtZZFZGUzdCTlpTeDJkZzRiSVRaYWdXYWk3Y1AwIiwiNWZmRXFZZUtvOENpUVh1Q0NPQWRwNno4X3c4MXdjWnp4MW94Ym1yRzZ5USIsIkJ4cUtpdG9UZjVQNDF1X1Y1OEdCcjR0Q1FhSGZjLUh1dDJWb0VENUlEVGciLCJ4NUZjVWIwYnFTZkNsWGo0b1BuUmxXMHRqemh4dkRNNk9sNGs3VWotQzc4IiwiT2g1Q3ZUbmFaRVFqSTE4Z0dRLXZKeFZXQW9wXzdwMnd0NTZtNTRydFYzNCIsImZhTTNERUFCNml5OVJyTnJjNmFXQ25tSENjZkt0aWh3RHY3MWZYUzA4ZDgiLCJfUWxnUERQSkdsY1ZpQXNlY0lYV0JheTB4bGE5MTN1X3U3R0RSWEowZEJrIiwiRVQxUmlFc2xvci1ab1dGS2hrYmpnX2dRdFprZmhQbThNTTJyT1VJLV9DWSJdfQ.yJfwSCKETHZy740Mg2Yk2qDW-rQcqmbdMfUYq8c9wvBAj_d2cssuxBiYA_Fl9tkX33J3UL9JzwdqCm3pq3pjAA~WyIwUDAwV2RhVmx3NHFuY0l3V0tiYTNRIiwgIjY1IiwgZmFsc2Vd~WyJhdGhfRkFMTDVqbEFrX3p2R2lxUGNnIiwgIjIxIiwgdHJ1ZV0~WyJOcEpwM1Q1RlR4SGR5MTk2UnBuUWh3IiwgIjE4IiwgdHJ1ZV0~WyJybW9OQ0NSSGZ3UUNxNmpfcHVpRE5nIiwgIjE2IiwgdHJ1ZV0~WyJGbTJyYkxrVlRoQTFlb2UwZFdxclpnIiwgIjE0IiwgdHJ1ZV0~WyJSbGdndGZyUTBoNHVtSmpKd1B2ekhnIiwgIjEyIiwgdHJ1ZV0~WyJBQk9yWkNIUGxXdlFDMFVQNkdISEp3IiwgImxvY2FsaXR5IiwgIkJlcmxpbiJd~WyI2bE1PME1QMVFsMG5RU2JJU2hRcVBnIiwgImNvdW50cnkiLCAiREUiXQ~WyJfWVNmbWRZZWdyR1R5aTVBc2ZjYXJBIiwgInBvc3RhbF9jb2RlIiwgIjUxMTQ3Il0~WyJvVC1rVFZMWm1aejk4Q1RvdnBsOTdBIiwgImxvY2FsaXR5IiwgIkvDtmxuIl0~WyJzdVJQRWNQdm5ha25vRldJdTdDNFp3IiwgInN0cmVldF9hZGRyZXNzIiwgIkhlaWRlc3RyYcOfZSAxNyJd~WyJVRlJSMmZ3OURia01McUQ2Q3VibGJBIiwgImFsc29fa25vd25fYXMiLCAiU2Nod2VzdGVyIEFnbmVzIl0~WyJTaWczZXJmX191aGo3WnJEcTdhM0VBIiwgInBsYWNlX29mX2JpcnRoIiwgeyJjb3VudHJ5IjoiREUiLCJfc2QiOlsib1ZZNWgxbDZpR0hpb1h0Z3NLV29UTTFtSEU5TDY3dzFqWmNuZXZYbi1hTSJdfV0~WyJyektQOXVsZWExS2M4MUhZdG5Nb1JnIiwgImJpcnRoX2ZhbWlseV9uYW1lIiwgIkdhYmxlciJd~WyJkejRoM1YyVzJGY1RmWllIMHo2aGdRIiwgImdlbmRlciIsICJmZW1hbGUiXQ~WyJNdFdkMXRHc2VYaEVhN1NscDJ0U1B3IiwgIm5hdGlvbmFsaXRpZXMiLCBbIkRFIl1d~WyJVOWNfeGVONFk3bURrZUdlM2c5UV9RIiwgImFkZHJlc3MiLCB7Il9zZCI6WyJJT0NvUGRCekljNGEzZFBRN0d5SjN3cjRSSjhVZjUtMzhPNWJaclZlbGhrIiwiNkI5WUNRc18xY25HaXNPQVJsNUVrZXRLZUxSZFpPd3MwZzM0OEdfYnRKWSIsIlVaUjVXQ0RHcU1VTG9OZElxVWlLWkptMTVfTUVZekZQTGNlLTB3c1hfR1UiLCJKdlVuRHpIM210SDVrZVhKMXBXVnFVdGplREh1YVNXdF9kU3ZXTmw4SUFrIl19XQ~WyJRWFAtalFtRFZyNlc2aWM5MENOeml3IiwgInNvdXJjZV9kb2N1bWVudF90eXBlIiwgImlkX2NhcmQiXQ~WyJDOU5vN1RNVXpOVjdFSGJXQjV1NW5RIiwgImJpcnRoZGF0ZSIsICIxOTYzLTA4LTEyIl0~WyJGUXpIN3ExczROSUVydWFEaHNmc2xnIiwgImZhbWlseV9uYW1lIiwgIk11c3Rlcm1hbm4iXQ~WyJrUEdPcHRuTkt1QkZSSEpaMXZsMGhnIiwgImdpdmVuX25hbWUiLCAiRXJpa2EiXQ~";
979
980        Holder::verify_issued(
981            issued_sd_jwt,
982            &X5ChainIssuerPublicKeyLookup::trust_all(),
983            |alg| (alg == HashingAlgorithm::Sha256).then_some(Box::new(Sha256)),
984            |alg| (alg == SigningAlgorithm::Es256).then_some(&Es256Verifier),
985            HOLDER_ACCEPT_TIME,
986        )
987        .await
988        .unwrap();
989    }
990}