diff --git a/Cargo.lock b/Cargo.lock index f98c6833..b811c7dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2541,6 +2541,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inventory" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767" + [[package]] name = "ipnet" version = "2.10.1" @@ -2892,17 +2898,6 @@ dependencies = [ "utf8-decode", ] -[[package]] -name = "jsonpath_lib" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaa63191d68230cccb81c5aa23abd53ed64d83337cacbb25a7b8c7979523774f" -dependencies = [ - "log", - "serde", - "serde_json", -] - [[package]] name = "jsonschema" version = "0.18.3" @@ -3620,14 +3615,13 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openid4vp" version = "0.1.0" -source = "git+https://github.com/spruceid/openid4vp?rev=372f5f5#372f5f59c3176786cda6ebcdf41944c5ff7fb932" +source = "git+https://github.com/spruceid/openid4vp?rev=4a76572#4a765727593463163bab37cd5d963a91dfb765ba" dependencies = [ "anyhow", "async-trait", "base64 0.21.7", "http 1.1.0", "json-syntax", - "jsonpath_lib", "jsonschema", "openid4vp-frontend", "p256", @@ -3635,8 +3629,10 @@ dependencies = [ "reqwest 0.12.8", "serde", "serde_json", + "serde_json_path", "serde_urlencoded", "ssi", + "thiserror", "tokio", "tracing", "url", @@ -3647,7 +3643,7 @@ dependencies = [ [[package]] name = "openid4vp-frontend" version = "0.1.0" -source = "git+https://github.com/spruceid/openid4vp?rev=372f5f5#372f5f59c3176786cda6ebcdf41944c5ff7fb932" +source = "git+https://github.com/spruceid/openid4vp?rev=4a76572#4a765727593463163bab37cd5d963a91dfb765ba" dependencies = [ "serde", "serde_json", @@ -4735,13 +4731,65 @@ version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ - "indexmap 2.6.0", "itoa", "memchr", "ryu", "serde", ] +[[package]] +name = "serde_json_path" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc0207b6351893eafa1e39aa9aea452abb6425ca7b02dd64faf29109e7a33ba" +dependencies = [ + "inventory", + "nom", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_json_path_core", + "serde_json_path_macros", + "thiserror", +] + +[[package]] +name = "serde_json_path_core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d64fe53ce1aaa31bea2b2b46d3b6ab6a37e61854bedcbd9f174e188f3f7d79" +dependencies = [ + "inventory", + "once_cell", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "serde_json_path_macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31e8177a443fd3e94917f12946ae7891dfb656e6d4c5e79b8c5d202fbcb723" +dependencies = [ + "inventory", + "once_cell", + "serde_json_path_core", + "serde_json_path_macros_internal", +] + +[[package]] +name = "serde_json_path_macros_internal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75dde5a1d2ed78dfc411fc45592f72d3694436524d3353683ecb3d22009731dc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "serde_path_to_error" version = "0.1.16" @@ -5995,18 +6043,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 79efc8e5..d269fac1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ cose-rs = { git = "https://github.com/spruceid/cose-rs", rev = "0018c9b", featur ] } isomdl = { git = "https://github.com/spruceid/isomdl", rev = "1f4f762" } oid4vci = { git = "https://github.com/spruceid/oid4vci-rs", rev = "d95fe3a" } -openid4vp = { git = "https://github.com/spruceid/openid4vp", rev = "372f5f5" } +openid4vp = { git = "https://github.com/spruceid/openid4vp", rev = "4a76572" } ssi = { version = "0.9", features = ["secp256r1", "secp384r1"] } async-trait = "0.1" @@ -41,7 +41,7 @@ reqwest = { version = "0.11", features = ["blocking"] } serde = { version = "1.0.204", features = ["derive"] } serde_cbor = "0.11.2" serde_json = "1.0.111" -thiserror = "1.0.56" +thiserror = "1.0.65" signature = "2.2.0" ssi-contexts = "0.1.6" time = { version = "0.3.36", features = [ diff --git a/MobileSdkRs/Sources/MobileSdkRs/mobile_sdk_rs.swift b/MobileSdkRs/Sources/MobileSdkRs/mobile_sdk_rs.swift index fb92bbc2..5324a908 100644 --- a/MobileSdkRs/Sources/MobileSdkRs/mobile_sdk_rs.swift +++ b/MobileSdkRs/Sources/MobileSdkRs/mobile_sdk_rs.swift @@ -3211,7 +3211,7 @@ public protocol PermissionRequestProtocol : AnyObject { /** * Construct a new permission response for the given credential. */ - func createPermissionResponse(selectedCredential: ParsedCredential) -> PermissionResponse + func createPermissionResponse(selectedCredentials: [ParsedCredential]) -> PermissionResponse /** * Return the filtered list of credentials that matched @@ -3277,10 +3277,10 @@ open class PermissionRequest: /** * Construct a new permission response for the given credential. */ -open func createPermissionResponse(selectedCredential: ParsedCredential) -> PermissionResponse { +open func createPermissionResponse(selectedCredentials: [ParsedCredential]) -> PermissionResponse { return try! FfiConverterTypePermissionResponse.lift(try! rustCall() { uniffi_mobile_sdk_rs_fn_method_permissionrequest_create_permission_response(self.uniffiClonePointer(), - FfiConverterTypeParsedCredential.lower(selectedCredential),$0 + FfiConverterSequenceTypeParsedCredential.lower(selectedCredentials),$0 ) }) } @@ -3481,13 +3481,18 @@ public protocol RequestedFieldProtocol : AnyObject { /** * Return the field name */ - func name() -> String + func name() -> String? /** * Return the purpose of the requested field. */ func purpose() -> String? + /** + * Return the stringified JSON raw fields. + */ + func rawFields() -> [String] + /** * Return the field required status */ @@ -3544,8 +3549,8 @@ open class RequestedField: /** * Return the field name */ -open func name() -> String { - return try! FfiConverterString.lift(try! rustCall() { +open func name() -> String? { + return try! FfiConverterOptionString.lift(try! rustCall() { uniffi_mobile_sdk_rs_fn_method_requestedfield_name(self.uniffiClonePointer(),$0 ) }) @@ -3559,6 +3564,16 @@ open func purpose() -> String? { uniffi_mobile_sdk_rs_fn_method_requestedfield_purpose(self.uniffiClonePointer(),$0 ) }) +} + + /** + * Return the stringified JSON raw fields. + */ +open func rawFields() -> [String] { + return try! FfiConverterSequenceString.lift(try! rustCall() { + uniffi_mobile_sdk_rs_fn_method_requestedfield_raw_fields(self.uniffiClonePointer(),$0 + ) +}) } /** @@ -5491,6 +5506,8 @@ public enum CredentialEncodingError { ) case SdJwt(SdJwtError ) + case VpToken(String + ) } @@ -5513,6 +5530,9 @@ public struct FfiConverterTypeCredentialEncodingError: FfiConverterRustBuffer { case 3: return .SdJwt( try FfiConverterTypeSdJwtError.read(from: &buf) ) + case 4: return .VpToken( + try FfiConverterString.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -5539,6 +5559,11 @@ public struct FfiConverterTypeCredentialEncodingError: FfiConverterRustBuffer { writeInt(&buf, Int32(3)) FfiConverterTypeSdJwtError.write(v1, into: &buf) + + case let .VpToken(v1): + writeInt(&buf, Int32(4)) + FfiConverterString.write(v1, into: &buf) + } } } @@ -6646,6 +6671,8 @@ public enum Oid4vpError { case RequestSignerNotFound case MetadataInitialization(String ) + case PermissionResponse(PermissionResponseError + ) } @@ -6726,6 +6753,9 @@ public struct FfiConverterTypeOID4VPError: FfiConverterRustBuffer { case 25: return .MetadataInitialization( try FfiConverterString.read(from: &buf) ) + case 26: return .PermissionResponse( + try FfiConverterTypePermissionResponseError.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } @@ -6858,6 +6888,11 @@ public struct FfiConverterTypeOID4VPError: FfiConverterRustBuffer { writeInt(&buf, Int32(25)) FfiConverterString.write(v1, into: &buf) + + case let .PermissionResponse(v1): + writeInt(&buf, Int32(26)) + FfiConverterTypePermissionResponseError.write(v1, into: &buf) + } } } @@ -7200,6 +7235,68 @@ extension PermissionRequestError: Foundation.LocalizedError { } +public enum PermissionResponseError { + + + + case JsonPathParse(String + ) + case CredentialEncoding(CredentialEncodingError + ) +} + + +public struct FfiConverterTypePermissionResponseError: FfiConverterRustBuffer { + typealias SwiftType = PermissionResponseError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PermissionResponseError { + let variant: Int32 = try readInt(&buf) + switch variant { + + + + + case 1: return .JsonPathParse( + try FfiConverterString.read(from: &buf) + ) + case 2: return .CredentialEncoding( + try FfiConverterTypeCredentialEncodingError.read(from: &buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: PermissionResponseError, into buf: inout [UInt8]) { + switch value { + + + + + + case let .JsonPathParse(v1): + writeInt(&buf, Int32(1)) + FfiConverterString.write(v1, into: &buf) + + + case let .CredentialEncoding(v1): + writeInt(&buf, Int32(2)) + FfiConverterTypeCredentialEncodingError.write(v1, into: &buf) + + } + } +} + + +extension PermissionResponseError: Equatable, Hashable {} + +extension PermissionResponseError: Foundation.LocalizedError { + public var errorDescription: String? { + String(reflecting: self) + } +} + + public enum PopError { @@ -9631,7 +9728,7 @@ private var initializationResult: InitializationResult = { if (uniffi_mobile_sdk_rs_checksum_method_parsedcredential_type() != 60750) { return InitializationResult.apiChecksumMismatch } - if (uniffi_mobile_sdk_rs_checksum_method_permissionrequest_create_permission_response() != 11132) { + if (uniffi_mobile_sdk_rs_checksum_method_permissionrequest_create_permission_response() != 23487) { return InitializationResult.apiChecksumMismatch } if (uniffi_mobile_sdk_rs_checksum_method_permissionrequest_credentials() != 38374) { @@ -9643,12 +9740,15 @@ private var initializationResult: InitializationResult = { if (uniffi_mobile_sdk_rs_checksum_method_permissionrequest_requested_fields() != 48174) { return InitializationResult.apiChecksumMismatch } - if (uniffi_mobile_sdk_rs_checksum_method_requestedfield_name() != 28018) { + if (uniffi_mobile_sdk_rs_checksum_method_requestedfield_name() != 19474) { return InitializationResult.apiChecksumMismatch } if (uniffi_mobile_sdk_rs_checksum_method_requestedfield_purpose() != 46977) { return InitializationResult.apiChecksumMismatch } + if (uniffi_mobile_sdk_rs_checksum_method_requestedfield_raw_fields() != 44847) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_mobile_sdk_rs_checksum_method_requestedfield_required() != 14409) { return InitializationResult.apiChecksumMismatch } diff --git a/src/credential/json_vc.rs b/src/credential/json_vc.rs index b04eb72b..3ca2b52a 100644 --- a/src/credential/json_vc.rs +++ b/src/credential/json_vc.rs @@ -129,7 +129,7 @@ impl JsonVc { } // Check the JSON-encoded credential against the definition. - definition.check_credential_validation(&self.raw) + definition.is_credential_match(&self.raw) } /// Returns the requested fields given a presentation definition. diff --git a/src/credential/jwt_vc.rs b/src/credential/jwt_vc.rs index 42dcf85c..60b65ef2 100644 --- a/src/credential/jwt_vc.rs +++ b/src/credential/jwt_vc.rs @@ -4,11 +4,16 @@ use crate::{oid4vp::permission_request::RequestedField, CredentialType, KeyAlias use std::sync::Arc; use base64::prelude::*; -use openid4vp::core::presentation_definition::PresentationDefinition; -use ssi::claims::{ - jwt::IntoDecodedJwt, - vc::v1::{Credential as _, JsonCredential}, - JwsString, +use openid4vp::core::{ + presentation_definition::PresentationDefinition, response::parameters::VpTokenItem, +}; +use ssi::{ + claims::{ + jwt::IntoDecodedJwt, + vc::v1::{Credential as _, JsonCredential, JsonPresentation}, + JwsString, + }, + json_ld::iref::UriBuf, }; use uuid::Uuid; @@ -170,7 +175,7 @@ impl JwtVc { }; // Check the JSON-encoded credential against the definition. - definition.check_credential_validation(&json) + definition.is_credential_match(&json) } /// Returns the requested fields given a presentation definition. @@ -192,6 +197,22 @@ impl JwtVc { .map(Arc::new) .collect() } + + /// Return the credential as a VpToken + pub fn as_vp_token(&self) -> VpTokenItem { + let id = UriBuf::new(format!("urn:uuid:{}", Uuid::new_v4()).as_bytes().to_vec()).ok(); + + // TODO: determine how the holder ID should be set. + let holder_id = None; + + // NOTE: JwtVc types are ALWAYS VCDM 1.1, therefore using the v1::syntax::JsonPresentation + // type. + VpTokenItem::from(JsonPresentation::new( + id, + holder_id, + vec![self.credential.clone()], + )) + } } impl TryFrom for Arc { diff --git a/src/credential/mod.rs b/src/credential/mod.rs index 2d7faddd..1080ce2d 100644 --- a/src/credential/mod.rs +++ b/src/credential/mod.rs @@ -9,7 +9,9 @@ use crate::{oid4vp::permission_request::RequestedField, CredentialType, KeyAlias use json_vc::{JsonVc, JsonVcEncodingError, JsonVcInitError}; use jwt_vc::{JwtVc, JwtVcInitError}; use mdoc::{Mdoc, MdocEncodingError, MdocInitError}; -use openid4vp::core::presentation_definition::PresentationDefinition; +use openid4vp::core::{ + presentation_definition::PresentationDefinition, response::parameters::VpTokenItem, +}; use serde::{Deserialize, Serialize}; use vcdm2_sd_jwt::{SdJwtError, VCDM2SdJwt}; @@ -253,6 +255,18 @@ impl ParsedCredential { } } } + + /// Return a VP Token for the credential. + pub fn as_vp_token(&self) -> Result { + match &self.inner { + ParsedCredentialInner::VCDM2SdJwt(sd_jwt) => Ok(sd_jwt.as_vp_token()), + ParsedCredentialInner::JwtVcJson(vc) => Ok(vc.as_vp_token()), + _ => Err(CredentialEncodingError::VpToken(format!( + "Credential encoding for VP Token is not implemented for {:?}.", + self.inner, + ))), + } + } } impl TryFrom for Arc { @@ -292,6 +306,8 @@ pub enum CredentialEncodingError { JsonVc(#[from] JsonVcEncodingError), #[error("SD-JWT encoding error: {0}")] SdJwt(#[from] SdJwtError), + #[error("VP Token encoding error: {0}")] + VpToken(String), } #[derive(Debug, uniffi::Error, thiserror::Error)] diff --git a/src/credential/vcdm2_sd_jwt.rs b/src/credential/vcdm2_sd_jwt.rs index 08678e86..52d6d3a4 100644 --- a/src/credential/vcdm2_sd_jwt.rs +++ b/src/credential/vcdm2_sd_jwt.rs @@ -3,7 +3,9 @@ use crate::{oid4vp::permission_request::RequestedField, CredentialType, KeyAlias use std::sync::Arc; -use openid4vp::core::presentation_definition::PresentationDefinition; +use openid4vp::core::{ + presentation_definition::PresentationDefinition, response::parameters::VpTokenItem, +}; use ssi::{ claims::{ sd_jwt::SdJwtBuf, @@ -71,7 +73,7 @@ impl VCDM2SdJwt { }; // Check the JSON-encoded credential against the definition. - definition.check_credential_validation(&json) + definition.is_credential_match(&json) } /// Return the requested fields for the SD-JWT credential. @@ -93,6 +95,19 @@ impl VCDM2SdJwt { .map(Arc::new) .collect() } + + /// Return the credential as a VpToken + pub fn as_vp_token(&self) -> VpTokenItem { + // TODO: need to provide the "filtered" (disclosed) fields of the + // credential to be encoded into the VpToken. + // + // Currently, this is encoding the entire revealed SD-JWT, + // without the selection of individual disclosed fields. + // + // We need to selectively disclosed fields. + let compact: &str = self.inner.as_ref(); + VpTokenItem::String(compact.to_string()) + } } #[uniffi::export] diff --git a/src/oid4vp/error.rs b/src/oid4vp/error.rs index 18776c1d..eb27bb0c 100644 --- a/src/oid4vp/error.rs +++ b/src/oid4vp/error.rs @@ -1,5 +1,7 @@ // use super::request_signer::RequestSignerError; +use super::permission_request::PermissionResponseError; + /// The [OID4VPError] enum represents the errors that can occur /// when using the oid4vp foreign library. #[derive(thiserror::Error, Debug, uniffi::Error)] @@ -54,6 +56,8 @@ pub enum OID4VPError { RequestSignerNotFound, #[error("Failed to initialize metadata: {0}")] MetadataInitialization(String), + #[error(transparent)] + PermissionResponse(#[from] PermissionResponseError), } // Handle unexpected errors when calling a foreign callback diff --git a/src/oid4vp/holder.rs b/src/oid4vp/holder.rs index 440de10b..28b95749 100644 --- a/src/oid4vp/holder.rs +++ b/src/oid4vp/holder.rs @@ -9,7 +9,6 @@ use std::sync::Arc; use openid4vp::core::authorization_request::parameters::ClientIdScheme; use openid4vp::core::credential_format::{ClaimFormatDesignation, ClaimFormatPayload}; use openid4vp::core::presentation_definition::PresentationDefinition; -use openid4vp::core::response::parameters::VpTokenItem; use openid4vp::{ core::{ authorization_request::{ @@ -18,15 +17,11 @@ use openid4vp::{ AuthorizationRequestObject, }, metadata::WalletMetadata, - presentation_submission::{DescriptorMap, PresentationSubmission}, - response::{parameters::VpToken, AuthorizationResponse, UnencodedAuthorizationResponse}, }, wallet::Wallet as OID4VPWallet, }; -use ssi::claims::vc::v1::syntax::JsonPresentation; use ssi::dids::DIDWeb; use ssi::dids::VerificationMethodDIDResolver; -use ssi::json_ld::iref::UriBuf; use ssi::prelude::AnyJwkMethod; use uniffi::deps::{anyhow, log}; @@ -122,49 +117,12 @@ impl Holder { &self, response: Arc, ) -> Result, OID4VPError> { - // Create a descriptor map for the presentation submission based on the credentials - // returned from the selection response. - let Some(input_descriptor_id) = response - .presentation_definition - .input_descriptors() - .first() - .map(|d| d.id().to_owned()) - else { - // NOTE: We may wish to add a more generic `BadRequest` error type - // we should always expect to have at least one input descriptor. - return Err(OID4VPError::InputDescriptorNotFound); - }; - - let descriptor_map = - self.create_descriptor_map(&response.selected_credential, input_descriptor_id); - - let presentation_submission_id = uuid::Uuid::new_v4(); - let presentation_definition_id = response.presentation_definition.id().clone(); - - // // Create a presentation submission. - let presentation_submission = PresentationSubmission::new( - presentation_submission_id, - presentation_definition_id, - vec![descriptor_map], - ); - - let vp_token = self - .create_verifiable_presentation(&response.selected_credential) - .await?; - - let response = self - .submit_response( - response.authorization_request.clone(), - AuthorizationResponse::Unencoded(UnencodedAuthorizationResponse( - Default::default(), - vp_token, - presentation_submission, - )), - ) - .await - .map_err(|e| OID4VPError::ResponseSubmission(format!("{e:?}")))?; - - Ok(response) + self.submit_response( + response.authorization_request.clone(), + response.authorization_response()?, + ) + .await + .map_err(|e| OID4VPError::ResponseSubmission(format!("{e:?}"))) } } @@ -225,20 +183,6 @@ impl Holder { Ok(credentials) } - // Construct a DescriptorMap for the presentation submission based on the - // credentials returned from the VDC collection. - fn create_descriptor_map( - &self, - credential: &Arc, - input_descriptor_id: String, - ) -> DescriptorMap { - DescriptorMap::new( - input_descriptor_id, - credential.format().to_string().as_str(), - "$.verifiableCredential".into(), - ) - } - // Internal method for returning the `PermissionRequest` for an oid4vp request. async fn permission_request( &self, @@ -261,46 +205,6 @@ impl Holder { request, )) } - - async fn create_verifiable_presentation( - &self, - credential: &Arc, - ) -> Result { - match &credential.inner { - ParsedCredentialInner::VCDM2SdJwt(sd_jwt) => { - // TODO: need to provide the "filtered" (disclosed) fields of the - // credential to be encoded into the VpToken. - // - // Currently, this is encoding the entire revealed SD-JWT, - // without the selection of individual disclosed fields. - // - // We need to selectively disclosed fields. - let compact: &str = sd_jwt.inner.as_ref(); - Ok(VpTokenItem::String(compact.to_string()).into()) - } - ParsedCredentialInner::JwtVcJson(vc) => { - let id = - UriBuf::new(format!("urn:uuid:{}", Uuid::new_v4()).as_bytes().to_vec()).ok(); - - // TODO: determine how the holder ID should be set. - let holder_id = None; - - // NOTE: JwtVc types are ALWAYS VCDM 1.1, therefore using the v1::syntax::JsonPresentation - // type. - let token = VpTokenItem::from(JsonPresentation::new( - id, - holder_id, - vec![vc.credential().to_owned()], - )); - - Ok(token.into()) - } - _ => Err(OID4VPError::VpTokenParse(format!( - "Credential parsing for VP Token is not implemented for {:?}.", - credential, - ))), - } - } } #[async_trait::async_trait] @@ -383,21 +287,20 @@ mod tests { let permission_request = holder.authorization_request(url).await?; - let mut parsed_credentials = permission_request.credentials(); + let parsed_credentials = permission_request.credentials(); assert_eq!(parsed_credentials.len(), 1); - let selected_credential = parsed_credentials - .pop() - .expect("failed to retrieve a parsed credential matching the presentation definition"); + for credential in parsed_credentials.iter() { + let requested_fields = permission_request.requested_fields(&credential); - let requested_fields = permission_request.requested_fields(&selected_credential); + println!("Requested Fields: {requested_fields:?}"); - println!("Requested Fields: {requested_fields:?}"); - - assert!(requested_fields.len() > 0); + assert!(requested_fields.len() > 0); + } - let response = permission_request.create_permission_response(selected_credential); + // NOTE: passing `parsed_credentials` as `selected_credentials`. + let response = permission_request.create_permission_response(parsed_credentials); holder.submit_permission_response(response).await?; diff --git a/src/oid4vp/permission_request.rs b/src/oid4vp/permission_request.rs index a8455616..70a87b2d 100644 --- a/src/oid4vp/permission_request.rs +++ b/src/oid4vp/permission_request.rs @@ -1,8 +1,11 @@ use openid4vp::core::authorization_request::AuthorizationRequestObject; use openid4vp::core::presentation_definition::PresentationDefinition; +use openid4vp::core::presentation_submission::{DescriptorMap, PresentationSubmission}; +use openid4vp::core::response::parameters::{VpToken, VpTokenItem}; +use openid4vp::core::response::{AuthorizationResponse, UnencodedAuthorizationResponse}; use crate::common::*; -use crate::credential::{Credential, ParsedCredential}; +use crate::credential::{Credential, CredentialEncodingError, ParsedCredential}; use std::collections::HashMap; use std::fmt::Debug; @@ -50,22 +53,30 @@ pub enum PermissionRequestError { CredentialPresentation(String), } +#[derive(uniffi::Error, thiserror::Error, Debug)] +pub enum PermissionResponseError { + #[error("Failed to parse JsonPath: {0}")] + JsonPathParse(String), + #[error(transparent)] + CredentialEncoding(#[from] CredentialEncodingError), +} + #[derive(Debug, uniffi::Object)] pub struct RequestedField { /// A unique ID for the requested field pub(crate) id: Uuid, - pub(crate) name: String, + pub(crate) name: Option, pub(crate) required: bool, pub(crate) retained: bool, pub(crate) purpose: Option, - pub(crate) constraint_field_id: Option, + pub(crate) input_descriptor_id: String, // the `raw_field` represents the actual field // being selected by the input descriptor JSON path // selector. - pub(crate) raw_fields: Option, + pub(crate) raw_fields: Vec, } -impl From for RequestedField { +impl<'a> From> for RequestedField { fn from(value: openid4vp::core::input_descriptor::RequestedField) -> Self { Self { id: value.id, @@ -73,53 +84,25 @@ impl From for RequestedField required: value.required, retained: value.retained, purpose: value.purpose, - constraint_field_id: value.constraint_field_id, - raw_fields: value.raw_fields, + input_descriptor_id: value.input_descriptor_id, + raw_fields: value + .raw_fields + .into_iter() + .map(ToOwned::to_owned) + .collect(), } } } impl RequestedField { - /// Construct a new requested field given the required parameters. This method is exposed as - /// public, however, it is likely that the `from_definition` method will be used to construct - /// requested fields from a presentation definition. - /// - /// See [RequestedField::from_definition] to return a vector of requested fields - /// according to a presentation definition. - pub fn new( - name: String, - required: bool, - retained: bool, - purpose: Option, - constraint_field_id: Option, - raw_fields: Option, - ) -> Arc { - Arc::new(Self { - id: Uuid::new_v4(), - name, - required, - retained, - purpose, - constraint_field_id, - raw_fields, - }) - } - /// Return the unique ID for the request field. pub fn id(&self) -> Uuid { self.id } - /// Return the constraint field id the requested field belongs to - pub fn constraint_field_id(&self) -> Option { - self.constraint_field_id.clone() - } - - /// Return the stringified JSON raw fields. - pub fn raw_fields(&self) -> Option { - self.raw_fields - .as_ref() - .and_then(|value| serde_json::to_string(value).ok()) + /// Return the input descriptor id the requested field belongs to + pub fn input_descriptor_id(&self) -> &String { + &self.input_descriptor_id } } @@ -127,7 +110,7 @@ impl RequestedField { #[uniffi::export] impl RequestedField { /// Return the field name - pub fn name(&self) -> String { + pub fn name(&self) -> Option { self.name.clone() } @@ -145,6 +128,14 @@ impl RequestedField { pub fn purpose(&self) -> Option { self.purpose.clone() } + + /// Return the stringified JSON raw fields. + pub fn raw_fields(&self) -> Vec { + self.raw_fields + .iter() + .filter_map(|value| serde_json::to_string(value).ok()) + .collect() + } } #[derive(Debug, Clone, uniffi::Object)] @@ -186,10 +177,10 @@ impl PermissionRequest { /// Construct a new permission response for the given credential. pub fn create_permission_response( &self, - selected_credential: Arc, + selected_credentials: Vec>, ) -> Arc { Arc::new(PermissionResponse { - selected_credential, + selected_credentials, presentation_definition: self.definition.clone(), authorization_request: self.request.clone(), }) @@ -209,9 +200,81 @@ impl PermissionRequest { /// explicitly setting the permission to true or false, based on the holder's decision. #[derive(Debug, Clone, uniffi::Object)] pub struct PermissionResponse { - pub selected_credential: Arc, + pub selected_credentials: Vec>, pub presentation_definition: PresentationDefinition, pub authorization_request: AuthorizationRequestObject, // TODO: provide an optional internal mapping of `JsonPointer`s // for selective disclosure that are selected as part of the requested fields. } + +impl PermissionResponse { + // Construct a DescriptorMap for the presentation submission based on the + // credentials returned from the VDC collection. + pub fn create_descriptor_map(&self) -> Result, PermissionResponseError> { + let is_singular = self.selected_credentials.len() == 1; + + self.presentation_definition + .input_descriptors() + // TODO: It is possible for an input descriptor to have multiple credentials, + // in which case, it may be expected that the descriptor map will have a nested + // path. When creating a descriptor map, it may be better to use a mapping of input descriptor + // id to a list of credentials, whereby each descriptor id is mapped to a descriptor map, + // with a nested path for each credential it maps onto. + // + // Currently, each selected credential is provided its own descriptor map associated with + // the corresponding input descriptor. It is assumed that each input descriptor corresponds + // to a single verifiable credential. + .iter() + .zip(self.selected_credentials.iter()) + .enumerate() + .map(|(idx, (descriptor, cred))| { + let vc_path = if is_singular { + "$.verifiableCredential".to_string() + } else { + format!("$.verifiableCredential[{idx}]") + } + .parse() + .map_err(|e| PermissionResponseError::JsonPathParse(format!("{e:?}")))?; + + Ok(DescriptorMap::new( + descriptor.id.to_string(), + cred.format().to_string().as_str(), + vc_path, + )) + }) + .collect() + } + + /// Create a VP token based on the selected credentials returned in the permission response. + pub fn create_vp_token(&self) -> Result { + let tokens = self + .selected_credentials + .iter() + .map(|cred| cred.as_vp_token()) + .collect::, CredentialEncodingError>>()?; + + Ok(VpToken(tokens)) + } + + /// Return the authorization response object. + pub fn authorization_response(&self) -> Result { + Ok(AuthorizationResponse::Unencoded( + UnencodedAuthorizationResponse( + Default::default(), + self.create_vp_token()?, + self.create_presentation_submission()?, + ), + )) + } + + /// Create a presentation submission based on the selected credentials returned in the permission response. + pub fn create_presentation_submission( + &self, + ) -> Result { + Ok(PresentationSubmission::new( + uuid::Uuid::new_v4(), + self.presentation_definition.id().clone(), + self.create_descriptor_map()?, + )) + } +}