From 3c878726395d59eef0948e7003e875d97fe8e371 Mon Sep 17 00:00:00 2001 From: nanderstabel Date: Mon, 2 Dec 2024 23:10:40 +0100 Subject: [PATCH 1/3] feat: implement SD-JWT --- Cargo.toml | 7 +- .../src/presentation_definition.rs | 30 +++++++++ oid4vc-manager/src/managers/presentation.rs | 62 +++++++++++++----- oid4vc-manager/tests/oid4vp/implicit.rs | 6 +- .../ietf_sd_jwt_vc/mod.rs | 7 ++ oid4vci/src/credential_format_profiles/mod.rs | 6 ++ .../jwt_vc_json_ld.rs | 2 +- oid4vp/src/oid4vp.rs | 64 +++++++++++++------ 8 files changed, 142 insertions(+), 42 deletions(-) create mode 100644 oid4vci/src/credential_format_profiles/ietf_sd_jwt_vc/mod.rs diff --git a/Cargo.toml b/Cargo.toml index b3d0184d..8f224b76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,8 +26,11 @@ repository = "https://github.com/impierce/openid4vc" [workspace.dependencies] chrono = "0.4" getset = "0.1" -identity_core = "1.2.0" -identity_credential = { version = "1.2.0", default-features = false, features = ["validator", "credential", "presentation"] } +# TODO: Set identity.rs dependencies back to the official repository once the following PR is merged: https://github.com/iotaledger/identity.rs/pull/1413 +# identity_core = { git = "https://github.com/iotaledger/identity.rs", branch = "feat/sd-jwt-vc" } +# identity_credential = { git = "https://github.com/iotaledger/identity.rs", branch = "feat/sd-jwt-vc", default-features = false, features = ["validator", "credential", "presentation"] } +identity_core = { git = "https://github.com/impierce/identity.rs", rev = "309c399" } +identity_credential = { git = "https://github.com/impierce/identity.rs", rev = "309c399", default-features = false, features = ["validator", "credential", "presentation", "sd-jwt-vc"] } is_empty = "0.2" jsonwebtoken = "9.3" monostate = "0.1" diff --git a/dif-presentation-exchange/src/presentation_definition.rs b/dif-presentation-exchange/src/presentation_definition.rs index ef62b7a0..dfea13ad 100644 --- a/dif-presentation-exchange/src/presentation_definition.rs +++ b/dif-presentation-exchange/src/presentation_definition.rs @@ -54,6 +54,8 @@ pub enum ClaimFormatDesignation { AcVc, AcVp, MsoMdoc, + #[serde(rename = "vc+sd-jwt")] + VcSdJwt, } #[allow(dead_code)] @@ -62,6 +64,13 @@ pub enum ClaimFormatDesignation { pub enum ClaimFormatProperty { Alg(Vec), ProofType(Vec), + #[serde(untagged)] + SdJwt { + #[serde(rename = "sd-jwt_alg_values", default, skip_serializing_if = "Vec::is_empty")] + sd_jwt_alg_values: Vec, + #[serde(rename = "kb-jwt_alg_values", default, skip_serializing_if = "Vec::is_empty")] + kb_jwt_alg_values: Vec, + }, } #[allow(dead_code)] @@ -387,4 +396,25 @@ mod tests { json_example::("../oid4vp/tests/examples/request/vp_token_type_only.json") ); } + + #[test] + fn test_claim_format_property() { + assert_eq!( + ClaimFormatProperty::Alg(vec![Algorithm::EdDSA, Algorithm::ES256]), + serde_json::from_str(r#"{"alg":["EdDSA","ES256"]}"#).unwrap() + ); + + assert_eq!( + ClaimFormatProperty::ProofType(vec!["JsonWebSignature2020".to_string()]), + serde_json::from_str(r#"{"proof_type":["JsonWebSignature2020"]}"#).unwrap() + ); + + assert_eq!( + ClaimFormatProperty::SdJwt { + sd_jwt_alg_values: vec![Algorithm::EdDSA], + kb_jwt_alg_values: vec![Algorithm::ES256], + }, + serde_json::from_str(r#"{"sd-jwt_alg_values":["EdDSA"],"kb-jwt_alg_values":["ES256"]}"#).unwrap() + ); + } } diff --git a/oid4vc-manager/src/managers/presentation.rs b/oid4vc-manager/src/managers/presentation.rs index acd6b595..6f2681dd 100644 --- a/oid4vc-manager/src/managers/presentation.rs +++ b/oid4vc-manager/src/managers/presentation.rs @@ -6,7 +6,7 @@ use oid4vp::{ /// Takes a [`PresentationDefinition`] and a credential and creates a [`PresentationSubmission`] from it if the /// credential meets the requirements. -// TODO: make VP/VC fromat agnostic. In current form only jwt_vp_json + jwt_vc_json are supported. +// TODO: make VP/VC format agnostic. In current form only jwt_vp_json + jwt_vc_json are supported. pub fn create_presentation_submission( presentation_definition: &PresentationDefinition, credentials: &[serde_json::Value], @@ -17,23 +17,51 @@ pub fn create_presentation_submission( .input_descriptors() .iter() .enumerate() - .map(|(index, input_descriptor)| { - credentials - .iter() - .find_map(|credential| { - evaluate_input(input_descriptor, credential).then_some(InputDescriptorMappingObject { - id: input_descriptor.id().clone(), - format: ClaimFormatDesignation::JwtVpJson, - path: "$".to_string(), - path_nested: Some(PathNested { - id: None, - path: format!("$.vp.verifiableCredential[{}]", index), - format: ClaimFormatDesignation::JwtVcJson, - path_nested: None, - }), - }) + .filter_map(|(index, input_descriptor)| { + credentials.iter().find_map(|credential| { + evaluate_input(input_descriptor, credential).then_some(InputDescriptorMappingObject { + id: input_descriptor.id().clone(), + format: ClaimFormatDesignation::JwtVpJson, + path: "$".to_string(), + path_nested: Some(PathNested { + id: None, + path: format!("$.vp.verifiableCredential[{}]", index), + format: ClaimFormatDesignation::JwtVcJson, + path_nested: None, + }), }) - .unwrap() + }) + }) + .collect::>(); + Ok(PresentationSubmission { + id, + definition_id, + descriptor_map, + }) +} + +// Creates a `PresentationSubmission` for a vc-sd-jwt presentation. +// TODO:remove this function and make sure that `create_presentation_submission` can generate submissions regardless of +// the VP/VC format. +pub fn create_sd_jwt_presentation_submission( + presentation_definition: &PresentationDefinition, + credentials: &[serde_json::Value], +) -> Result { + let id = "Submission ID".to_string(); + let definition_id = presentation_definition.id().clone(); + let descriptor_map = presentation_definition + .input_descriptors() + .iter() + .enumerate() + .filter_map(|(_index, input_descriptor)| { + credentials.iter().find_map(|credential| { + evaluate_input(input_descriptor, credential).then_some(InputDescriptorMappingObject { + id: input_descriptor.id().clone(), + format: ClaimFormatDesignation::VcSdJwt, + path: "$".to_string(), + path_nested: None, + }) + }) }) .collect::>(); Ok(PresentationSubmission { diff --git a/oid4vc-manager/tests/oid4vp/implicit.rs b/oid4vc-manager/tests/oid4vp/implicit.rs index c0a9f9f6..0e62ee90 100644 --- a/oid4vc-manager/tests/oid4vp/implicit.rs +++ b/oid4vc-manager/tests/oid4vp/implicit.rs @@ -15,7 +15,7 @@ use oid4vc_manager::{ use oid4vci::VerifiableCredentialJwt; use oid4vp::{ authorization_request::ClientMetadataParameters, - oid4vp::{AuthorizationResponseInput, OID4VP}, + oid4vp::{AuthorizationResponseInput, PresentationInputType, OID4VP}, ClaimFormatDesignation, ClaimFormatProperty, PresentationDefinition, }; use serde_json::json; @@ -176,12 +176,14 @@ async fn test_implicit_flow() { .build() .unwrap(); + let verifiable_presentation_input = PresentationInputType::Unsigned(verifiable_presentation); + // Generate the authorization_response. It will include both an IdToken and a VpToken. let authorization_response: AuthorizationResponse = provider_manager .generate_response( &authorization_request, AuthorizationResponseInput { - verifiable_presentation, + verifiable_presentation_input, presentation_submission, }, ) diff --git a/oid4vci/src/credential_format_profiles/ietf_sd_jwt_vc/mod.rs b/oid4vci/src/credential_format_profiles/ietf_sd_jwt_vc/mod.rs new file mode 100644 index 00000000..2131cd2e --- /dev/null +++ b/oid4vci/src/credential_format_profiles/ietf_sd_jwt_vc/mod.rs @@ -0,0 +1,7 @@ +use crate::credential_format; + +credential_format!("vc+sd-jwt", VcSdJwt, { + vct: String, + claims: Option, + order: Option> +}); diff --git a/oid4vci/src/credential_format_profiles/mod.rs b/oid4vci/src/credential_format_profiles/mod.rs index ea064f4b..547d95d6 100644 --- a/oid4vci/src/credential_format_profiles/mod.rs +++ b/oid4vci/src/credential_format_profiles/mod.rs @@ -1,3 +1,4 @@ +pub mod ietf_sd_jwt_vc; pub mod iso_mdl; pub mod w3c_verifiable_credentials; @@ -8,6 +9,7 @@ use self::{ jwt_vc_json::JwtVcJson, jwt_vc_json_ld::JwtVcJsonLd, ldp_vc::LdpVc, CredentialSubject, }, }; +use ietf_sd_jwt_vc::VcSdJwt; use serde::{Deserialize, Serialize}; use serde_with::skip_serializing_none; @@ -107,6 +109,8 @@ where LdpVc(C::Container), #[serde(rename = "mso_mdoc")] MsoMdoc(C::Container), + #[serde(rename = "vc+sd-jwt")] + VcSdJwt(C::Container), #[default] Unknown, } @@ -132,6 +136,7 @@ where CredentialFormats::JwtVcJsonLd(_) => CredentialFormats::JwtVcJsonLd(()), CredentialFormats::LdpVc(_) => CredentialFormats::LdpVc(()), CredentialFormats::MsoMdoc(_) => CredentialFormats::MsoMdoc(()), + CredentialFormats::VcSdJwt(_) => CredentialFormats::VcSdJwt(()), CredentialFormats::Unknown => CredentialFormats::Unknown, } } @@ -144,6 +149,7 @@ impl CredentialFormats { CredentialFormats::JwtVcJsonLd(credential) => Ok(&credential.credential), CredentialFormats::LdpVc(credential) => Ok(&credential.credential), CredentialFormats::MsoMdoc(credential) => Ok(&credential.credential), + CredentialFormats::VcSdJwt(credential) => Ok(&credential.credential), CredentialFormats::Unknown => Err(anyhow::anyhow!("Unknown credential format")), } } diff --git a/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json_ld.rs b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json_ld.rs index 335f5f75..54512c36 100644 --- a/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json_ld.rs +++ b/oid4vci/src/credential_format_profiles/w3c_verifiable_credentials/jwt_vc_json_ld.rs @@ -6,7 +6,7 @@ use super::CredentialSubject; credential_format!("jwt_vc_json-ld", JwtVcJsonLd, { credential_definition: CredentialDefinition, - order: Option + order: Option> }); #[skip_serializing_none] diff --git a/oid4vp/src/oid4vp.rs b/oid4vp/src/oid4vp.rs index e890f85e..72c2b5b3 100644 --- a/oid4vp/src/oid4vp.rs +++ b/oid4vp/src/oid4vp.rs @@ -69,25 +69,36 @@ impl Extension for OID4VP { .identifier(&subject_syntax_type_string, signing_algorithm) .await?; - let vp_token = VpToken::builder() - .iss(subject_identifier.clone()) - .sub(subject_identifier) - .aud(client_id) - .nonce(extension_parameters.nonce.to_owned()) - // TODO: make this configurable. - .exp((Utc::now() + Duration::minutes(10)).timestamp()) - .iat((Utc::now()).timestamp()) - .verifiable_presentation(user_input.verifiable_presentation.clone()) - .build()?; - - let jwt = jwt::encode( - subject.clone(), - Header::new(signing_algorithm), - vp_token, - &subject_syntax_type_string, - ) - .await?; - Ok(vec![jwt]) + let mut jwts = vec![]; + match &user_input.verifiable_presentation_input { + PresentationInputType::Unsigned(verifiable_presentation) => { + let vp_token = VpToken::builder() + .iss(subject_identifier.clone()) + .sub(subject_identifier.clone()) + .aud(client_id) + .nonce(extension_parameters.nonce.to_owned()) + // TODO: make this configurable. + .exp((Utc::now() + Duration::minutes(10)).timestamp()) + .iat((Utc::now()).timestamp()) + .verifiable_presentation(verifiable_presentation.clone()) + .build()?; + + let jwt = jwt::encode( + subject.clone(), + Header::new(signing_algorithm), + vp_token, + &subject_syntax_type_string, + ) + .await?; + + jwts.push(jwt); + } + PresentationInputType::Signed(jwt) => { + jwts.push(jwt.to_owned()); + } + } + + Ok(jwts) } // TODO: combine this function with `get_relying_party_supported_syntax_types`. @@ -117,10 +128,16 @@ impl Extension for OID4VP { ClientMetadataResource::ClientMetadata { extension, .. } => extension .vp_formats .get(&ClaimFormatDesignation::JwtVcJson) + .or_else(|| extension.vp_formats.get(&ClaimFormatDesignation::VcSdJwt)) .and_then(|claim_format_property| match claim_format_property { ClaimFormatProperty::Alg(algs) => Some(algs.clone()), // TODO: implement `ProofType`. ClaimFormatProperty::ProofType(_) => None, + ClaimFormatProperty::SdJwt { + sd_jwt_alg_values, + // TODO: implement Key Binding + kb_jwt_alg_values: _kb_jwt_alg_values, + } => Some(sd_jwt_alg_values.clone()), }) .ok_or(anyhow::anyhow!("No supported algorithms found.")), } @@ -218,7 +235,14 @@ pub struct AuthorizationResponseParameters { pub oid4vp_parameters: Oid4vpParams, } +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] pub struct AuthorizationResponseInput { - pub verifiable_presentation: Presentation, + pub verifiable_presentation_input: PresentationInputType, pub presentation_submission: PresentationSubmission, } + +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub enum PresentationInputType { + Unsigned(Presentation), + Signed(String), +} From d56dd778c9af85c44baf16ff1c899482a964d01f Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Tue, 3 Dec 2024 20:41:17 +0100 Subject: [PATCH 2/3] refactor: remove redundant `enumerate` --- oid4vc-manager/src/managers/presentation.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/oid4vc-manager/src/managers/presentation.rs b/oid4vc-manager/src/managers/presentation.rs index 6f2681dd..1614967e 100644 --- a/oid4vc-manager/src/managers/presentation.rs +++ b/oid4vc-manager/src/managers/presentation.rs @@ -52,8 +52,7 @@ pub fn create_sd_jwt_presentation_submission( let descriptor_map = presentation_definition .input_descriptors() .iter() - .enumerate() - .filter_map(|(_index, input_descriptor)| { + .filter_map(|input_descriptor| { credentials.iter().find_map(|credential| { evaluate_input(input_descriptor, credential).then_some(InputDescriptorMappingObject { id: input_descriptor.id().clone(), From 22b02b0da5921a45420fa46d38480dc62df80f8c Mon Sep 17 00:00:00 2001 From: Nander Stabel Date: Fri, 17 Jan 2025 14:41:40 +0100 Subject: [PATCH 3/3] fix: set identity.rs dependencies back to original --- Cargo.toml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8f224b76..ba2a7050 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,10 +27,8 @@ repository = "https://github.com/impierce/openid4vc" chrono = "0.4" getset = "0.1" # TODO: Set identity.rs dependencies back to the official repository once the following PR is merged: https://github.com/iotaledger/identity.rs/pull/1413 -# identity_core = { git = "https://github.com/iotaledger/identity.rs", branch = "feat/sd-jwt-vc" } -# identity_credential = { git = "https://github.com/iotaledger/identity.rs", branch = "feat/sd-jwt-vc", default-features = false, features = ["validator", "credential", "presentation"] } -identity_core = { git = "https://github.com/impierce/identity.rs", rev = "309c399" } -identity_credential = { git = "https://github.com/impierce/identity.rs", rev = "309c399", default-features = false, features = ["validator", "credential", "presentation", "sd-jwt-vc"] } +identity_core = { git = "https://github.com/iotaledger/identity.rs", branch = "feat/sd-jwt-vc" } +identity_credential = { git = "https://github.com/iotaledger/identity.rs", branch = "feat/sd-jwt-vc", default-features = false, features = ["validator", "credential", "presentation", "sd-jwt-vc"] } is_empty = "0.2" jsonwebtoken = "9.3" monostate = "0.1"