From dac4102bca3f4627f0f4c343cb3e4295aec8e05f Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 16 Jul 2024 15:22:16 +0200 Subject: [PATCH] feat: add support for multiple signing algorithms (#76) * feat: add support for multiple singing algorithms * docs: improve TODO comments * feat: add selelect functionality for subject syntax type * docs: add TODO comment regarding `subject_syntax_types_supported` parameter * fix: fix typos --- Cargo.toml | 2 +- dif-presentation-exchange/Cargo.toml | 1 + dif-presentation-exchange/src/lib.rs | 4 +- .../src/presentation_definition.rs | 5 +- oid4vc-core/Cargo.toml | 2 +- oid4vc-core/src/authentication/sign.rs | 5 +- oid4vc-core/src/authentication/subject.rs | 3 +- oid4vc-core/src/client_metadata.rs | 3 + oid4vc-core/src/jwt.rs | 15 ++- oid4vc-core/src/openid4vc_extension.rs | 16 +++ oid4vc-core/src/subject_syntax_type.rs | 8 ++ oid4vc-core/src/test_utils.rs | 7 +- oid4vc-manager/Cargo.toml | 2 +- oid4vc-manager/src/managers/provider.rs | 28 ++++- oid4vc-manager/src/managers/relying_party.rs | 15 ++- oid4vc-manager/src/methods/key_method.rs | 14 ++- .../driver_license.json | 1 + .../university_degree.json | 1 + oid4vc-manager/tests/common/mod.rs | 6 +- .../tests/oid4vci/authorization_code.rs | 14 +-- .../tests/oid4vci/pre_authorized_code.rs | 10 +- oid4vc-manager/tests/oid4vp/implicit.rs | 32 +++-- oid4vc-manager/tests/siopv2/implicit.rs | 28 +++-- oid4vci/Cargo.toml | 2 +- .../credential_configurations_supported.rs | 3 +- .../credential_issuer_metadata.rs | 3 +- oid4vci/src/proof.rs | 16 +-- oid4vci/src/wallet/mod.rs | 110 +++++++++++++++--- oid4vp/Cargo.toml | 3 + oid4vp/src/authorization_request.rs | 15 ++- oid4vp/src/lib.rs | 4 +- oid4vp/src/oid4vp.rs | 102 +++++++++++++++- oid4vp/src/token/vp_token_builder.rs | 11 +- .../client_metadata/client_client_id_did.json | 2 +- siopv2/Cargo.toml | 2 +- siopv2/src/authorization_request.rs | 19 ++- siopv2/src/provider.rs | 59 ++++++++-- siopv2/src/relying_party.rs | 7 +- siopv2/src/siopv2.rs | 80 ++++++++++++- siopv2/src/test_utils.rs | 7 +- siopv2/src/token/id_token_builder.rs | 28 +++-- 41 files changed, 560 insertions(+), 135 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d6688ce1..b3d0184d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ getset = "0.1" identity_core = "1.2.0" identity_credential = { version = "1.2.0", default-features = false, features = ["validator", "credential", "presentation"] } is_empty = "0.2" -jsonwebtoken = "8.2" +jsonwebtoken = "9.3" monostate = "0.1" reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } reqwest-middleware = "0.2" diff --git a/dif-presentation-exchange/Cargo.toml b/dif-presentation-exchange/Cargo.toml index ecb03536..b905243a 100644 --- a/dif-presentation-exchange/Cargo.toml +++ b/dif-presentation-exchange/Cargo.toml @@ -12,6 +12,7 @@ repository.workspace = true getset.workspace = true jsonpath_lib = "0.3" jsonschema = "0.17" +jsonwebtoken.workspace = true serde.workspace = true serde_json.workspace = true serde_with.workspace = true diff --git a/dif-presentation-exchange/src/lib.rs b/dif-presentation-exchange/src/lib.rs index 17d1a8a0..039647de 100644 --- a/dif-presentation-exchange/src/lib.rs +++ b/dif-presentation-exchange/src/lib.rs @@ -3,5 +3,7 @@ pub mod presentation_definition; pub mod presentation_submission; pub use input_evaluation::evaluate_input; -pub use presentation_definition::{ClaimFormatDesignation, InputDescriptor, PresentationDefinition}; +pub use presentation_definition::{ + ClaimFormatDesignation, ClaimFormatProperty, InputDescriptor, PresentationDefinition, +}; pub use presentation_submission::{InputDescriptorMappingObject, PathNested, PresentationSubmission}; diff --git a/dif-presentation-exchange/src/presentation_definition.rs b/dif-presentation-exchange/src/presentation_definition.rs index 22e23ff7..ef62b7a0 100644 --- a/dif-presentation-exchange/src/presentation_definition.rs +++ b/dif-presentation-exchange/src/presentation_definition.rs @@ -1,4 +1,5 @@ use getset::Getters; +use jsonwebtoken::Algorithm; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; use std::collections::HashMap; @@ -59,7 +60,7 @@ pub enum ClaimFormatDesignation { #[derive(Deserialize, Debug, PartialEq, Clone, Serialize)] #[serde(rename_all = "snake_case")] pub enum ClaimFormatProperty { - Alg(Vec), + Alg(Vec), ProofType(Vec), } @@ -268,7 +269,7 @@ mod tests { purpose: None, format: Some(HashMap::from_iter(vec![( ClaimFormatDesignation::MsoMdoc, - ClaimFormatProperty::Alg(vec!["EdDSA".to_string(), "ES256".to_string()]) + ClaimFormatProperty::Alg(vec![Algorithm::EdDSA, Algorithm::ES256]) )])), constraints: Constraints { limit_disclosure: Some(LimitDisclosure::Required), diff --git a/oid4vc-core/Cargo.toml b/oid4vc-core/Cargo.toml index 2fda1868..8196d89b 100644 --- a/oid4vc-core/Cargo.toml +++ b/oid4vc-core/Cargo.toml @@ -13,7 +13,7 @@ did_url = "0.1.0" ed25519-dalek = { version = "2.0.0", features = ["rand_core"] } getset = "0.1.2" is_empty = "0.2.0" -jsonwebtoken = "8.2.0" +jsonwebtoken.workspace = true lazy_static = "1.4.0" rand = "0.8" serde.workspace = true diff --git a/oid4vc-core/src/authentication/sign.rs b/oid4vc-core/src/authentication/sign.rs index 7ba7b29a..8718c865 100644 --- a/oid4vc-core/src/authentication/sign.rs +++ b/oid4vc-core/src/authentication/sign.rs @@ -1,13 +1,14 @@ use anyhow::Result; use async_trait::async_trait; +use jsonwebtoken::Algorithm; use std::sync::Arc; #[async_trait] pub trait Sign: Send + Sync { // TODO: add this? // fn jwt_alg_name() -> &'static str; - async fn key_id(&self, subject_syntax_type: &str) -> Option; - async fn sign(&self, message: &str, subject_syntax_type: &str) -> Result>; + async fn key_id(&self, subject_syntax_type: &str, algorithm: Algorithm) -> Option; + async fn sign(&self, message: &str, subject_syntax_type: &str, algorithm: Algorithm) -> Result>; fn external_signer(&self) -> Option>; } diff --git a/oid4vc-core/src/authentication/subject.rs b/oid4vc-core/src/authentication/subject.rs index 475020b4..39bab988 100644 --- a/oid4vc-core/src/authentication/subject.rs +++ b/oid4vc-core/src/authentication/subject.rs @@ -1,6 +1,7 @@ use crate::{Sign, Verify}; use anyhow::Result; use async_trait::async_trait; +use jsonwebtoken::Algorithm; use std::sync::Arc; pub type SigningSubject = Arc; @@ -9,5 +10,5 @@ pub type SigningSubject = Arc; /// This [`Subject`] trait is used to sign and verify JWTs. #[async_trait] pub trait Subject: Sign + Verify + Send + Sync { - async fn identifier(&self, subject_syntax_type: &str) -> Result; + async fn identifier(&self, subject_syntax_type: &str, algorithm: Algorithm) -> Result; } diff --git a/oid4vc-core/src/client_metadata.rs b/oid4vc-core/src/client_metadata.rs index a5aea690..59d8dab2 100644 --- a/oid4vc-core/src/client_metadata.rs +++ b/oid4vc-core/src/client_metadata.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; +use std::collections::HashMap; use url::Url; /// [`ClientMetadata`] is a request parameter used by a [`crate::RelyingParty`] to communicate its capabilities to a @@ -16,6 +17,8 @@ pub enum ClientMetadataResource { /// expanded with Extensions and profiles. #[serde(flatten)] extension: T, + #[serde(flatten, skip_serializing_if = "HashMap::is_empty")] + other: HashMap, }, ClientMetadataUri(String), } diff --git a/oid4vc-core/src/jwt.rs b/oid4vc-core/src/jwt.rs index 20cd1e2f..0404a246 100644 --- a/oid4vc-core/src/jwt.rs +++ b/oid4vc-core/src/jwt.rs @@ -43,11 +43,17 @@ pub fn decode(jwt: &str, public_key: Vec, algorithm: Algorithm) -> Result where T: DeserializeOwned, { - let key = DecodingKey::from_ed_der(public_key.as_slice()); + let decoding_key = match algorithm { + Algorithm::EdDSA => DecodingKey::from_ed_der(public_key.as_slice()), + Algorithm::ES256 => DecodingKey::from_ec_der(public_key.as_slice()), + _ => return Err(anyhow!("Unsupported algorithm.")), + }; + let mut validation = Validation::new(algorithm); validation.validate_exp = false; + validation.validate_aud = false; validation.required_spec_claims.clear(); - Ok(jsonwebtoken::decode::(jwt, &key, &validation)?.claims) + Ok(jsonwebtoken::decode::(jwt, &decoding_key, &validation)?.claims) } pub async fn encode(signer: Arc, header: Header, claims: C, subject_syntax_type: &str) -> Result @@ -55,8 +61,9 @@ where C: Serialize, S: Sign + ?Sized, { + let algorithm = header.alg; let kid = signer - .key_id(subject_syntax_type) + .key_id(subject_syntax_type, algorithm) .await .ok_or(anyhow!("No key identifier found."))?; @@ -64,7 +71,7 @@ where let message = [base64_url_encode(&jwt.header)?, base64_url_encode(&jwt.payload)?].join("."); - let proof_value = signer.sign(&message, subject_syntax_type).await?; + let proof_value = signer.sign(&message, subject_syntax_type, algorithm).await?; let signature = base64_url::encode(proof_value.as_slice()); let message = [message, signature].join("."); Ok(message) diff --git a/oid4vc-core/src/openid4vc_extension.rs b/oid4vc-core/src/openid4vc_extension.rs index 76061f0a..7499943e 100644 --- a/oid4vc-core/src/openid4vc_extension.rs +++ b/oid4vc-core/src/openid4vc_extension.rs @@ -1,4 +1,5 @@ use crate::{authorization_response::AuthorizationResponse, Subject, SubjectSyntaxType, Validator}; +use jsonwebtoken::Algorithm; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{future::Future, sync::Arc}; @@ -29,11 +30,26 @@ pub trait Extension: Serialize + PartialEq + Sized + std::fmt::Debug + Clone + S _extension_parameters: &::Parameters, _user_input: &::Input, _subject_syntax_type: impl TryInto, + _signing_algorithm: impl TryInto, ) -> impl Future>> { // Will be overwritten by the extension. async { Err(anyhow::anyhow!("Not implemented.")) } } + fn get_relying_party_supported_algorithms( + _authorization_request: &::Parameters, + ) -> impl Future>> { + // Will be overwritten by the extension. + async { Err(anyhow::anyhow!("Not implemented.")) } + } + + fn get_relying_party_supported_syntax_types( + _authorization_request: &::Parameters, + ) -> impl Future>> { + // Will be overwritten by the extension. + async { Err(anyhow::anyhow!("Not implemented.")) } + } + fn build_authorization_response( _jwts: Vec, _user_input: ::Input, diff --git a/oid4vc-core/src/subject_syntax_type.rs b/oid4vc-core/src/subject_syntax_type.rs index 9640b4e1..d79e4f02 100644 --- a/oid4vc-core/src/subject_syntax_type.rs +++ b/oid4vc-core/src/subject_syntax_type.rs @@ -40,6 +40,14 @@ impl TryFrom<&str> for SubjectSyntaxType { } } +impl TryFrom for SubjectSyntaxType { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + SubjectSyntaxType::from_str(value.as_str()) + } +} + impl From for SubjectSyntaxType { fn from(did_method: DidMethod) -> Self { SubjectSyntaxType::Did(did_method) diff --git a/oid4vc-core/src/test_utils.rs b/oid4vc-core/src/test_utils.rs index 12a6f6e2..b1d3bc4f 100644 --- a/oid4vc-core/src/test_utils.rs +++ b/oid4vc-core/src/test_utils.rs @@ -5,6 +5,7 @@ use anyhow::Result; use async_trait::async_trait; use derivative::{self, Derivative}; use ed25519_dalek::{Signature, Signer, SigningKey}; +use jsonwebtoken::Algorithm; use lazy_static::lazy_static; use rand::rngs::OsRng; @@ -32,11 +33,11 @@ impl TestSubject { #[async_trait] impl Sign for TestSubject { - async fn key_id(&self, _subject_syntax_type: &str) -> Option { + async fn key_id(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Option { Some(self.key_id.clone()) } - async fn sign(&self, message: &str, _subject_syntax_type: &str) -> Result> { + async fn sign(&self, message: &str, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result> { let signature: Signature = TEST_KEYPAIR.sign(message.as_bytes()); Ok(signature.to_bytes().to_vec()) } @@ -55,7 +56,7 @@ impl Verify for TestSubject { #[async_trait] impl Subject for TestSubject { - async fn identifier(&self, _subject_syntax_type: &str) -> Result { + async fn identifier(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result { Ok(self.did.to_string()) } } diff --git a/oid4vc-manager/Cargo.toml b/oid4vc-manager/Cargo.toml index 21888638..925ab02d 100644 --- a/oid4vc-manager/Cargo.toml +++ b/oid4vc-manager/Cargo.toml @@ -21,7 +21,7 @@ futures = "0.3" getset.workspace = true identity_core.workspace = true identity_credential.workspace = true -jsonwebtoken = "8.3" +jsonwebtoken.workspace = true paste = "1.0" reqwest.workspace = true serde.workspace = true diff --git a/oid4vc-manager/src/managers/provider.rs b/oid4vc-manager/src/managers/provider.rs index ff93abdf..aac51e60 100644 --- a/oid4vc-manager/src/managers/provider.rs +++ b/oid4vc-manager/src/managers/provider.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use jsonwebtoken::Algorithm; use oid4vc_core::{ authorization_request::{AuthorizationRequest, Object}, authorization_response::AuthorizationResponse, @@ -17,10 +18,11 @@ pub struct ProviderManager { impl ProviderManager { pub fn new( subject: Arc, - default_subject_syntax_type: impl TryInto, + supported_subject_syntax_types: Vec>, + supported_signing_algorithms: Vec, ) -> Result { Ok(Self { - provider: Provider::new(subject, default_subject_syntax_type)?, + provider: Provider::new(subject, supported_subject_syntax_types, supported_signing_algorithms)?, }) } @@ -28,6 +30,24 @@ impl ProviderManager { self.provider.validate_request(authorization_request).await } + pub async fn get_matching_signing_algorithm( + &self, + authorization_request: &AuthorizationRequest>, + ) -> Result { + self.provider + .get_matching_signing_algorithm(authorization_request) + .await + } + + pub async fn get_matching_subject_syntax_type( + &self, + authorization_request: &AuthorizationRequest>, + ) -> Result { + self.provider + .get_matching_subject_syntax_type(authorization_request) + .await + } + pub async fn generate_response( &self, authorization_request: &AuthorizationRequest>, @@ -43,7 +63,7 @@ impl ProviderManager { self.provider.send_response(authorization_response).await } - pub fn default_subject_syntax_type(&self) -> &SubjectSyntaxType { - &self.provider.default_subject_syntax_type + pub fn default_subject_syntax_types(&self) -> &Vec { + &self.provider.supported_subject_syntax_types } } diff --git a/oid4vc-manager/src/managers/relying_party.rs b/oid4vc-manager/src/managers/relying_party.rs index ab759d44..d07c140a 100644 --- a/oid4vc-manager/src/managers/relying_party.rs +++ b/oid4vc-manager/src/managers/relying_party.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use jsonwebtoken::Algorithm; use oid4vc_core::{ authorization_request::{AuthorizationRequest, Object}, authorization_response::AuthorizationResponse, @@ -11,15 +12,19 @@ use std::sync::Arc; /// Manager struct for [`siopv2::RelyingParty`]. pub struct RelyingPartyManager { pub relying_party: RelyingParty, + // TODO: this should be replaced with `client_metadata` + pub supported_signing_algorithms: Vec, } impl RelyingPartyManager { pub fn new( subject: Arc, default_subject_syntax_type: impl TryInto, + supported_signing_algorithms: Vec, ) -> Result { Ok(Self { relying_party: RelyingParty::new(subject, default_subject_syntax_type)?, + supported_signing_algorithms, }) } @@ -27,7 +32,15 @@ impl RelyingPartyManager { &self, authorization_request: &AuthorizationRequest>, ) -> Result { - self.relying_party.encode(authorization_request).await + self.relying_party + .encode( + authorization_request, + *self + .supported_signing_algorithms + .first() + .ok_or(anyhow::anyhow!("No supported signing algorithms"))?, + ) + .await } pub async fn validate_response( diff --git a/oid4vc-manager/src/methods/key_method.rs b/oid4vc-manager/src/methods/key_method.rs index f23f9c32..7c59f7e4 100644 --- a/oid4vc-manager/src/methods/key_method.rs +++ b/oid4vc-manager/src/methods/key_method.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use did_key::{generate, resolve, Config, CoreSign, DIDCore, Document, Ed25519KeyPair, KeyMaterial, PatchedKeyPair}; +use jsonwebtoken::Algorithm; use oid4vc_core::{authentication::sign::ExternalSign, Sign, Subject, Verify}; use std::sync::Arc; @@ -43,14 +44,14 @@ impl Default for KeySubject { #[async_trait] impl Sign for KeySubject { - async fn key_id(&self, _subject_syntax_type: &str) -> Option { + async fn key_id(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Option { self.document .authentication .as_ref() .and_then(|authentication_methods| authentication_methods.first().cloned()) } - async fn sign(&self, message: &str, _subject_syntax_type: &str) -> Result> { + async fn sign(&self, message: &str, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result> { match self.external_signer() { Some(external_signer) => external_signer.sign(message), None => Ok(self.keypair.sign(message.as_bytes())), @@ -71,7 +72,7 @@ impl Verify for KeySubject { #[async_trait] impl Subject for KeySubject { - async fn identifier(&self, _subject_syntax_type: &str) -> Result { + async fn identifier(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result { Ok(self.document.id.clone()) } } @@ -111,6 +112,7 @@ async fn resolve_public_key(kid: &str) -> Result> { mod tests { use super::*; use crate::{ProviderManager, RelyingPartyManager}; + use jsonwebtoken::Algorithm; use oid4vc_core::authorization_request::{AuthorizationRequest, Object}; use siopv2::siopv2::SIOPv2; use std::sync::Arc; @@ -121,7 +123,8 @@ mod tests { let subject = KeySubject::new(); // Create a new provider manager. - let provider_manager = ProviderManager::new(Arc::new(subject), "did:key").unwrap(); + let provider_manager = + ProviderManager::new(Arc::new(subject), vec!["did:key"], vec![Algorithm::EdDSA]).unwrap(); // Get a new SIOP authorization_request with response mode `direct_post` for cross-device communication. let request_url = "\ @@ -153,7 +156,8 @@ mod tests { .unwrap(); // Let the relying party validate the authorization_response. - let relying_party_manager = RelyingPartyManager::new(Arc::new(KeySubject::new()), "did:key").unwrap(); + let relying_party_manager = + RelyingPartyManager::new(Arc::new(KeySubject::new()), "did:key", vec![Algorithm::EdDSA]).unwrap(); assert!(relying_party_manager .validate_response(&authorization_response) .await diff --git a/oid4vc-manager/tests/common/credential_configurations_supported_objects/driver_license.json b/oid4vc-manager/tests/common/credential_configurations_supported_objects/driver_license.json index bab08895..8c7c0961 100644 --- a/oid4vc-manager/tests/common/credential_configurations_supported_objects/driver_license.json +++ b/oid4vc-manager/tests/common/credential_configurations_supported_objects/driver_license.json @@ -16,6 +16,7 @@ "proof_types_supported": { "jwt": { "proof_signing_alg_values_supported": [ + "EdDSA", "ES256" ] } diff --git a/oid4vc-manager/tests/common/credential_configurations_supported_objects/university_degree.json b/oid4vc-manager/tests/common/credential_configurations_supported_objects/university_degree.json index 31a8c66c..91413d97 100644 --- a/oid4vc-manager/tests/common/credential_configurations_supported_objects/university_degree.json +++ b/oid4vc-manager/tests/common/credential_configurations_supported_objects/university_degree.json @@ -42,6 +42,7 @@ "proof_types_supported": { "jwt": { "proof_signing_alg_values_supported": [ + "EdDSA", "ES256" ] } diff --git a/oid4vc-manager/tests/common/mod.rs b/oid4vc-manager/tests/common/mod.rs index 7e6a2390..63aaa1fb 100644 --- a/oid4vc-manager/tests/common/mod.rs +++ b/oid4vc-manager/tests/common/mod.rs @@ -37,11 +37,11 @@ impl TestSubject { #[async_trait] impl Sign for TestSubject { - async fn key_id(&self, _subject_syntax_type: &str) -> Option { + async fn key_id(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Option { Some(self.key_id.clone()) } - async fn sign(&self, message: &str, _subject_syntax_type: &str) -> Result> { + async fn sign(&self, message: &str, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result> { let signature: Signature = TEST_KEYPAIR.sign(message.as_bytes()); Ok(signature.to_bytes().to_vec()) } @@ -60,7 +60,7 @@ impl Verify for TestSubject { #[async_trait] impl Subject for TestSubject { - async fn identifier(&self, _subject_syntax_type: &str) -> Result { + async fn identifier(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result { Ok(self.did.to_string()) } } diff --git a/oid4vc-manager/tests/oid4vci/authorization_code.rs b/oid4vc-manager/tests/oid4vci/authorization_code.rs index b9b8c408..57e24663 100644 --- a/oid4vc-manager/tests/oid4vci/authorization_code.rs +++ b/oid4vc-manager/tests/oid4vci/authorization_code.rs @@ -1,5 +1,6 @@ use crate::common::{get_jwt_claims, memory_storage::MemoryStorage}; use did_key::{generate, Ed25519KeyPair}; +use jsonwebtoken::Algorithm; use oid4vc_core::Subject; use oid4vc_manager::{ managers::credential_issuer::CredentialIssuerManager, methods::key_method::KeySubject, @@ -38,10 +39,10 @@ async fn test_authorization_code_flow() { // Create a new subject. let subject = KeySubject::new(); - let subject_did = subject.identifier("did:key").await.unwrap(); + let subject_did = subject.identifier("did:key", Algorithm::EdDSA).await.unwrap(); // Create a new wallet. - let wallet = Wallet::new(Arc::new(subject), "did:key").unwrap(); + let wallet: Wallet = Wallet::new(Arc::new(subject), vec!["did:key"], vec![Algorithm::EdDSA]).unwrap(); // Get the credential issuer url. let credential_issuer_url = credential_issuer @@ -62,12 +63,11 @@ async fn test_authorization_code_flow() { .unwrap(); // Get the credential format for a university degree. - let university_degree_credential_format: CredentialFormats = credential_issuer_metadata + let university_degree_credential_format = credential_issuer_metadata .credential_configurations_supported .get("UniversityDegree_JWT") .unwrap() - .clone() - .credential_format; + .clone(); // Get the authorization code. let authorization_response = wallet @@ -77,7 +77,7 @@ async fn test_authorization_code_flow() { r#type: OpenidCredential::Type, locations: None, credential_configuration_or_format: CredentialConfigurationOrFormat::CredentialFormat( - university_degree_credential_format.clone(), + university_degree_credential_format.credential_format.clone(), ), } .into()], @@ -102,7 +102,7 @@ async fn test_authorization_code_flow() { .get_credential( credential_issuer_metadata, &token_response, - university_degree_credential_format, + &university_degree_credential_format, ) .await .unwrap(); diff --git a/oid4vc-manager/tests/oid4vci/pre_authorized_code.rs b/oid4vc-manager/tests/oid4vci/pre_authorized_code.rs index 0105eece..5cc41eb5 100644 --- a/oid4vc-manager/tests/oid4vci/pre_authorized_code.rs +++ b/oid4vc-manager/tests/oid4vci/pre_authorized_code.rs @@ -1,5 +1,6 @@ use crate::common::{get_jwt_claims, memory_storage::MemoryStorage}; use did_key::{generate, Ed25519KeyPair}; +use jsonwebtoken::Algorithm; use oid4vc_core::Subject; use oid4vc_manager::{ managers::credential_issuer::CredentialIssuerManager, methods::key_method::KeySubject, @@ -42,10 +43,10 @@ async fn test_pre_authorized_code_flow(#[case] batch: bool, #[case] by_reference // Create a new subject. let subject = KeySubject::new(); - let subject_did = subject.identifier("did:key").await.unwrap(); + let subject_did = subject.identifier("did:key", Algorithm::EdDSA).await.unwrap(); // Create a new wallet. - let wallet: Wallet = Wallet::new(Arc::new(subject), "did:key").unwrap(); + let wallet: Wallet = Wallet::new(Arc::new(subject), vec!["did:key"], vec![Algorithm::EdDSA]).unwrap(); // Get the credential offer url. let credential_offer_query = credential_issuer @@ -109,7 +110,6 @@ async fn test_pre_authorized_code_flow(#[case] batch: bool, #[case] by_reference .credential_configurations_supported .get(credential) .unwrap() - .credential_format .clone() }) .collect(); @@ -122,7 +122,7 @@ async fn test_pre_authorized_code_flow(#[case] batch: bool, #[case] by_reference .get_credential( credential_issuer_metadata, &token_response, - university_degree_credential_format, + &university_degree_credential_format, ) .await .unwrap(); @@ -162,7 +162,7 @@ async fn test_pre_authorized_code_flow(#[case] batch: bool, #[case] by_reference } else if batch { // Get the credentials. let batch_credential_response: BatchCredentialResponse = wallet - .get_batch_credential(credential_issuer_metadata, &token_response, credentials) + .get_batch_credential(credential_issuer_metadata, &token_response, &credentials) .await .unwrap(); diff --git a/oid4vc-manager/tests/oid4vp/implicit.rs b/oid4vc-manager/tests/oid4vp/implicit.rs index d8de19cf..c0a9f9f6 100644 --- a/oid4vc-manager/tests/oid4vp/implicit.rs +++ b/oid4vc-manager/tests/oid4vp/implicit.rs @@ -5,6 +5,7 @@ use lazy_static::lazy_static; use oid4vc_core::{ authorization_request::{AuthorizationRequest, Object}, authorization_response::AuthorizationResponse, + client_metadata::ClientMetadataResource, jwt, Subject, }; use oid4vc_manager::{ @@ -13,11 +14,12 @@ use oid4vc_manager::{ }; use oid4vci::VerifiableCredentialJwt; use oid4vp::{ + authorization_request::ClientMetadataParameters, oid4vp::{AuthorizationResponseInput, OID4VP}, - PresentationDefinition, + ClaimFormatDesignation, ClaimFormatProperty, PresentationDefinition, }; use serde_json::json; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; lazy_static! { pub static ref PRESENTATION_DEFINITION: PresentationDefinition = serde_json::from_value(json!( @@ -77,31 +79,47 @@ async fn test_implicit_flow() { )), None, ); - let issuer_did = issuer.identifier("did:key").await.unwrap(); + let issuer_did = issuer.identifier("did:key", Algorithm::EdDSA).await.unwrap(); // Create a new subject. let subject = Arc::new(KeySubject::from_keypair( generate::(Some("this-is-a-very-UNSAFE-secret-key".as_bytes().try_into().unwrap())), None, )); - let subject_did = subject.identifier("did:key").await.unwrap(); + let subject_did = subject.identifier("did:key", Algorithm::EdDSA).await.unwrap(); // Create a new relying party. let relying_party = Arc::new(KeySubject::new()); - let relying_party_did = relying_party.identifier("did:key").await.unwrap(); - let relying_party_manager = RelyingPartyManager::new(relying_party, "did:key").unwrap(); + let relying_party_did = relying_party.identifier("did:key", Algorithm::EdDSA).await.unwrap(); + let relying_party_manager = RelyingPartyManager::new(relying_party, "did:key", vec![Algorithm::EdDSA]).unwrap(); // Create authorization request with response_type `id_token vp_token` let authorization_request = AuthorizationRequest::>::builder() .client_id(relying_party_did) .redirect_uri("https://example.com".parse::().unwrap()) .presentation_definition(PRESENTATION_DEFINITION.clone()) + .client_metadata(ClientMetadataResource::ClientMetadata { + client_name: None, + logo_uri: None, + extension: ClientMetadataParameters { + vp_formats: vec![( + ClaimFormatDesignation::JwtVcJson, + ClaimFormatProperty::Alg(vec![Algorithm::EdDSA]), + )] + .into_iter() + .collect(), + }, + other: HashMap::from_iter(vec![( + "subject_syntax_types_supported".to_string(), + json!(vec!["did:key".to_string(),]), + )]), + }) .nonce("nonce".to_string()) .build() .unwrap(); // Create a provider manager and validate the authorization request. - let provider_manager = ProviderManager::new(subject, "did:key").unwrap(); + let provider_manager = ProviderManager::new(subject, vec!["did:key"], vec![Algorithm::EdDSA]).unwrap(); // Create a new verifiable credential. let verifiable_credential = VerifiableCredentialJwt::builder() diff --git a/oid4vc-manager/tests/siopv2/implicit.rs b/oid4vc-manager/tests/siopv2/implicit.rs index 48688c50..3e895756 100644 --- a/oid4vc-manager/tests/siopv2/implicit.rs +++ b/oid4vc-manager/tests/siopv2/implicit.rs @@ -1,6 +1,7 @@ use crate::common::{MemoryStorage, Storage, TestSubject}; use axum::async_trait; use did_key::{generate, Ed25519KeyPair}; +use jsonwebtoken::Algorithm; use lazy_static::lazy_static; use oid4vc_core::{ authentication::sign::ExternalSign, @@ -32,18 +33,18 @@ pub struct MultiDidMethodSubject { #[async_trait] impl Sign for MultiDidMethodSubject { - async fn key_id(&self, subject_syntax_type: &str) -> Option { + async fn key_id(&self, subject_syntax_type: &str, algorithm: Algorithm) -> Option { match subject_syntax_type { - "did:test" => self.test_subject.key_id(subject_syntax_type).await, - "did:key" => self.key_subject.key_id(subject_syntax_type).await, + "did:test" => self.test_subject.key_id(subject_syntax_type, algorithm).await, + "did:key" => self.key_subject.key_id(subject_syntax_type, algorithm).await, _ => None, } } - async fn sign(&self, message: &str, subject_syntax_type: &str) -> anyhow::Result> { + async fn sign(&self, message: &str, subject_syntax_type: &str, algorithm: Algorithm) -> anyhow::Result> { match subject_syntax_type { - "did:test" => self.test_subject.sign(message, subject_syntax_type).await, - "did:key" => self.key_subject.sign(message, subject_syntax_type).await, + "did:test" => self.test_subject.sign(message, subject_syntax_type, algorithm).await, + "did:key" => self.key_subject.sign(message, subject_syntax_type, algorithm).await, _ => Err(anyhow::anyhow!("Unsupported DID method.")), } } @@ -66,10 +67,10 @@ impl Verify for MultiDidMethodSubject { #[async_trait] impl Subject for MultiDidMethodSubject { - async fn identifier(&self, subject_syntax_type: &str) -> anyhow::Result { + async fn identifier(&self, subject_syntax_type: &str, algorithm: Algorithm) -> anyhow::Result { match subject_syntax_type { - "did:test" => self.test_subject.identifier(subject_syntax_type).await, - "did:key" => self.key_subject.identifier(subject_syntax_type).await, + "did:test" => self.test_subject.identifier(subject_syntax_type, algorithm).await, + "did:key" => self.key_subject.identifier(subject_syntax_type, algorithm).await, _ => Err(anyhow::anyhow!("Unsupported DID method.")), } } @@ -121,10 +122,11 @@ async fn test_implicit_flow(#[case] did_method: &str) { key_subject: KeySubject::from_keypair(generate::(None), None), }; - let client_id = subject.identifier(did_method).await.unwrap(); + let client_id = subject.identifier(did_method, Algorithm::EdDSA).await.unwrap(); // Create a new relying party manager. - let relying_party_manager = RelyingPartyManager::new(Arc::new(subject), did_method).unwrap(); + let relying_party_manager = + RelyingPartyManager::new(Arc::new(subject), did_method, vec![Algorithm::EdDSA]).unwrap(); // Create a new RequestUrl with response mode `direct_post` for cross-device communication. let authorization_request: AuthorizationRequest> = AuthorizationRequest::>::builder() @@ -137,7 +139,9 @@ async fn test_implicit_flow(#[case] did_method: &str) { logo_uri: None, extension: ClientMetadataParameters { subject_syntax_types_supported: vec![SubjectSyntaxType::Did(DidMethod::from_str(did_method).unwrap())], + id_token_signed_response_alg: Some(Algorithm::EdDSA), }, + other: Default::default(), }) .claims( r#"{ @@ -182,7 +186,7 @@ async fn test_implicit_flow(#[case] did_method: &str) { }; // Create a new provider manager. - let provider_manager = ProviderManager::new(Arc::new(subject), did_method).unwrap(); + let provider_manager = ProviderManager::new(Arc::new(subject), vec![did_method], vec![Algorithm::EdDSA]).unwrap(); // Create a new RequestUrl which includes a `request_uri` pointing to the mock server's `request_uri` endpoint. let authorization_request = AuthorizationRequest:: { diff --git a/oid4vci/Cargo.toml b/oid4vci/Cargo.toml index 66311114..d79d34ed 100644 --- a/oid4vci/Cargo.toml +++ b/oid4vci/Cargo.toml @@ -16,7 +16,7 @@ anyhow = "1.0" derivative = "2.2.0" getset.workspace = true lazy_static = "1.4" -jsonwebtoken = "8.3" +jsonwebtoken.workspace = true paste = "1.0" reqwest.workspace = true reqwest-middleware.workspace = true diff --git a/oid4vci/src/credential_issuer/credential_configurations_supported.rs b/oid4vci/src/credential_issuer/credential_configurations_supported.rs index c16153f1..f68effce 100644 --- a/oid4vci/src/credential_issuer/credential_configurations_supported.rs +++ b/oid4vci/src/credential_issuer/credential_configurations_supported.rs @@ -37,6 +37,7 @@ mod tests { w3c_verifiable_credentials::{jwt_vc_json, ldp_vc, CredentialSubject}, CredentialFormats, Parameters, }; + use jsonwebtoken::Algorithm; use serde::de::DeserializeOwned; use serde_json::json; use std::{collections::HashMap, fs::File, path::Path}; @@ -112,7 +113,7 @@ mod tests { proof_types_supported: vec![( ProofType::Jwt, KeyProofMetadata { - proof_signing_alg_values_supported: vec!["ES256".to_string()] + proof_signing_alg_values_supported: vec![Algorithm::ES256] } )] .into_iter() diff --git a/oid4vci/src/credential_issuer/credential_issuer_metadata.rs b/oid4vci/src/credential_issuer/credential_issuer_metadata.rs index 09a12f87..17093de9 100644 --- a/oid4vci/src/credential_issuer/credential_issuer_metadata.rs +++ b/oid4vci/src/credential_issuer/credential_issuer_metadata.rs @@ -51,6 +51,7 @@ mod tests { proof::KeyProofMetadata, ProofType, }; + use jsonwebtoken::Algorithm; use serde::de::DeserializeOwned; use serde_json::json; use std::{fs::File, path::Path}; @@ -151,7 +152,7 @@ mod tests { proof_types_supported: vec![( ProofType::Jwt, KeyProofMetadata { - proof_signing_alg_values_supported: vec!["ES256".to_string()] + proof_signing_alg_values_supported: vec![Algorithm::ES256] } )] .into_iter() diff --git a/oid4vci/src/proof.rs b/oid4vci/src/proof.rs index acbe8002..03a9936c 100644 --- a/oid4vci/src/proof.rs +++ b/oid4vci/src/proof.rs @@ -14,14 +14,14 @@ pub enum KeyProofType { } impl KeyProofType { - pub fn builder() -> ProofBuilder { - ProofBuilder::default() + pub fn builder() -> KeyProofTypeBuilder { + KeyProofTypeBuilder::default() } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct KeyProofMetadata { - pub proof_signing_alg_values_supported: Vec, + pub proof_signing_alg_values_supported: Vec, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] @@ -33,8 +33,9 @@ pub enum ProofType { } #[derive(Default)] -pub struct ProofBuilder { +pub struct KeyProofTypeBuilder { proof_type: Option, + algorithm: Option, rfc7519_claims: RFC7519Claims, nonce: Option, signer: Option>, @@ -48,7 +49,7 @@ pub struct ProofOfPossession { pub nonce: String, } -impl ProofBuilder { +impl KeyProofTypeBuilder { pub async fn build(self) -> anyhow::Result { anyhow::ensure!(self.rfc7519_claims.aud.is_some(), "aud claim is required"); anyhow::ensure!(self.rfc7519_claims.iat.is_some(), "iat claim is required"); @@ -63,7 +64,7 @@ impl ProofBuilder { jwt: jwt::encode( self.signer.ok_or(anyhow::anyhow!("No subject found"))?.clone(), Header { - alg: Algorithm::EdDSA, + alg: self.algorithm.ok_or(anyhow::anyhow!("algorithm is required"))?, typ: Some("openid4vci-proof+jwt".to_string()), ..Default::default() }, @@ -86,10 +87,9 @@ impl ProofBuilder { } builder_fn!(proof_type, ProofType); + builder_fn!(algorithm, Algorithm); builder_fn!(rfc7519_claims, iss, String); builder_fn!(rfc7519_claims, aud, String); - // TODO: fix this, required by jsonwebtoken crate. - builder_fn!(rfc7519_claims, exp, i64); builder_fn!(rfc7519_claims, iat, i64); builder_fn!(nonce, String); builder_fn!(subject_syntax_type, String); diff --git a/oid4vci/src/wallet/mod.rs b/oid4vci/src/wallet/mod.rs index c706b903..f2895c8e 100644 --- a/oid4vci/src/wallet/mod.rs +++ b/oid4vci/src/wallet/mod.rs @@ -2,6 +2,7 @@ use crate::authorization_details::AuthorizationDetailsObject; use crate::authorization_request::AuthorizationRequest; use crate::authorization_response::AuthorizationResponse; use crate::credential_format_profiles::{CredentialFormatCollection, CredentialFormats, WithParameters}; +use crate::credential_issuer::credential_configurations_supported::CredentialConfigurationsSupportedObject; use crate::credential_issuer::{ authorization_server_metadata::AuthorizationServerMetadata, credential_issuer_metadata::CredentialIssuerMetadata, }; @@ -10,7 +11,8 @@ use crate::credential_request::{BatchCredentialRequest, CredentialRequest}; use crate::credential_response::BatchCredentialResponse; use crate::proof::{KeyProofType, ProofType}; use crate::{credential_response::CredentialResponse, token_request::TokenRequest, token_response::TokenResponse}; -use anyhow::Result; +use anyhow::{anyhow, Result}; +use jsonwebtoken::Algorithm; use oid4vc_core::authentication::subject::SigningSubject; use oid4vc_core::SubjectSyntaxType; use reqwest::Url; @@ -18,21 +20,24 @@ use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use reqwest_retry::policies::ExponentialBackoff; use reqwest_retry::RetryTransientMiddleware; use serde::de::DeserializeOwned; +use std::str::FromStr; pub struct Wallet> where CFC: CredentialFormatCollection, { pub subject: SigningSubject, - pub default_subject_syntax_type: SubjectSyntaxType, + pub supported_subject_syntax_types: Vec, pub client: ClientWithMiddleware, + pub proof_signing_alg_values_supported: Vec, phantom: std::marker::PhantomData, } impl Wallet { pub fn new( subject: SigningSubject, - default_subject_syntax_type: impl TryInto, + supported_subject_syntax_types: Vec>, + proof_signing_alg_values_supported: Vec, ) -> anyhow::Result { let retry_policy = ExponentialBackoff::builder().build_with_max_retries(5); let client = ClientBuilder::new(reqwest::Client::new()) @@ -40,10 +45,16 @@ impl Wallet { .build(); Ok(Self { subject, - default_subject_syntax_type: default_subject_syntax_type - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid did method"))?, + supported_subject_syntax_types: supported_subject_syntax_types + .into_iter() + .map(|subject_syntax_type| { + subject_syntax_type + .try_into() + .map_err(|_| anyhow::anyhow!("Invalid did method.")) + }) + .collect::>()?, client, + proof_signing_alg_values_supported, phantom: std::marker::PhantomData, }) } @@ -115,7 +126,14 @@ impl Wallet { response_type: "code".to_string(), client_id: self .subject - .identifier(&self.default_subject_syntax_type.to_string()) + .identifier( + &self + .supported_subject_syntax_types + .first() + .map(ToString::to_string) + .ok_or(anyhow!("No supported subject syntax types found."))?, + self.proof_signing_alg_values_supported[0], + ) .await?, redirect_uri: None, scope: None, @@ -140,26 +158,73 @@ impl Wallet { .map_err(|e| e.into()) } + fn select_signing_algorithm( + &self, + credential_configuration: &CredentialConfigurationsSupportedObject, + ) -> Result { + let credential_issuer_proof_signing_alg_values_supported = &credential_configuration + .proof_types_supported + .get(&ProofType::Jwt) + .ok_or(anyhow::anyhow!( + "`jwt` proof type is missing from the `proof_types_supported` parameter" + ))? + .proof_signing_alg_values_supported; + + self.proof_signing_alg_values_supported + .iter() + .find(|supported_algorithm| { + credential_issuer_proof_signing_alg_values_supported.contains(supported_algorithm) + }) + .cloned() + .ok_or(anyhow::anyhow!("No supported signing algorithm found.")) + } + + fn select_subject_syntax_type( + &self, + credential_configuration: &CredentialConfigurationsSupportedObject, + ) -> Result { + let credential_issuer_cryptographic_binding_methods_supported: Vec = + credential_configuration + .cryptographic_binding_methods_supported + .iter() + .filter_map(|binding_method| SubjectSyntaxType::from_str(binding_method).ok()) + .collect(); + + self.supported_subject_syntax_types + .iter() + .find(|supported_syntax_type| { + credential_issuer_cryptographic_binding_methods_supported.contains(supported_syntax_type) + }) + .cloned() + .ok_or(anyhow::anyhow!("No supported subject syntax types found.")) + } + pub async fn get_credential( &self, credential_issuer_metadata: CredentialIssuerMetadata, token_response: &TokenResponse, - credential_format: CFC, + credential_configuration: &CredentialConfigurationsSupportedObject, ) -> Result { + let credential_format = credential_configuration.credential_format.to_owned(); + + let signing_algorithm = self.select_signing_algorithm(credential_configuration)?; + let subject_syntax_type = self.select_subject_syntax_type(credential_configuration)?; + let credential_request = CredentialRequest { credential_format, proof: Some( KeyProofType::builder() .proof_type(ProofType::Jwt) + .algorithm(signing_algorithm) .signer(self.subject.clone()) .iss( self.subject - .identifier(&self.default_subject_syntax_type.to_string()) + .identifier(&subject_syntax_type.to_string(), signing_algorithm) .await?, ) .aud(credential_issuer_metadata.credential_issuer) + // TODO: Use current time. .iat(1571324800) - .exp(9999999999i64) // TODO: so is this REQUIRED or OPTIONAL? .nonce( token_response @@ -168,7 +233,7 @@ impl Wallet { .ok_or(anyhow::anyhow!("No c_nonce found."))? .clone(), ) - .subject_syntax_type(self.default_subject_syntax_type.to_string()) + .subject_syntax_type(&subject_syntax_type.to_string()) .build() .await?, ), @@ -189,20 +254,29 @@ impl Wallet { &self, credential_issuer_metadata: CredentialIssuerMetadata, token_response: &TokenResponse, - credential_formats: Vec, + credential_configurations: &[CredentialConfigurationsSupportedObject], ) -> Result { + // TODO: This needs to be fixed since this current implementation assumes that for all credentials the same Proof Type is supported. + let credential_configuration = credential_configurations + .first() + .ok_or(anyhow::anyhow!("No credential configurations found."))?; + + let signing_algorithm = self.select_signing_algorithm(credential_configuration)?; + let subject_syntax_type = self.select_subject_syntax_type(credential_configuration)?; + let proof = Some( KeyProofType::builder() .proof_type(ProofType::Jwt) + .algorithm(signing_algorithm) .signer(self.subject.clone()) .iss( self.subject - .identifier(&self.default_subject_syntax_type.to_string()) + .identifier(&subject_syntax_type.to_string(), signing_algorithm) .await?, ) .aud(credential_issuer_metadata.credential_issuer) + // TODO: Use current time. .iat(1571324800) - .exp(9999999999i64) // TODO: so is this REQUIRED or OPTIONAL? .nonce( token_response @@ -211,16 +285,16 @@ impl Wallet { .ok_or(anyhow::anyhow!("No c_nonce found."))? .clone(), ) - .subject_syntax_type(self.default_subject_syntax_type.to_string()) + .subject_syntax_type(subject_syntax_type.to_string()) .build() .await?, ); let batch_credential_request = BatchCredentialRequest { - credential_requests: credential_formats + credential_requests: credential_configurations .iter() - .map(|credential_format| CredentialRequest { - credential_format: credential_format.to_owned(), + .map(|credential_configuration| CredentialRequest { + credential_format: credential_configuration.credential_format.to_owned(), proof: proof.clone(), }) .collect(), diff --git a/oid4vp/Cargo.toml b/oid4vp/Cargo.toml index 8395821c..d5a33318 100644 --- a/oid4vp/Cargo.toml +++ b/oid4vp/Cargo.toml @@ -23,6 +23,9 @@ identity_credential.workspace = true is_empty.workspace = true jsonwebtoken.workspace = true monostate.workspace = true +reqwest.workspace = true +reqwest-middleware.workspace = true +reqwest-retry.workspace = true serde.workspace = true serde_json.workspace = true serde_with.workspace = true diff --git a/oid4vp/src/authorization_request.rs b/oid4vp/src/authorization_request.rs index 325f31bf..49dd9f24 100644 --- a/oid4vp/src/authorization_request.rs +++ b/oid4vp/src/authorization_request.rs @@ -37,7 +37,7 @@ pub struct AuthorizationRequestParameters { pub scope: Option, pub nonce: String, #[serde(flatten)] - pub client_metadata: Option>, + pub client_metadata: ClientMetadataResource, } #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] @@ -45,7 +45,7 @@ pub struct ClientMetadataParameters { /// Object defining the formats and proof types of Verifiable Presentations and Verifiable Credentials that a /// Verifier supports. /// As described here: https://openid.net/specs/openid-4-verifiable-presentations-1_0-20.html#name-additional-verifier-metadat - vp_formats: HashMap, + pub vp_formats: HashMap, } #[derive(Debug, Default, IsEmpty)] @@ -99,7 +99,10 @@ impl AuthorizationRequestBuilder { .nonce .take() .ok_or_else(|| anyhow!("nonce parameter is required."))?, - client_metadata: self.client_metadata.take(), + client_metadata: self + .client_metadata + .take() + .ok_or_else(|| anyhow!("client_metadata or client_metadata_uri is required."))?, }; Ok(AuthorizationRequest::> { @@ -127,6 +130,7 @@ impl AuthorizationRequestBuilder { mod tests { use std::{fs::File, path::Path}; + use jsonwebtoken::Algorithm; use serde::de::DeserializeOwned; use super::*; @@ -209,7 +213,7 @@ mod tests { vp_formats: vec![ ( ClaimFormatDesignation::JwtVpJson, - ClaimFormatProperty::Alg(vec!["EdDSA".to_string(), "ES256K".to_string(),]) + ClaimFormatProperty::Alg(vec![Algorithm::EdDSA, Algorithm::ES256,]) ), ( ClaimFormatDesignation::LdpVp, @@ -218,7 +222,8 @@ mod tests { ] .into_iter() .collect() - } + }, + other: HashMap::from_iter(vec![("application_type".to_string(), serde_json::json!("web"))]), }), }, json_example::("tests/examples/client_metadata/client_client_id_did.json") diff --git a/oid4vp/src/lib.rs b/oid4vp/src/lib.rs index b73d9e05..eb8636f3 100644 --- a/oid4vp/src/lib.rs +++ b/oid4vp/src/lib.rs @@ -4,7 +4,7 @@ pub mod oid4vp_params; pub mod token; pub use dif_presentation_exchange::{ - evaluate_input, ClaimFormatDesignation, InputDescriptor, InputDescriptorMappingObject, PathNested, - PresentationDefinition, PresentationSubmission, + evaluate_input, ClaimFormatDesignation, ClaimFormatProperty, InputDescriptor, InputDescriptorMappingObject, + PathNested, PresentationDefinition, PresentationSubmission, }; pub use {oid4vp_params::Oid4vpParams, token::vp_token::VpToken}; diff --git a/oid4vp/src/oid4vp.rs b/oid4vp/src/oid4vp.rs index 698ac516..e890f85e 100644 --- a/oid4vp/src/oid4vp.rs +++ b/oid4vp/src/oid4vp.rs @@ -1,7 +1,10 @@ -use crate::authorization_request::{AuthorizationRequestBuilder, AuthorizationRequestParameters}; +use crate::authorization_request::{ + AuthorizationRequestBuilder, AuthorizationRequestParameters, ClientMetadataParameters, +}; use crate::oid4vp_params::{serde_oid4vp_response, Oid4vpParams}; use crate::token::vp_token::VpToken; use chrono::{Duration, Utc}; +use dif_presentation_exchange::presentation_definition::ClaimFormatProperty; pub use dif_presentation_exchange::{ evaluate_input, ClaimFormatDesignation, InputDescriptor, InputDescriptorMappingObject, PathNested, PresentationDefinition, PresentationSubmission, @@ -9,11 +12,16 @@ pub use dif_presentation_exchange::{ use futures::future::join_all; use identity_credential::{credential::Jwt, presentation::Presentation}; use jsonwebtoken::{Algorithm, Header}; +use oid4vc_core::client_metadata::ClientMetadataResource; use oid4vc_core::openid4vc_extension::{OpenID4VC, RequestHandle, ResponseHandle}; use oid4vc_core::{authorization_response::AuthorizationResponse, jwt, openid4vc_extension::Extension, Subject}; use oid4vc_core::{SubjectSyntaxType, Validator}; use oid4vci::VerifiableCredentialJwt; +use reqwest_middleware::ClientBuilder; +use reqwest_retry::policies::ExponentialBackoff; +use reqwest_retry::RetryTransientMiddleware; use serde::{Deserialize, Serialize}; +use std::str::FromStr; use std::sync::Arc; /// This is the [`RequestHandle`] for the [`OID4VP`] extension. @@ -47,12 +55,19 @@ impl Extension for OID4VP { extension_parameters: &::Parameters, user_input: &::Input, subject_syntax_type: impl TryInto, + signing_algorithm: impl TryInto, ) -> anyhow::Result> { + let signing_algorithm = signing_algorithm + .try_into() + .map_err(|_| anyhow::anyhow!("Failed to convert the signing algorithm"))?; + let subject_syntax_type_string = subject_syntax_type .try_into() .map_err(|_| anyhow::anyhow!("Failed to convert the subject syntax type"))? .to_string(); - let subject_identifier = subject.identifier(&subject_syntax_type_string).await?; + let subject_identifier = subject + .identifier(&subject_syntax_type_string, signing_algorithm) + .await?; let vp_token = VpToken::builder() .iss(subject_identifier.clone()) @@ -67,7 +82,7 @@ impl Extension for OID4VP { let jwt = jwt::encode( subject.clone(), - Header::new(Algorithm::EdDSA), + Header::new(signing_algorithm), vp_token, &subject_syntax_type_string, ) @@ -75,6 +90,87 @@ impl Extension for OID4VP { Ok(vec![jwt]) } + // TODO: combine this function with `get_relying_party_supported_syntax_types`. + async fn get_relying_party_supported_algorithms( + authorization_request: &::Parameters, + ) -> anyhow::Result> { + let client_metadata = match &authorization_request.client_metadata { + // Fetch the client metadata from the given URI. + ClientMetadataResource::ClientMetadataUri(client_metadata_uri) => { + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(5); + let client = ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + let client_metadata: ClientMetadataResource = + client.get(client_metadata_uri).send().await?.json().await?; + client_metadata + } + client_metadata => client_metadata.clone(), + }; + + // TODO: in this current solution we assume that if there is a`ClaimFormatDesignation::JwtVcJson` `alg` present + // in the client_metadata that this same `alg` will apply for the signing of all the credentials and the VP as + // well as the Proof of Possession. + match client_metadata { + // Fetch the client metadata from the given URI. + ClientMetadataResource::ClientMetadataUri(_) => unreachable!(), + ClientMetadataResource::ClientMetadata { extension, .. } => extension + .vp_formats + .get(&ClaimFormatDesignation::JwtVcJson) + .and_then(|claim_format_property| match claim_format_property { + ClaimFormatProperty::Alg(algs) => Some(algs.clone()), + // TODO: implement `ProofType`. + ClaimFormatProperty::ProofType(_) => None, + }) + .ok_or(anyhow::anyhow!("No supported algorithms found.")), + } + } + + async fn get_relying_party_supported_syntax_types( + authorization_request: &::Parameters, + ) -> anyhow::Result> { + let client_metadata = match &authorization_request.client_metadata { + ClientMetadataResource::ClientMetadataUri(client_metadata_uri) => { + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(5); + let client = ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + let client_metadata: ClientMetadataResource = + client.get(client_metadata_uri).send().await?.json().await?; + client_metadata + } + client_metadata => client_metadata.clone(), + }; + + match client_metadata { + ClientMetadataResource::ClientMetadataUri(_) => unreachable!(), + ClientMetadataResource::ClientMetadata { other, .. } => { + let subject_syntax_types_supported: Vec = other + // TODO(ngdil): this is a custom implementation at the moment as `subject_syntax_types_supported` is + // strictly a `SIOPv2` Client Metadata parameter and is not mentioned in the `OID4VP` documentation. It + // is expected that that a similar parameter will be added to the `OID4VP` Client Metadata. + .get("subject_syntax_types_supported") + .and_then(|subject_syntax_types_supported| { + subject_syntax_types_supported + .as_array() + .and_then(|subject_syntax_types_supported| { + subject_syntax_types_supported + .iter() + .map(|subject_syntax_type| { + subject_syntax_type.as_str().map(|subject_syntax_type| { + SubjectSyntaxType::from_str(subject_syntax_type).unwrap() + }) + }) + .collect() + }) + }) + .unwrap_or_default(); + + Ok(subject_syntax_types_supported) + } + } + } + fn build_authorization_response( jwts: Vec, user_input: ::Input, diff --git a/oid4vp/src/token/vp_token_builder.rs b/oid4vp/src/token/vp_token_builder.rs index 91883564..0dbd9bcf 100644 --- a/oid4vp/src/token/vp_token_builder.rs +++ b/oid4vp/src/token/vp_token_builder.rs @@ -19,10 +19,13 @@ impl VpTokenBuilder { pub fn build(self) -> Result { anyhow::ensure!(self.rfc7519_claims.iss.is_some(), "iss claim is required"); anyhow::ensure!(self.rfc7519_claims.sub.is_some(), "sub claim is required"); - anyhow::ensure!( - self.rfc7519_claims.sub.as_ref().filter(|s| s.len() <= 255).is_some(), - "sub claim MUST NOT exceed 255 ASCII characters in length" - ); + // TODO: According to https://openid.net/specs/openid-connect-core-1_0.html#IDToken, the sub claim MUST NOT + // exceed 255 ASCII characters in length. However, for `did:jwk` it can be longer so we need to figure out how + // to deal with this. + // anyhow::ensure!( + // self.rfc7519_claims.sub.as_ref().filter(|s| s.len() <= 255).is_some(), + // "sub claim MUST NOT exceed 255 ASCII characters in length" + // ); anyhow::ensure!(self.rfc7519_claims.aud.is_some(), "aud claim is required"); anyhow::ensure!(self.rfc7519_claims.exp.is_some(), "exp claim is required"); anyhow::ensure!(self.rfc7519_claims.iat.is_some(), "iat claim is required"); diff --git a/oid4vp/tests/examples/client_metadata/client_client_id_did.json b/oid4vp/tests/examples/client_metadata/client_client_id_did.json index 5331d61b..9463959b 100644 --- a/oid4vp/tests/examples/client_metadata/client_client_id_did.json +++ b/oid4vp/tests/examples/client_metadata/client_client_id_did.json @@ -7,7 +7,7 @@ "jwt_vp_json": { "alg": [ "EdDSA", - "ES256K" + "ES256" ] }, "ldp_vp": { diff --git a/siopv2/Cargo.toml b/siopv2/Cargo.toml index 2bfdec87..00199c63 100644 --- a/siopv2/Cargo.toml +++ b/siopv2/Cargo.toml @@ -20,7 +20,7 @@ did_url = "0.1.0" futures = "0.3" getset.workspace = true is_empty = "0.2.0" -jsonwebtoken = "8.2.0" +jsonwebtoken.workspace = true monostate.workspace = true reqwest.workspace = true reqwest-middleware.workspace = true diff --git a/siopv2/src/authorization_request.rs b/siopv2/src/authorization_request.rs index d96fbec7..f322513d 100644 --- a/siopv2/src/authorization_request.rs +++ b/siopv2/src/authorization_request.rs @@ -1,6 +1,7 @@ use crate::{siopv2::SIOPv2, ClaimRequests, StandardClaimsRequests}; use anyhow::{anyhow, Result}; use is_empty::IsEmpty; +use jsonwebtoken::Algorithm; use monostate::MustBe; use oid4vc_core::authorization_request::Object; use oid4vc_core::builder_fn; @@ -22,14 +23,16 @@ pub struct AuthorizationRequestParameters { pub nonce: String, pub claims: Option, #[serde(flatten)] - pub client_metadata: Option>, + pub client_metadata: ClientMetadataResource, } +#[skip_serializing_none] #[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct ClientMetadataParameters { /// Represents the URI scheme identifiers of supported Subject Syntax Types. /// As described here: https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#section-7.5-2.1.1 pub subject_syntax_types_supported: Vec, + pub id_token_signed_response_alg: Option, } impl AuthorizationRequestParameters { @@ -38,13 +41,13 @@ impl AuthorizationRequestParameters { } pub fn subject_syntax_types_supported(&self) -> Option<&Vec> { - self.client_metadata.as_ref().and_then(|r| match r { + match &self.client_metadata { ClientMetadataResource::ClientMetadata { extension, .. } => { Some(extension.subject_syntax_types_supported.as_ref()) } // TODO: impl client_metadata_uri. ClientMetadataResource::ClientMetadataUri(_) => None, - }) + } } /// Returns the `id_token` claims from the `claims` parameter including those from the request's scope values. @@ -111,7 +114,10 @@ impl AuthorizationRequestBuilder { .take() .ok_or_else(|| anyhow!("nonce parameter is required."))?, claims: self.claims.take().transpose()?, - client_metadata: self.client_metadata.take(), + client_metadata: self + .client_metadata + .take() + .ok_or_else(|| anyhow!("client_metadata or client_metadata_uri is required."))?, }; Ok(AuthorizationRequest::> { @@ -182,6 +188,9 @@ mod tests { } }"#, ) + .client_metadata(ClientMetadataResource::ClientMetadataUri( + "https://example.com".to_string(), + )) .build() .unwrap(); @@ -206,7 +215,7 @@ mod tests { }), ..Default::default() }), - client_metadata: None, + client_metadata: ClientMetadataResource::ClientMetadataUri("https://example.com".to_string(),), }, } } diff --git a/siopv2/src/provider.rs b/siopv2/src/provider.rs index 68675f3f..e7be4dd3 100644 --- a/siopv2/src/provider.rs +++ b/siopv2/src/provider.rs @@ -1,6 +1,7 @@ use std::str::FromStr; use anyhow::Result; +use jsonwebtoken::Algorithm; use oid4vc_core::{ authentication::subject::SigningSubject, authorization_request::{AuthorizationRequest, Body, ByReference, ByValue, Object}, @@ -17,23 +18,35 @@ use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; /// the user who is trying to authenticate. pub struct Provider { pub subject: SigningSubject, - pub default_subject_syntax_type: SubjectSyntaxType, + pub supported_subject_syntax_types: Vec, + pub supported_signing_algorithms: Vec, client: ClientWithMiddleware, } impl Provider { // TODO: Use ProviderBuilder instead. - pub fn new(subject: SigningSubject, default_subject_syntax_type: impl TryInto) -> Result { + pub fn new( + subject: SigningSubject, + supported_subject_syntax_types: Vec>, + supported_signing_algorithms: Vec, + ) -> Result { let retry_policy = ExponentialBackoff::builder().build_with_max_retries(5); let client = ClientBuilder::new(reqwest::Client::new()) .with(RetryTransientMiddleware::new_with_policy(retry_policy)) .build(); + Ok(Provider { subject, client, - default_subject_syntax_type: default_subject_syntax_type - .try_into() - .map_err(|_| anyhow::anyhow!("Invalid did method."))?, + supported_subject_syntax_types: supported_subject_syntax_types + .into_iter() + .map(|subject_syntax_type| { + subject_syntax_type + .try_into() + .map_err(|_| anyhow::anyhow!("Invalid did method.")) + }) + .collect::>()?, + supported_signing_algorithms, }) } @@ -80,6 +93,34 @@ impl Provider { Ok(authorization_request) } + pub async fn get_matching_signing_algorithm( + &self, + authorization_request: &AuthorizationRequest>, + ) -> Result { + let relying_party_supported_algorithms = + E::get_relying_party_supported_algorithms(&authorization_request.body.extension).await?; + + self.supported_signing_algorithms + .iter() + .find(|supported_algorithm| relying_party_supported_algorithms.contains(supported_algorithm)) + .cloned() + .ok_or(anyhow::anyhow!("No supported signing algorithms found.")) + } + + pub async fn get_matching_subject_syntax_type( + &self, + authorization_request: &AuthorizationRequest>, + ) -> Result { + let relying_party_supported_syntax_types = + E::get_relying_party_supported_syntax_types(&authorization_request.body.extension).await?; + + self.supported_subject_syntax_types + .iter() + .find(|supported_syntax_type| relying_party_supported_syntax_types.contains(supported_syntax_type)) + .cloned() + .ok_or(anyhow::anyhow!("No supported subject syntax types found.")) + } + /// Generates an [`AuthorizationResponse`] in response to an [`AuthorizationRequest`] and the user's claims. The [`AuthorizationResponse`] /// contains an [`IdToken`], which is signed by the [`Subject`] of the [`Provider`]. pub async fn generate_response( @@ -90,12 +131,16 @@ impl Provider { let redirect_uri = authorization_request.body.redirect_uri.to_string(); let state = authorization_request.body.state.clone(); + let signing_algorithm = self.get_matching_signing_algorithm(authorization_request).await?; + let subject_syntax_type = self.get_matching_subject_syntax_type(authorization_request).await?; + let jwts = E::generate_token( self.subject.clone(), &authorization_request.body.client_id, &authorization_request.body.extension, &input, - self.default_subject_syntax_type.clone(), + subject_syntax_type.clone(), + signing_algorithm, ) .await?; @@ -128,7 +173,7 @@ mod tests { let subject = TestSubject::new("did:test:123".to_string(), "key_id".to_string()).unwrap(); // Create a new provider. - let provider = Provider::new(Arc::new(subject), "did:test").unwrap(); + let provider = Provider::new(Arc::new(subject), vec!["did:test"], vec![Algorithm::EdDSA]).unwrap(); // Get a new SIOP authorization_request with response mode `direct_post` for cross-device communication. let request_url = "\ diff --git a/siopv2/src/relying_party.rs b/siopv2/src/relying_party.rs index 6fea0036..912bef9f 100644 --- a/siopv2/src/relying_party.rs +++ b/siopv2/src/relying_party.rs @@ -34,10 +34,15 @@ impl RelyingParty { pub async fn encode( &self, authorization_request: &AuthorizationRequest>, + signing_algorithm: impl TryInto, ) -> Result { jwt::encode( self.subject.clone(), - Header::new(Algorithm::EdDSA), + Header::new( + signing_algorithm + .try_into() + .map_err(|_| anyhow::anyhow!("Invalid signing algorithm."))?, + ), authorization_request, &self.default_subject_syntax_type.to_string(), ) diff --git a/siopv2/src/siopv2.rs b/siopv2/src/siopv2.rs index 72544c7d..e0db9dff 100644 --- a/siopv2/src/siopv2.rs +++ b/siopv2/src/siopv2.rs @@ -1,11 +1,17 @@ -use crate::authorization_request::{AuthorizationRequestBuilder, AuthorizationRequestParameters}; +use crate::authorization_request::{ + AuthorizationRequestBuilder, AuthorizationRequestParameters, ClientMetadataParameters, +}; use crate::claims::StandardClaimsValues; use crate::token::id_token::IdToken; use chrono::{Duration, Utc}; use jsonwebtoken::{Algorithm, Header}; +use oid4vc_core::client_metadata::ClientMetadataResource; use oid4vc_core::openid4vc_extension::{OpenID4VC, RequestHandle, ResponseHandle}; use oid4vc_core::{authorization_response::AuthorizationResponse, jwt, openid4vc_extension::Extension, Subject}; use oid4vc_core::{SubjectSyntaxType, Validator}; +use reqwest_middleware::ClientBuilder; +use reqwest_retry::policies::ExponentialBackoff; +use reqwest_retry::RetryTransientMiddleware; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -40,12 +46,20 @@ impl Extension for SIOPv2 { extension_parameters: &::Parameters, user_input: &::Input, subject_syntax_type: impl TryInto, + signing_algorithm: impl TryInto, ) -> anyhow::Result> { + let signing_algorithm = signing_algorithm + .try_into() + .map_err(|_| anyhow::anyhow!("Failed to convert the signing algorithm"))?; + let subject_syntax_type_string = subject_syntax_type .try_into() .map_err(|_| anyhow::anyhow!("Failed to convert the subject syntax type"))? .to_string(); - let subject_identifier = subject.identifier(&subject_syntax_type_string).await?; + + let subject_identifier = subject + .identifier(&subject_syntax_type_string, signing_algorithm) + .await?; let id_token = IdToken::builder() .iss(subject_identifier.clone()) @@ -60,7 +74,7 @@ impl Extension for SIOPv2 { let jwt = jwt::encode( subject.clone(), - Header::new(Algorithm::EdDSA), + Header::new(signing_algorithm), id_token, &subject_syntax_type_string, ) @@ -69,6 +83,66 @@ impl Extension for SIOPv2 { Ok(vec![jwt]) } + // TODO: combine this function with `get_relying_party_supported_syntax_types`. + async fn get_relying_party_supported_algorithms( + authorization_request: &::Parameters, + ) -> anyhow::Result> { + let client_metadata = match &authorization_request.client_metadata { + // Fetch the client metadata from the given URI. + ClientMetadataResource::ClientMetadataUri(client_metadata_uri) => { + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(5); + let client = ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + let client_metadata: ClientMetadataResource = + client.get(client_metadata_uri).send().await?.json().await?; + client_metadata + } + client_metadata => client_metadata.clone(), + }; + + match client_metadata { + ClientMetadataResource::ClientMetadataUri(_) => unreachable!(), + ClientMetadataResource::ClientMetadata { extension, .. } => { + match extension.id_token_signed_response_alg { + Some(alg) => Ok(vec![alg]), + // TODO: default to RS256 + None => Ok(vec![Algorithm::EdDSA]), + } + } + } + } + + async fn get_relying_party_supported_syntax_types( + authorization_request: &::Parameters, + ) -> anyhow::Result> { + let client_metadata = match &authorization_request.client_metadata { + // Fetch the client metadata from the given URI. + ClientMetadataResource::ClientMetadataUri(client_metadata_uri) => { + let retry_policy = ExponentialBackoff::builder().build_with_max_retries(5); + let client = ClientBuilder::new(reqwest::Client::new()) + .with(RetryTransientMiddleware::new_with_policy(retry_policy)) + .build(); + let client_metadata: ClientMetadataResource = + client.get(client_metadata_uri).send().await?.json().await?; + client_metadata + } + client_metadata => client_metadata.clone(), + }; + + match client_metadata { + ClientMetadataResource::ClientMetadataUri(_) => unreachable!(), + ClientMetadataResource::ClientMetadata { + extension: + ClientMetadataParameters { + subject_syntax_types_supported, + .. + }, + .. + } => Ok(subject_syntax_types_supported), + } + } + fn build_authorization_response( jwts: Vec, _user_input: ::Input, diff --git a/siopv2/src/test_utils.rs b/siopv2/src/test_utils.rs index e449e892..a61a12ac 100644 --- a/siopv2/src/test_utils.rs +++ b/siopv2/src/test_utils.rs @@ -3,6 +3,7 @@ use anyhow::Result; use async_trait::async_trait; use derivative::{self, Derivative}; use ed25519_dalek::{Signature, Signer, SigningKey}; +use jsonwebtoken::Algorithm; use lazy_static::lazy_static; use oid4vc_core::{authentication::sign::ExternalSign, Sign, Subject, Verify}; use rand::rngs::OsRng; @@ -32,11 +33,11 @@ impl TestSubject { #[async_trait] impl Sign for TestSubject { - async fn key_id(&self, _subject_syntax_type: &str) -> Option { + async fn key_id(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Option { Some(self.key_id.clone()) } - async fn sign(&self, message: &str, _subject_syntax_type: &str) -> Result> { + async fn sign(&self, message: &str, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result> { let signature: Signature = TEST_KEYPAIR.sign(message.as_bytes()); Ok(signature.to_bytes().to_vec()) } @@ -55,7 +56,7 @@ impl Verify for TestSubject { #[async_trait] impl Subject for TestSubject { - async fn identifier(&self, _subject_syntax_type: &str) -> Result { + async fn identifier(&self, _subject_syntax_type: &str, _algorithm: Algorithm) -> Result { Ok(self.did.to_string()) } } diff --git a/siopv2/src/token/id_token_builder.rs b/siopv2/src/token/id_token_builder.rs index 48e06295..9aea1c87 100644 --- a/siopv2/src/token/id_token_builder.rs +++ b/siopv2/src/token/id_token_builder.rs @@ -23,10 +23,13 @@ impl IdTokenBuilder { pub fn build(self) -> anyhow::Result { anyhow::ensure!(self.rfc7519_claims.iss.is_some(), "iss claim is required"); anyhow::ensure!(self.rfc7519_claims.sub.is_some(), "sub claim is required"); - anyhow::ensure!( - self.rfc7519_claims.sub.as_ref().filter(|s| s.len() <= 255).is_some(), - "sub claim MUST NOT exceed 255 ASCII characters in length" - ); + // TODO: According to https://openid.net/specs/openid-connect-core-1_0.html#IDToken, the sub claim MUST NOT + // exceed 255 ASCII characters in length. However, for `did:jwk` it can be longer so we need to figure out how + // to deal with this. + // anyhow::ensure!( + // self.rfc7519_claims.sub.as_ref().filter(|s| s.len() <= 255).is_some(), + // "sub claim MUST NOT exceed 255 ASCII characters in length" + // ); anyhow::ensure!(self.rfc7519_claims.aud.is_some(), "aud claim is required"); anyhow::ensure!(self.rfc7519_claims.exp.is_some(), "exp claim is required"); anyhow::ensure!(self.rfc7519_claims.iat.is_some(), "iat claim is required"); @@ -96,13 +99,16 @@ mod tests { .to_string() .contains("sub claim is required")); - assert!(IdTokenBuilder::new() - .iss("iss") - .sub("x".repeat(256)) - .build() - .unwrap_err() - .to_string() - .contains("sub claim MUST NOT exceed 255 ASCII characters in length")); + // TODO: According to https://openid.net/specs/openid-connect-core-1_0.html#IDToken, the sub claim MUST NOT + // exceed 255 ASCII characters in length. However, for `did:jwk` it can be longer so we need to figure out how + // to deal with this. + // assert!(IdTokenBuilder::new() + // .iss("iss") + // .sub("x".repeat(256)) + // .build() + // .unwrap_err() + // .to_string() + // .contains("sub claim MUST NOT exceed 255 ASCII characters in length")); assert!(IdTokenBuilder::new() .iss("iss")