From 0e771c834fa632fce6672ff2bc03aa30f3f37e73 Mon Sep 17 00:00:00 2001 From: Tiago Nascimento Date: Thu, 12 Dec 2024 16:08:16 -0300 Subject: [PATCH] Add oid4vci exchange options to allow verification to be optional (#60) Signed-off-by: Tiago Nascimento --- .../Sources/MobileSdkRs/mobile_sdk_rs.swift | 84 +++++++++- src/oid4vci/mod.rs | 145 ++++++++++++------ src/oid4vci/options.rs | 4 + src/oid4vci/wrapper.rs | 4 +- src/tests.rs | 6 +- 5 files changed, 183 insertions(+), 60 deletions(-) create mode 100644 src/oid4vci/options.rs diff --git a/MobileSdkRs/Sources/MobileSdkRs/mobile_sdk_rs.swift b/MobileSdkRs/Sources/MobileSdkRs/mobile_sdk_rs.swift index 79679e1..7473239 100644 --- a/MobileSdkRs/Sources/MobileSdkRs/mobile_sdk_rs.swift +++ b/MobileSdkRs/Sources/MobileSdkRs/mobile_sdk_rs.swift @@ -2757,7 +2757,7 @@ public protocol Oid4vciProtocol : AnyObject { func clearContextMap() throws - func exchangeCredential(proofsOfPossession: [String]) async throws -> [CredentialResponse] + func exchangeCredential(proofsOfPossession: [String], options: Oid4vciExchangeOptions) async throws -> [CredentialResponse] func exchangeToken() async throws -> String? @@ -2857,13 +2857,13 @@ open func clearContextMap()throws {try rustCallWithError(FfiConverterTypeOid4vc } } -open func exchangeCredential(proofsOfPossession: [String])async throws -> [CredentialResponse] { +open func exchangeCredential(proofsOfPossession: [String], options: Oid4vciExchangeOptions)async throws -> [CredentialResponse] { return try await uniffiRustCallAsync( rustFutureFunc: { uniffi_mobile_sdk_rs_fn_method_oid4vci_exchange_credential( self.uniffiClonePointer(), - FfiConverterSequenceString.lower(proofsOfPossession) + FfiConverterSequenceString.lower(proofsOfPossession),FfiConverterTypeOid4vciExchangeOptions.lower(options) ) }, pollFunc: ffi_mobile_sdk_rs_rust_future_poll_rust_buffer, @@ -6048,6 +6048,55 @@ public func FfiConverterTypeMDLReaderSessionData_lower(_ value: MdlReaderSession } +public struct Oid4vciExchangeOptions { + public var verifyAfterExchange: Bool? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(verifyAfterExchange: Bool?) { + self.verifyAfterExchange = verifyAfterExchange + } +} + + + +extension Oid4vciExchangeOptions: Equatable, Hashable { + public static func ==(lhs: Oid4vciExchangeOptions, rhs: Oid4vciExchangeOptions) -> Bool { + if lhs.verifyAfterExchange != rhs.verifyAfterExchange { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(verifyAfterExchange) + } +} + + +public struct FfiConverterTypeOid4vciExchangeOptions: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Oid4vciExchangeOptions { + return + try Oid4vciExchangeOptions( + verifyAfterExchange: FfiConverterOptionBool.read(from: &buf) + ) + } + + public static func write(_ value: Oid4vciExchangeOptions, into buf: inout [UInt8]) { + FfiConverterOptionBool.write(value.verifyAfterExchange, into: &buf) + } +} + + +public func FfiConverterTypeOid4vciExchangeOptions_lift(_ buf: RustBuffer) throws -> Oid4vciExchangeOptions { + return try FfiConverterTypeOid4vciExchangeOptions.lift(buf) +} + +public func FfiConverterTypeOid4vciExchangeOptions_lower(_ value: Oid4vciExchangeOptions) -> RustBuffer { + return FfiConverterTypeOid4vciExchangeOptions.lower(value) +} + + public struct StatusMessage { /** * The value of the entry in the status list @@ -9659,6 +9708,27 @@ fileprivate struct FfiConverterOptionInt64: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterOptionBool: FfiConverterRustBuffer { + typealias SwiftType = Bool? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterBool.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterBool.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer { typealias SwiftType = String? @@ -10908,11 +10978,11 @@ public func initializeMdlPresentationFromBytes(mdoc: Mdoc, uuid: Uuid)throws -> ) }) } -public func oid4vciExchangeCredential(session: Oid4vciSession, proofsOfPossession: [String], contextMap: [String: String]?, httpClient: IHttpClient)async throws -> [CredentialResponse] { +public func oid4vciExchangeCredential(session: Oid4vciSession, proofsOfPossession: [String], options: Oid4vciExchangeOptions, contextMap: [String: String]?, httpClient: IHttpClient)async throws -> [CredentialResponse] { return try await uniffiRustCallAsync( rustFutureFunc: { - uniffi_mobile_sdk_rs_fn_func_oid4vci_exchange_credential(FfiConverterTypeOid4vciSession.lower(session),FfiConverterSequenceString.lower(proofsOfPossession),FfiConverterOptionDictionaryStringString.lower(contextMap),FfiConverterTypeIHttpClient.lower(httpClient) + uniffi_mobile_sdk_rs_fn_func_oid4vci_exchange_credential(FfiConverterTypeOid4vciSession.lower(session),FfiConverterSequenceString.lower(proofsOfPossession),FfiConverterTypeOid4vciExchangeOptions.lower(options),FfiConverterOptionDictionaryStringString.lower(contextMap),FfiConverterTypeIHttpClient.lower(httpClient) ) }, pollFunc: ffi_mobile_sdk_rs_rust_future_poll_rust_buffer, @@ -11078,7 +11148,7 @@ private var initializationResult: InitializationResult = { if (uniffi_mobile_sdk_rs_checksum_func_initialize_mdl_presentation_from_bytes() != 26972) { return InitializationResult.apiChecksumMismatch } - if (uniffi_mobile_sdk_rs_checksum_func_oid4vci_exchange_credential() != 59343) { + if (uniffi_mobile_sdk_rs_checksum_func_oid4vci_exchange_credential() != 25671) { return InitializationResult.apiChecksumMismatch } if (uniffi_mobile_sdk_rs_checksum_func_oid4vci_exchange_token() != 3394) { @@ -11207,7 +11277,7 @@ private var initializationResult: InitializationResult = { if (uniffi_mobile_sdk_rs_checksum_method_oid4vci_clear_context_map() != 165) { return InitializationResult.apiChecksumMismatch } - if (uniffi_mobile_sdk_rs_checksum_method_oid4vci_exchange_credential() != 17336) { + if (uniffi_mobile_sdk_rs_checksum_method_oid4vci_exchange_credential() != 53636) { return InitializationResult.apiChecksumMismatch } if (uniffi_mobile_sdk_rs_checksum_method_oid4vci_exchange_token() != 35585) { diff --git a/src/oid4vci/mod.rs b/src/oid4vci/mod.rs index fafb3d3..2d958a9 100644 --- a/src/oid4vci/mod.rs +++ b/src/oid4vci/mod.rs @@ -31,6 +31,7 @@ pub use context_loader::context_loader_from_map; pub use error::*; pub use http_client::*; pub use metadata::*; +pub use options::*; pub use session::*; pub use wrapper::*; @@ -40,6 +41,7 @@ mod context_loader; mod error; mod http_client; mod metadata; +mod options; mod session; mod wrapper; @@ -265,6 +267,7 @@ pub async fn oid4vci_exchange_token( pub async fn oid4vci_exchange_credential( session: Arc, proofs_of_possession: Vec, + options: Oid4vciExchangeOptions, context_map: Option>, http_client: Arc, ) -> Result, Oid4vciError> { @@ -348,56 +351,98 @@ pub async fn oid4vci_exchange_credential( .collect() }; - log::trace!("create vm_resolver"); - let vm_resolver = AnyDidMethod::default().into_vm_resolver(); - log::trace!("create verification params"); - let params = match context_map { - Some(context_map) => VerificationParameters::from_resolver(vm_resolver) - .with_json_ld_loader(context_loader_from_map(context_map)?), - None => VerificationParameters::from_resolver(vm_resolver), - }; - - log::trace!("verify and convert http response into credential response"); - futures::future::try_join_all(credential_responses.into_iter().map( - |credential_response| async { - use oid4vci::core::profiles::CoreProfilesCredentialResponseType::*; - - match credential_response { - JwtVcJson(response) => { - log::trace!("processing a JwtVcJson"); - let rt = tokio::runtime::Runtime::new().unwrap(); - let ret = response.as_bytes().to_vec(); - - Ok(CredentialResponse { - format: CredentialFormat::JwtVcJson, - payload: rt - .block_on(async { response.verify_jwt(¶ms).await.map(|_| ret) })?, - }) - } - JwtVcJsonLd(response) => { - log::trace!("processing a JwtVcJsonLd"); - let vc = serde_json::to_string(&response)?; - let ret = serde_json::to_vec(&response)?; - Ok(CredentialResponse { - format: CredentialFormat::JwtVcJsonLd, - payload: any_credential_from_json_str(&vc)? - .verify(¶ms) - .await - .map(|_| ret)?, - }) + if options.verify_after_exchange.unwrap_or(false) { + futures::future::try_join_all(credential_responses.into_iter().map( + |credential_response| async { + use oid4vci::core::profiles::CoreProfilesCredentialResponseType::*; + + match credential_response { + JwtVcJson(response) => { + log::trace!("processing a JwtVcJson"); + let ret = response.as_bytes().to_vec(); + + Ok(CredentialResponse { + format: CredentialFormat::JwtVcJson, + payload: ret, + }) + } + JwtVcJsonLd(response) => { + log::trace!("processing a JwtVcJsonLd"); + let ret = serde_json::to_vec(&response)?; + Ok(CredentialResponse { + format: CredentialFormat::JwtVcJsonLd, + payload: ret, + }) + } + LdpVc(response) => { + log::trace!("processing an LdpVc"); + let vc: AnyDataIntegrity = + serde_json::from_value(response)?; + let ret = serde_json::to_vec(&vc)?; + Ok(CredentialResponse { + format: CredentialFormat::LdpVc, + payload: ret, + }) + } + MsoMdoc(_) => todo!(), } - LdpVc(response) => { - log::trace!("processing an LdpVc"); - let vc: AnyDataIntegrity = serde_json::from_value(response)?; - let ret = serde_json::to_vec(&vc)?; - Ok(CredentialResponse { - format: CredentialFormat::LdpVc, - payload: vc.verify(¶ms).await.map(|_| ret)?, - }) + }, + )) + .await + } else { + log::trace!("create vm_resolver"); + let vm_resolver = AnyDidMethod::default().into_vm_resolver(); + log::trace!("create verification params"); + let params = match context_map { + Some(context_map) => VerificationParameters::from_resolver(vm_resolver) + .with_json_ld_loader(context_loader_from_map(context_map)?), + None => VerificationParameters::from_resolver(vm_resolver), + }; + + log::trace!("verify and convert http response into credential response"); + futures::future::try_join_all(credential_responses.into_iter().map( + |credential_response| async { + use oid4vci::core::profiles::CoreProfilesCredentialResponseType::*; + + match credential_response { + JwtVcJson(response) => { + log::trace!("processing a JwtVcJson"); + let rt = tokio::runtime::Runtime::new().unwrap(); + let ret = response.as_bytes().to_vec(); + + Ok(CredentialResponse { + format: CredentialFormat::JwtVcJson, + payload: rt.block_on(async { + response.verify_jwt(¶ms).await.map(|_| ret) + })?, + }) + } + JwtVcJsonLd(response) => { + log::trace!("processing a JwtVcJsonLd"); + let vc = serde_json::to_string(&response)?; + let ret = serde_json::to_vec(&response)?; + Ok(CredentialResponse { + format: CredentialFormat::JwtVcJsonLd, + payload: any_credential_from_json_str(&vc)? + .verify(¶ms) + .await + .map(|_| ret)?, + }) + } + LdpVc(response) => { + log::trace!("processing an LdpVc"); + let vc: AnyDataIntegrity = + serde_json::from_value(response)?; + let ret = serde_json::to_vec(&vc)?; + Ok(CredentialResponse { + format: CredentialFormat::LdpVc, + payload: vc.verify(¶ms).await.map(|_| ret)?, + }) + } + MsoMdoc(_) => todo!(), } - MsoMdoc(_) => todo!(), - } - }, - )) - .await + }, + )) + .await + } } diff --git a/src/oid4vci/options.rs b/src/oid4vci/options.rs new file mode 100644 index 0000000..690f558 --- /dev/null +++ b/src/oid4vci/options.rs @@ -0,0 +1,4 @@ +#[derive(uniffi::Record, Clone, Debug, Default)] +pub struct Oid4vciExchangeOptions { + pub verify_after_exchange: Option, +} diff --git a/src/oid4vci/wrapper.rs b/src/oid4vci/wrapper.rs index 938cdb6..0918029 100644 --- a/src/oid4vci/wrapper.rs +++ b/src/oid4vci/wrapper.rs @@ -6,7 +6,7 @@ use std::{ use super::{ oid4vci_exchange_credential, oid4vci_exchange_token, oid4vci_get_metadata, oid4vci_initiate, oid4vci_initiate_with_offer, AsyncHttpClient, CredentialResponse, IHttpClient, Oid4vciError, - Oid4vciMetadata, Oid4vciSession, SyncHttpClient, + Oid4vciExchangeOptions, Oid4vciMetadata, Oid4vciSession, SyncHttpClient, }; #[derive(uniffi::Object)] @@ -167,10 +167,12 @@ impl Oid4vci { pub async fn exchange_credential( &self, proofs_of_possession: Vec, + options: Oid4vciExchangeOptions, ) -> Result, Oid4vciError> { oid4vci_exchange_credential( self.session()?, proofs_of_possession, + options, self.context_map()?, self.http_client.clone(), ) diff --git a/src/tests.rs b/src/tests.rs index 64de770..0f82ee3 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -15,7 +15,7 @@ use ssi::{ }; use uniffi::deps::anyhow::Result; -use crate::oid4vci::Oid4vci; +use crate::oid4vci::{Oid4vci, Oid4vciExchangeOptions}; const OID4VCI_CREDENTIAL_OFFER_URI: &str = "openid-credential-offer://?credential_offer_uri=https%3A%2F%2Fqa.veresexchanger.dev%2Fexchangers%2Fz1A68iKqcX2HbQGQfVSfFnjkM%2Fexchanges%2Fz1ADKJLFpFtovZkxXHbQz47f5%2Fopenid%2Fcredential-offer"; const OID4VP_URI: &str = "openid4vp://?client_id=https%3A%2F%2Fqa.veresexchanger.dev%2Fexchangers%2Fz19vRLNoFaBKDeDaMzRjUj8hi%2Fexchanges%2Fz19prZuVakk5Rzt1tE12evjQX%2Fopenid%2Fclient%2Fauthorization%2Fresponse&request_uri=https%3A%2F%2Fqa.veresexchanger.dev%2Fexchangers%2Fz19vRLNoFaBKDeDaMzRjUj8hi%2Fexchanges%2Fz19prZuVakk5Rzt1tE12evjQX%2Fopenid%2Fclient%2Fauthorization%2Frequest"; @@ -138,7 +138,9 @@ pub async fn test_vc_playground_oid4vci() -> Result<()> { // Load VC Playground Context session.set_context_map(vc_playground_context())?; - let credentials = session.exchange_credential(vec![pop]).await?; + let credentials = session + .exchange_credential(vec![pop], Oid4vciExchangeOptions::default()) + .await?; println!("Credentials: {credentials:?}");