From 55849bca77670c34485e96483178e9555855b4ad Mon Sep 17 00:00:00 2001 From: Ross Schulman Date: Tue, 19 Nov 2024 10:09:26 -0500 Subject: [PATCH 1/4] Add VC SD-JWT profile --- src/credential.rs | 4 + src/custom/mod.rs | 37 ++++ src/custom/profiles/mod.rs | 185 ++++++++++++++++++ .../vc_sd_jwt/authorization_detail.rs | 71 +++++++ .../vc_sd_jwt/credential_configuration.rs | 122 ++++++++++++ .../profiles/vc_sd_jwt/credential_request.rs | 98 ++++++++++ .../profiles/vc_sd_jwt/credential_response.rs | 36 ++++ src/custom/profiles/vc_sd_jwt/mod.rs | 34 ++++ src/lib.rs | 1 + 9 files changed, 588 insertions(+) create mode 100644 src/custom/mod.rs create mode 100644 src/custom/profiles/mod.rs create mode 100644 src/custom/profiles/vc_sd_jwt/authorization_detail.rs create mode 100644 src/custom/profiles/vc_sd_jwt/credential_configuration.rs create mode 100644 src/custom/profiles/vc_sd_jwt/credential_request.rs create mode 100644 src/custom/profiles/vc_sd_jwt/credential_response.rs create mode 100644 src/custom/profiles/vc_sd_jwt/mod.rs diff --git a/src/credential.rs b/src/credential.rs index 6eaee8d..fd2a1d6 100644 --- a/src/credential.rs +++ b/src/credential.rs @@ -332,7 +332,9 @@ where { #[serde(flatten, bound = "CR: CredentialResponseProfile")] response_kind: ResponseEnum, + #[serde(skip_serializing_if = "Option::is_none")] c_nonce: Option, + #[serde(skip_serializing_if = "Option::is_none")] c_nonce_expires_in: Option, } @@ -416,7 +418,9 @@ where { #[serde(bound = "CR: CredentialResponseProfile")] credential_responses: Vec>, + #[serde(skip_serializing_if = "Option::is_none")] c_nonce: Option, + #[serde(skip_serializing_if = "Option::is_none")] c_nonce_expires_in: Option, } diff --git a/src/custom/mod.rs b/src/custom/mod.rs new file mode 100644 index 0000000..def98d2 --- /dev/null +++ b/src/custom/mod.rs @@ -0,0 +1,37 @@ +pub mod profiles; + +pub mod metadata { + use crate::metadata; + + use super::profiles::CustomProfilesCredentialConfiguration; + + pub type CredentialIssuerMetadata = + metadata::CredentialIssuerMetadata; +} + +pub mod credential { + use crate::credential; + + use super::profiles::CustomProfilesCredentialRequest; + + pub type Request = credential::Request; + pub type BatchRequest = credential::BatchRequest; +} + +pub mod authorization { + use crate::authorization; + + use super::profiles::CustomProfilesAuthorizationDetailsObject; + + pub type AuthorizationDetailsObject = + authorization::AuthorizationDetailsObject; +} + +pub mod client { + + use crate::client; + + use super::profiles::CustomProfiles; + + pub type Client = client::Client; +} diff --git a/src/custom/profiles/mod.rs b/src/custom/profiles/mod.rs new file mode 100644 index 0000000..f0089e3 --- /dev/null +++ b/src/custom/profiles/mod.rs @@ -0,0 +1,185 @@ +use std::{collections::HashMap, fmt::Debug}; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::{ + profiles::{ + AuthorizationDetailsObjectProfile, CredentialConfigurationProfile, + CredentialRequestProfile, CredentialResponseProfile, Profile, + }, + types::{ClaimValueType, CredentialConfigurationId, LanguageTag}, +}; + +pub mod vc_sd_jwt; + +pub struct CustomProfiles; +impl Profile for CustomProfiles { + type CredentialConfiguration = CustomProfilesCredentialConfiguration; + type AuthorizationDetailsObject = CustomProfilesAuthorizationDetailsObject; + type CredentialRequest = CustomProfilesCredentialRequest; + type CredentialResponse = CustomProfilesCredentialResponse; +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum CustomProfilesCredentialConfiguration { + VcSdJwt(vc_sd_jwt::CredentialConfiguration), +} + +impl CredentialConfigurationProfile for CustomProfilesCredentialConfiguration {} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum CustomProfilesAuthorizationDetailsObject { + WithFormat { + #[serde(flatten)] + inner: AuthorizationDetailsObjectWithFormat, + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_identifier" + )] + _credential_identifier: (), + }, + WithIdAndUnresolvedProfile { + credential_configuration_id: CredentialConfigurationId, + #[serde(flatten)] + inner: HashMap, + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "format" + )] + _format: (), + }, + #[serde(skip_deserializing)] + WithId { + credential_configuration_id: CredentialConfigurationId, + #[serde(flatten)] + inner: AuthorizationDetailsObjectWithCredentialConfigurationId, + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "format" + )] + _format: (), + }, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum AuthorizationDetailsObjectWithFormat { + VcSdJwt(vc_sd_jwt::AuthorizationDetailsObjectWithFormat), +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum AuthorizationDetailsObjectWithCredentialConfigurationId { + VcSdJwt(vc_sd_jwt::AuthorizationDetailsObject), +} + +impl AuthorizationDetailsObjectProfile for CustomProfilesAuthorizationDetailsObject {} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum CustomProfilesCredentialRequest { + WithFormat { + #[serde(flatten)] + inner: CredentialRequestWithFormat, + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "credential_identifier" + )] + _credential_identifier: (), + }, + WithIdAndUnresolvedProfile { + credential_identifier: CredentialConfigurationId, + #[serde(flatten)] + inner: HashMap, + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "format" + )] + _format: (), + }, + #[serde(skip_deserializing)] + WithId { + credential_identifier: CredentialConfigurationId, + #[serde(flatten)] + inner: CredentialRequestWithCredentialIdentifier, + #[serde( + default, + skip_serializing, + deserialize_with = "crate::deny_field::deny_field", + rename = "format" + )] + _format: (), + }, +} + +impl CredentialRequestProfile for CustomProfilesCredentialRequest { + type Response = CustomProfilesCredentialResponse; +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum CredentialRequestWithFormat { + VcSdJwt(vc_sd_jwt::CredentialRequestWithFormat), +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum CredentialRequestWithCredentialIdentifier { + VcSdJwt(vc_sd_jwt::CredentialRequest), +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CustomProfilesCredentialResponse; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum CustomProfilesCredentialResponseType { + VcSdJwt(::Type), +} + +impl CredentialResponseProfile for CustomProfilesCredentialResponse { + type Type = CustomProfilesCredentialResponseType; +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct AuthorizationDetailsObjectClaim { + #[serde(default, skip_serializing_if = "is_false")] + mandatory: bool, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct CredentialConfigurationClaim { + #[serde(default, skip_serializing_if = "is_false")] + mandatory: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + value_type: Option, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + display: Vec, +} + +fn is_false(b: &bool) -> bool { + !b +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct ClaimDisplay { + #[serde(default, skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + locale: Option, + #[serde(flatten)] + additional_fields: HashMap, +} diff --git a/src/custom/profiles/vc_sd_jwt/authorization_detail.rs b/src/custom/profiles/vc_sd_jwt/authorization_detail.rs new file mode 100644 index 0000000..2f1be5e --- /dev/null +++ b/src/custom/profiles/vc_sd_jwt/authorization_detail.rs @@ -0,0 +1,71 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + core::profiles::CredentialConfigurationClaim, profiles::AuthorizationDetailsObjectProfile, +}; + +use super::{Claims, Format}; + +#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize)] +pub struct AuthorizationDetailsObjectWithFormat { + format: Format, + vct: String, + #[serde(skip_serializing_if = "Option::is_none")] + claims: Option>, +} + +impl AuthorizationDetailsObjectWithFormat { + field_getters_setters![ + pub self [self] ["VC SD-JWT authorization detail value"] { + set_vct -> vct[String], + set_claims -> claims[Option>], + } + ]; +} + +impl AuthorizationDetailsObjectProfile for AuthorizationDetailsObjectWithFormat {} + +#[derive(Clone, Debug, Deserialize, Default, PartialEq, Serialize)] +pub struct AuthorizationDetailsObject { + vct: String, + claims: Option>, +} + +impl AuthorizationDetailsObject { + field_getters_setters![ + pub self [self] ["VC SD-JWT authorization detail value"] { + set_vct -> vct[String], + set_claims -> claims[Option>], + } + ]; +} + +impl AuthorizationDetailsObjectProfile for AuthorizationDetailsObject {} + +#[cfg(test)] +mod test { + use serde_json::json; + + use crate::authorization::AuthorizationDetailsObject; + + #[test] + fn roundtrip_with_format() { + let expected_json = json!( + { + "type": "openid_credential", + "format": "vc+sd-jwt", + "vct": "SD_JWT_VC_example_in_OpenID4VCI" + } + ); + + let authorization_detail: AuthorizationDetailsObject< + super::AuthorizationDetailsObjectWithFormat, + > = serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str( + &serde_json::to_string(&expected_json).unwrap(), + )) + .unwrap(); + + let roundtripped = serde_json::to_value(authorization_detail).unwrap(); + assert_json_diff::assert_json_eq!(expected_json, roundtripped) + } +} diff --git a/src/custom/profiles/vc_sd_jwt/credential_configuration.rs b/src/custom/profiles/vc_sd_jwt/credential_configuration.rs new file mode 100644 index 0000000..b0262de --- /dev/null +++ b/src/custom/profiles/vc_sd_jwt/credential_configuration.rs @@ -0,0 +1,122 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ + core::profiles::CredentialConfigurationClaim, profiles::CredentialConfigurationProfile, +}; + +use super::{Claims, Format}; + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub struct CredentialConfiguration { + format: Format, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + credential_signing_alg_values_supported: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + claims: Option>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + order: Vec, + vct: String, +} + +impl CredentialConfiguration { + field_getters_setters![ + pub self [self] ["VC SD-JWT metadata value"] { + set_credential_signing_alg_values_supported -> credential_signing_alg_values_supported[Vec], + set_order -> order[Vec], + set_vct -> vct[String], + set_claims -> claims[Option>], + } + ]; +} + +impl CredentialConfigurationProfile for CredentialConfiguration {} + +#[cfg(test)] +mod test { + use serde_json::json; + + use crate::metadata::credential_issuer::CredentialConfiguration; + + #[test] + fn roundtrip() { + let expected_json = json!( + { + "$key$": "SD_JWT_VC_example_in_OpenID4VCI", + "format": "vc+sd-jwt", + "scope": "SD_JWT_VC_example_in_OpenID4VCI", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "credential_signing_alg_values_supported": [ + "ES256" + ], + "display": [ + { + "name": "IdentityCredential", + "logo": { + "uri": "https://university.example.edu/public/logo.png", + "alt_text": "a square logo of a university" + }, + "locale": "en-US", + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "ES256" + ] + } + }, + "vct": "SD_JWT_VC_example_in_OpenID4VCI", + "claims": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + }, + { + "name": "Vorname", + "locale": "de-DE" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + }, + { + "name": "Nachname", + "locale": "de-DE" + } + ] + }, + "email": {}, + "phone_number": {}, + "address": { + "street_address": {}, + "locality": {}, + "region": {}, + "country": {} + }, + "birthdate": {}, + "is_over_18": {}, + "is_over_21": {}, + "is_over_65": {} + } + } + ); + let credential_configuration: CredentialConfiguration = + serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str( + &serde_json::to_string(&expected_json).unwrap(), + )) + .unwrap(); + + let roundtripped = serde_json::to_value(credential_configuration).unwrap(); + assert_json_diff::assert_json_eq!(expected_json, roundtripped) + } +} diff --git a/src/custom/profiles/vc_sd_jwt/credential_request.rs b/src/custom/profiles/vc_sd_jwt/credential_request.rs new file mode 100644 index 0000000..ac73918 --- /dev/null +++ b/src/custom/profiles/vc_sd_jwt/credential_request.rs @@ -0,0 +1,98 @@ +use serde::{Deserialize, Serialize}; + +use crate::{core::profiles::CredentialConfigurationClaim, profiles::CredentialRequestProfile}; + +use super::{Claims, CredentialResponse, Format}; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct CredentialRequestWithFormat { + format: Format, + vct: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + claims: Option>, +} + +impl CredentialRequestWithFormat { + pub fn new(vct: String, claims: Claims) -> Self { + Self { + format: Format::default(), + vct, + claims: Some(claims), + } + } + field_getters_setters![ + pub self [self] ["VC SD-JWT request value"] { + set_vct -> vct[String], + set_claims -> claims[Option>], + } + ]; +} + +impl CredentialRequestProfile for CredentialRequestWithFormat { + type Response = CredentialResponse; +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +pub struct CredentialRequest { + vct: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + claims: Option>, +} + +impl Default for CredentialRequest { + fn default() -> Self { + Self { + vct: String::new(), + claims: Default::default(), + } + } +} + +impl CredentialRequest { + pub fn new(vct: String, claims: Claims) -> Self { + Self { + vct, + claims: Some(claims), + } + } + field_getters_setters![ + pub self [self] ["VC SD-JWT request value"] { + set_vct -> vct[String], + set_claims -> claims[Option>], + } + ]; +} + +impl CredentialRequestProfile for CredentialRequest { + type Response = CredentialResponse; +} + +#[cfg(test)] +mod test { + use serde_json::json; + + use crate::credential::Request; + + #[test] + fn roundtrip_with_format() { + let expected_json = json!( + { + "format": "vc+sd-jwt", + "vct": "SD_JWT_VC_example_in_OpenID4VCI", + "proof": { + "proof_type": "jwt", + "jwt": "eyJ0eXAiOiJvcGVuaWQ0dmNpLXByb29mK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiblVXQW9BdjNYWml0aDhFN2kxOU9kYXhPTFlGT3dNLVoyRXVNMDJUaXJUNCIsInkiOiJIc2tIVThCalVpMVU5WHFpN1N3bWo4Z3dBS18weGtjRGpFV183MVNvc0VZIn19.eyJhdWQiOiJodHRwczovL2NyZWRlbnRpYWwtaXNzdWVyLmV4YW1wbGUuY29tIiwiaWF0IjoxNzAxOTYwNDQ0LCJub25jZSI6IkxhclJHU2JtVVBZdFJZTzZCUTR5bjgifQ.-a3EDsxClUB4O3LeDD5DVGEnNMT01FCQW4P6-2-BNBqc_Zxf0Qw4CWayLEpqkAomlkLb9zioZoipdP-jvh1WlA" + } + } + ); + + let credential_request: Request = + serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str( + &serde_json::to_string(&expected_json).unwrap(), + )) + .unwrap(); + + let roundtripped = serde_json::to_value(credential_request).unwrap(); + assert_json_diff::assert_json_eq!(expected_json, roundtripped); + } +} diff --git a/src/custom/profiles/vc_sd_jwt/credential_response.rs b/src/custom/profiles/vc_sd_jwt/credential_response.rs new file mode 100644 index 0000000..5124156 --- /dev/null +++ b/src/custom/profiles/vc_sd_jwt/credential_response.rs @@ -0,0 +1,36 @@ +use serde::{Deserialize, Serialize}; +use ssi_claims::sd_jwt::SdJwtBuf; + +use crate::profiles::CredentialResponseProfile; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct CredentialResponse; + +impl CredentialResponseProfile for CredentialResponse { + type Type = SdJwtBuf; +} + +#[cfg(test)] +mod test { + use serde_json::json; + + use crate::credential::Response; + + #[test] + fn roundtrip() { + let expected_json = json!( + { + "credential": "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogInZjK3NkLWp3dCIsICJraWQiOiAiZG9jLXNpZ25lci0wNS0yNS0yMDIyIn0.eyJfc2QiOiBbIjA5dktySk1PbHlUV00wc2pwdV9wZE9CVkJRMk0xeTNLaHBINTE1blhrcFkiLCAiMnJzakdiYUMwa3k4bVQwcEpyUGlvV1RxMF9kYXcxc1g3NnBvVWxnQ3diSSIsICJFa084ZGhXMGRIRUpidlVIbEVfVkNldUM5dVJFTE9pZUxaaGg3WGJVVHRBIiwgIklsRHpJS2VpWmREd3BxcEs2WmZieXBoRnZ6NUZnbldhLXNONndxUVhDaXciLCAiSnpZakg0c3ZsaUgwUjNQeUVNZmVadTZKdDY5dTVxZWhabzdGN0VQWWxTRSIsICJQb3JGYnBLdVZ1Nnh5bUphZ3ZrRnNGWEFiUm9jMkpHbEFVQTJCQTRvN2NJIiwgIlRHZjRvTGJnd2Q1SlFhSHlLVlFaVTlVZEdFMHc1cnREc3JaemZVYW9tTG8iLCAiamRyVEU4WWNiWTRFaWZ1Z2loaUFlX0JQZWt4SlFaSUNlaVVRd1k5UXF4SSIsICJqc3U5eVZ1bHdRUWxoRmxNXzNKbHpNYVNGemdsaFFHMERwZmF5UXdMVUs0Il0sICJpc3MiOiAiaHR0cHM6Ly9leGFtcGxlLmNvbS9pc3N1ZXIiLCAiaWF0IjogMTY4MzAwMDAwMCwgImV4cCI6IDE4ODMwMDAwMDAsICJ2Y3QiOiAiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxlLmNvbS9pZGVudGl0eV9jcmVkZW50aWFsIiwgIl9zZF9hbGciOiAic2hhLTI1NiIsICJjbmYiOiB7Imp3ayI6IHsia3R5IjogIkVDIiwgImNydiI6ICJQLTI1NiIsICJ4IjogIlRDQUVSMTladnUzT0hGNGo0VzR2ZlNWb0hJUDFJTGlsRGxzN3ZDZUdlbWMiLCAieSI6ICJaeGppV1diWk1RR0hWV0tWUTRoYlNJaXJzVmZ1ZWNDRTZ0NGpUOUYySFpRIn19fQ.oiDeF5QD8nCi8NHpKCVBsyitThK1xdRPtMePDdEIqJFY1BKtd5PhYjXLUVg3VuQZqyuOUev0OQAgu1KuMY0DNA~WyIyR0xDNDJzS1F2ZUNmR2ZyeU5STjl3IiwgImdpdmVuX25hbWUiLCAiSm9obiJd~WyJlbHVWNU9nM2dTTklJOEVZbnN4QV9BIiwgImZhbWlseV9uYW1lIiwgIkRvZSJd~WyI2SWo3dE0tYTVpVlBHYm9TNXRtdlZBIiwgImVtYWlsIiwgImpvaG5kb2VAZXhhbXBsZS5jb20iXQ~WyJlSThaV205UW5LUHBOUGVOZW5IZGhRIiwgInBob25lX251bWJlciIsICIrMS0yMDItNTU1LTAxMDEiXQ~WyJRZ19PNjR6cUF4ZTQxMmExMDhpcm9BIiwgImFkZHJlc3MiLCB7InN0cmVldF9hZGRyZXNzIjogIjEyMyBNYWluIFN0IiwgImxvY2FsaXR5IjogIkFueXRvd24iLCAicmVnaW9uIjogIkFueXN0YXRlIiwgImNvdW50cnkiOiAiVVMifV0~WyJBSngtMDk1VlBycFR0TjRRTU9xUk9BIiwgImJpcnRoZGF0ZSIsICIxOTQwLTAxLTAxIl0~WyJQYzMzSk0yTGNoY1VfbEhnZ3ZfdWZRIiwgImlzX292ZXJfMTgiLCB0cnVlXQ~WyJHMDJOU3JRZmpGWFE3SW8wOXN5YWpBIiwgImlzX292ZXJfMjEiLCB0cnVlXQ~WyJsa2x4RjVqTVlsR1RQVW92TU5JdkNBIiwgImlzX292ZXJfNjUiLCB0cnVlXQ~" + } + ); + + let credential_response: Response = + serde_path_to_error::deserialize(&mut serde_json::Deserializer::from_str( + &serde_json::to_string(&expected_json).unwrap(), + )) + .unwrap(); + + let roundtripped = serde_json::to_value(credential_response).unwrap(); + assert_json_diff::assert_json_eq!(expected_json, roundtripped); + } +} diff --git a/src/custom/profiles/vc_sd_jwt/mod.rs b/src/custom/profiles/vc_sd_jwt/mod.rs new file mode 100644 index 0000000..18412a5 --- /dev/null +++ b/src/custom/profiles/vc_sd_jwt/mod.rs @@ -0,0 +1,34 @@ +pub mod authorization_detail; +pub mod credential_configuration; +pub mod credential_request; +pub mod credential_response; + +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +pub const FORMAT_IDENTIFIER: &str = "vc+sd-jwt"; +pub use authorization_detail::{AuthorizationDetailsObject, AuthorizationDetailsObjectWithFormat}; +pub use credential_configuration::CredentialConfiguration; +pub use credential_request::{CredentialRequest, CredentialRequestWithFormat}; +pub use credential_response::CredentialResponse; + +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +pub enum Format { + #[default] + #[serde(rename = "vc+sd-jwt")] + VcSdJwt, +} + +pub type Claims = HashMap>>; + +// Object containing a list of name/value pairs, where each name identifies a claim offered in the Credential. +// The value can be another such object (nested data structures), or an array of such objects. +// https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-ID1.html#appendix-A.1.1.2-3.1.2.2.1 +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[serde(untagged)] +pub enum MaybeNestedClaims { + Object(Claims), + Array(Vec>), + Leaf(T), +} diff --git a/src/lib.rs b/src/lib.rs index 02a9d3f..2163578 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod core; pub mod credential; pub mod credential_offer; pub mod credential_response_encryption; +pub mod custom; mod deny_field; mod http_utils; pub mod metadata; From 10bf29ceb70b251fb495262d16729d7d41df35a2 Mon Sep 17 00:00:00 2001 From: Ross Schulman Date: Wed, 20 Nov 2024 14:18:28 -0500 Subject: [PATCH 2/4] Create initializer for CredentialConfiguration --- src/custom/profiles/vc_sd_jwt/credential_configuration.rs | 7 +++++++ src/metadata/authorization_server.rs | 1 - 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/custom/profiles/vc_sd_jwt/credential_configuration.rs b/src/custom/profiles/vc_sd_jwt/credential_configuration.rs index b0262de..7838efb 100644 --- a/src/custom/profiles/vc_sd_jwt/credential_configuration.rs +++ b/src/custom/profiles/vc_sd_jwt/credential_configuration.rs @@ -19,6 +19,13 @@ pub struct CredentialConfiguration { } impl CredentialConfiguration { + pub fn new(vct: String) -> Self { + Self { + vct, + ..Default::default() + } + } + field_getters_setters![ pub self [self] ["VC SD-JWT metadata value"] { set_credential_signing_alg_values_supported -> credential_signing_alg_values_supported[Vec], diff --git a/src/metadata/authorization_server.rs b/src/metadata/authorization_server.rs index a78a92d..438e93b 100644 --- a/src/metadata/authorization_server.rs +++ b/src/metadata/authorization_server.rs @@ -61,7 +61,6 @@ pub struct AuthorizationServerMetadata { } impl AuthorizationServerMetadata { - #[cfg(test)] pub fn new(issuer: IssuerUrl, token_endpoint: TokenUrl) -> Self { Self { issuer, From f60c4a3df2c62e376265c90fc1a7a14a72cf3c1d Mon Sep 17 00:00:00 2001 From: Ross Schulman Date: Thu, 21 Nov 2024 08:51:30 -0500 Subject: [PATCH 3/4] Move Default impl to derive --- src/custom/profiles/vc_sd_jwt/credential_request.rs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/custom/profiles/vc_sd_jwt/credential_request.rs b/src/custom/profiles/vc_sd_jwt/credential_request.rs index ac73918..7d78a83 100644 --- a/src/custom/profiles/vc_sd_jwt/credential_request.rs +++ b/src/custom/profiles/vc_sd_jwt/credential_request.rs @@ -32,22 +32,13 @@ impl CredentialRequestProfile for CredentialRequestWithFormat { type Response = CredentialResponse; } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct CredentialRequest { vct: String, #[serde(default, skip_serializing_if = "Option::is_none")] claims: Option>, } -impl Default for CredentialRequest { - fn default() -> Self { - Self { - vct: String::new(), - claims: Default::default(), - } - } -} - impl CredentialRequest { pub fn new(vct: String, claims: Claims) -> Self { Self { From d3e18362ef1c88fe1d44c136eebec1f6d29ed29d Mon Sep 17 00:00:00 2001 From: Ross Schulman Date: Thu, 21 Nov 2024 13:21:17 -0500 Subject: [PATCH 4/4] Change format identifier to Spruce-specific namespace --- src/custom/profiles/vc_sd_jwt/authorization_detail.rs | 2 +- src/custom/profiles/vc_sd_jwt/credential_configuration.rs | 2 +- src/custom/profiles/vc_sd_jwt/credential_request.rs | 2 +- src/custom/profiles/vc_sd_jwt/mod.rs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/custom/profiles/vc_sd_jwt/authorization_detail.rs b/src/custom/profiles/vc_sd_jwt/authorization_detail.rs index 2f1be5e..2cd51e1 100644 --- a/src/custom/profiles/vc_sd_jwt/authorization_detail.rs +++ b/src/custom/profiles/vc_sd_jwt/authorization_detail.rs @@ -53,7 +53,7 @@ mod test { let expected_json = json!( { "type": "openid_credential", - "format": "vc+sd-jwt", + "format": "spruce-vc+sd-jwt", "vct": "SD_JWT_VC_example_in_OpenID4VCI" } ); diff --git a/src/custom/profiles/vc_sd_jwt/credential_configuration.rs b/src/custom/profiles/vc_sd_jwt/credential_configuration.rs index 7838efb..045ae4b 100644 --- a/src/custom/profiles/vc_sd_jwt/credential_configuration.rs +++ b/src/custom/profiles/vc_sd_jwt/credential_configuration.rs @@ -49,7 +49,7 @@ mod test { let expected_json = json!( { "$key$": "SD_JWT_VC_example_in_OpenID4VCI", - "format": "vc+sd-jwt", + "format": "spruce-vc+sd-jwt", "scope": "SD_JWT_VC_example_in_OpenID4VCI", "cryptographic_binding_methods_supported": [ "jwk" diff --git a/src/custom/profiles/vc_sd_jwt/credential_request.rs b/src/custom/profiles/vc_sd_jwt/credential_request.rs index 7d78a83..3d7a958 100644 --- a/src/custom/profiles/vc_sd_jwt/credential_request.rs +++ b/src/custom/profiles/vc_sd_jwt/credential_request.rs @@ -68,7 +68,7 @@ mod test { fn roundtrip_with_format() { let expected_json = json!( { - "format": "vc+sd-jwt", + "format": "spruce-vc+sd-jwt", "vct": "SD_JWT_VC_example_in_OpenID4VCI", "proof": { "proof_type": "jwt", diff --git a/src/custom/profiles/vc_sd_jwt/mod.rs b/src/custom/profiles/vc_sd_jwt/mod.rs index 18412a5..4feaded 100644 --- a/src/custom/profiles/vc_sd_jwt/mod.rs +++ b/src/custom/profiles/vc_sd_jwt/mod.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -pub const FORMAT_IDENTIFIER: &str = "vc+sd-jwt"; +pub const FORMAT_IDENTIFIER: &str = "spruce-vc+sd-jwt"; pub use authorization_detail::{AuthorizationDetailsObject, AuthorizationDetailsObjectWithFormat}; pub use credential_configuration::CredentialConfiguration; pub use credential_request::{CredentialRequest, CredentialRequestWithFormat}; @@ -16,7 +16,7 @@ pub use credential_response::CredentialResponse; #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub enum Format { #[default] - #[serde(rename = "vc+sd-jwt")] + #[serde(rename = "spruce-vc+sd-jwt")] VcSdJwt, }