1use 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#[derive(Serialize, Deserialize, Debug, PartialEq)]
53#[serde(try_from = "JwtVcIssuerMetadataUnverified")]
54pub struct JwtVcIssuerMetadata {
55 pub issuer: String,
57 #[serde(flatten)]
59 pub jwks_param: JwksParam,
60 #[serde(flatten)]
62 pub params: JsonObject,
63}
64
65#[derive(Debug, PartialEq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum JwksParam {
69 Jwks(JwkSet),
71 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#[derive(Deserialize, Debug)]
121struct JwtVcIssuerMetadataUnverified {
122 issuer: String,
123 #[serde(flatten)]
124 jwks_param: JwksParam,
125 #[serde(flatten)]
126 params: JsonObject,
127}
128
129impl TryFrom<JwtVcIssuerMetadataUnverified> for JwtVcIssuerMetadata {
131 type Error = &'static str;
132
133 fn try_from(value: JwtVcIssuerMetadataUnverified) -> Result<Self, Self::Error> {
134 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
148pub trait HttpGetClient: Sync {
154 type Err: std::error::Error + Send + Sync + 'static;
156 fn get(
161 &self,
162 url: &str,
163 ) -> impl Future<Output = std::result::Result<reqwest::Response, Self::Err>> + Send;
164}
165
166pub struct ReqwestGetClient(Client);
168
169impl ReqwestGetClient {
170 pub fn new(client: Client) -> Self {
172 Self(client)
173 }
174 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
188pub struct HttpsIssuerPublicKeyLookup<C: HttpGetClient> {
193 client: C,
195}
196
197impl<C: HttpGetClient> HttpsIssuerPublicKeyLookup<C> {
198 pub fn new(client: C) -> Self {
200 HttpsIssuerPublicKeyLookup { client }
201 }
202}
203
204pub const ISSUER_METADATA_URL_SUFFIX: &str = "/.well-known/jwt-vc-issuer";
206
207#[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, {
223 type Err = LookupError;
224 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 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 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 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 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
298fn 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
324fn 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
341pub 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
348pub 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
356pub struct X5ChainIssuerPublicKeyLookup {
363 trust: Option<X509Trust>,
364}
365
366impl X5ChainIssuerPublicKeyLookup {
367 pub fn trust_all() -> Self {
371 tracing::warn!("Issuer's authenticity will not be verified");
372
373 Self { trust: None }
374 }
375
376 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 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 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 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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 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 fn dummy_jwt_x5chain() -> bhx5chain::JwtX5Chain {
891 dummy_x5chain().try_into().unwrap()
892 }
893
894 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 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 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 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 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}