From cae5516832c7c4e053808f443098d92b47804e09 Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 21 May 2024 15:03:08 +0200 Subject: [PATCH] feat: add support for multiple singing algorithms --- 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/jwt.rs | 15 +++-- oid4vc-core/src/openid4vc_extension.rs | 9 +++ oid4vc-core/src/test_utils.rs | 7 +- oid4vc-manager/Cargo.toml | 2 +- oid4vc-manager/src/managers/provider.rs | 4 +- oid4vc-manager/src/managers/relying_party.rs | 15 ++++- oid4vc-manager/src/methods/key_method.rs | 13 ++-- .../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 | 26 ++++++-- oid4vc-manager/tests/siopv2/implicit.rs | 27 ++++---- oid4vci/Cargo.toml | 2 +- .../credential_configurations_supported.rs | 3 +- .../credential_issuer_metadata.rs | 3 +- oid4vci/src/proof.rs | 16 ++--- oid4vci/src/wallet/mod.rs | 64 ++++++++++++++++--- oid4vp/Cargo.toml | 3 + oid4vp/src/authorization_request.rs | 12 ++-- oid4vp/src/lib.rs | 4 +- oid4vp/src/oid4vp.rs | 52 ++++++++++++++- oid4vp/src/token/vp_token_builder.rs | 10 +-- .../client_metadata/client_client_id_did.json | 2 +- siopv2/Cargo.toml | 2 +- siopv2/src/authorization_request.rs | 19 ++++-- siopv2/src/provider.rs | 21 +++++- siopv2/src/relying_party.rs | 7 +- siopv2/src/siopv2.rs | 48 +++++++++++++- siopv2/src/test_utils.rs | 7 +- siopv2/src/token/id_token_builder.rs | 26 ++++---- 39 files changed, 356 insertions(+), 117 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/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..32f8807e 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,19 @@ 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 build_authorization_response( _jwts: Vec, _user_input: ::Input, 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..5f07f25b 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, @@ -18,9 +19,10 @@ impl ProviderManager { pub fn new( subject: Arc, default_subject_syntax_type: impl TryInto, + supported_signing_algorithms: Vec, ) -> Result { Ok(Self { - provider: Provider::new(subject, default_subject_syntax_type)?, + provider: Provider::new(subject, default_subject_syntax_type, supported_signing_algorithms)?, }) } 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..d20d4011 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,7 @@ 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), "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 +155,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..53d8344b 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), "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..976362dd 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), "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..fc62f91f 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,8 +14,9 @@ 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; @@ -77,31 +79,43 @@ 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(), + }, + }) .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, "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..2782df19 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,6 +139,7 @@ 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), }, }) .claims( @@ -182,7 +185,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), 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..e37ad4e1 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, }; @@ -11,6 +12,7 @@ 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 jsonwebtoken::Algorithm; use oid4vc_core::authentication::subject::SigningSubject; use oid4vc_core::SubjectSyntaxType; use reqwest::Url; @@ -26,6 +28,7 @@ where pub subject: SigningSubject, pub default_subject_syntax_type: SubjectSyntaxType, pub client: ClientWithMiddleware, + pub proof_signing_alg_values_supported: Vec, phantom: std::marker::PhantomData, } @@ -33,6 +36,7 @@ impl Wallet { pub fn new( subject: SigningSubject, default_subject_syntax_type: impl TryInto, + 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()) @@ -44,6 +48,7 @@ impl Wallet { .try_into() .map_err(|_| anyhow::anyhow!("Invalid did method"))?, client, + proof_signing_alg_values_supported, phantom: std::marker::PhantomData, }) } @@ -115,7 +120,10 @@ impl Wallet { response_type: "code".to_string(), client_id: self .subject - .identifier(&self.default_subject_syntax_type.to_string()) + .identifier( + &self.default_subject_syntax_type.to_string(), + self.proof_signing_alg_values_supported[0], + ) .await?, redirect_uri: None, scope: None, @@ -144,22 +152,39 @@ impl Wallet { &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 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; + + let signing_algorithm = self + .proof_signing_alg_values_supported + .iter() + .find(|supported_algorithm| { + credential_issuer_proof_signing_alg_values_supported.contains(supported_algorithm) + }) + .ok_or(anyhow::anyhow!("No supported signing algorithm found."))?; let credential_request = CredentialRequest { credential_format, proof: Some( KeyProofType::builder() .proof_type(ProofType::Jwt) + // FIX THIS + .algorithm(*signing_algorithm) .signer(self.subject.clone()) .iss( self.subject - .identifier(&self.default_subject_syntax_type.to_string()) + .identifier(&self.default_subject_syntax_type.to_string(), *signing_algorithm) .await?, ) .aud(credential_issuer_metadata.credential_issuer) .iat(1571324800) - .exp(9999999999i64) // TODO: so is this REQUIRED or OPTIONAL? .nonce( token_response @@ -189,20 +214,39 @@ impl Wallet { &self, credential_issuer_metadata: CredentialIssuerMetadata, token_response: &TokenResponse, - credential_formats: Vec, + credential_configurations: &[CredentialConfigurationsSupportedObject], ) -> Result { + // FIX THIS: This assumes that for all credentials the same Proof Type is supported. + let credential_issuer_proof_signing_alg_values_supported = &credential_configurations + .first() + .ok_or(anyhow::anyhow!("No credential configurations found."))? + .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; + + let signing_algorithm = self + .proof_signing_alg_values_supported + .iter() + .find(|supported_algorithm| { + credential_issuer_proof_signing_alg_values_supported.contains(supported_algorithm) + }) + .ok_or(anyhow::anyhow!("No supported signing algorithm found."))?; + 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(&self.default_subject_syntax_type.to_string(), *signing_algorithm) .await?, ) .aud(credential_issuer_metadata.credential_issuer) .iat(1571324800) - .exp(9999999999i64) // TODO: so is this REQUIRED or OPTIONAL? .nonce( token_response @@ -217,10 +261,10 @@ impl Wallet { ); 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..68c96744 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, 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..eda06eb6 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,10 +12,14 @@ 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::sync::Arc; @@ -47,12 +54,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 +81,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 +89,38 @@ impl Extension for OID4VP { Ok(vec![jwt]) } + async fn get_relying_party_supported_algorithms( + 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(), + }; + + // 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. + match client_metadata { + 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.")), + } + } + 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..69307563 100644 --- a/oid4vp/src/token/vp_token_builder.rs +++ b/oid4vp/src/token/vp_token_builder.rs @@ -19,10 +19,12 @@ 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. + // 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..4c0c7ac8 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}, @@ -18,12 +19,17 @@ use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; pub struct Provider { pub subject: SigningSubject, pub default_subject_syntax_type: SubjectSyntaxType, + pub supported_sigining_alogrithms: 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, + default_subject_syntax_type: impl TryInto, + supported_sigining_alogrithms: 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)) @@ -34,6 +40,7 @@ impl Provider { default_subject_syntax_type: default_subject_syntax_type .try_into() .map_err(|_| anyhow::anyhow!("Invalid did method."))?, + supported_sigining_alogrithms, }) } @@ -90,12 +97,22 @@ impl Provider { let redirect_uri = authorization_request.body.redirect_uri.to_string(); let state = authorization_request.body.state.clone(); + let relying_party_supported_algorithms = + E::get_relying_party_supported_algorithms(&authorization_request.body.extension).await?; + + let signing_algorithm = self + .supported_sigining_alogrithms + .iter() + .find(|supported_algorithm| relying_party_supported_algorithms.contains(supported_algorithm)) + .ok_or(anyhow::anyhow!("No supported signing algorithms found."))?; + let jwts = E::generate_token( self.subject.clone(), &authorization_request.body.client_id, &authorization_request.body.extension, &input, self.default_subject_syntax_type.clone(), + *signing_algorithm, ) .await?; @@ -128,7 +145,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), "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..b4070517 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,34 @@ impl Extension for SIOPv2 { Ok(vec![jwt]) } + async fn get_relying_party_supported_algorithms( + 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 { extension, .. } => { + match extension.id_token_signed_response_alg { + Some(alg) => Ok(vec![alg]), + // TODO: default to RS256 + None => Ok(vec![Algorithm::EdDSA]), + } + } + } + } + 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..1ca35fb5 100644 --- a/siopv2/src/token/id_token_builder.rs +++ b/siopv2/src/token/id_token_builder.rs @@ -23,10 +23,12 @@ 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. + // 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 +98,15 @@ 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. + // 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")