From 87988cf8ba1cc2a1fcacca5c83cec5d07c346f8a Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 18 Nov 2024 10:16:04 +0200 Subject: [PATCH 01/21] [fix] remove cwt proof --- README.md | 2 +- .../Entities/CredentialIssuer/ProofType.swift | 25 +++---------------- .../IssuanceFlows/AuthCodeFlowIssuance.swift | 20 ++++++++++++--- Sources/Entities/Types/Types.swift | 9 ------- Sources/Issuers/Issuer.swift | 17 +++++++++---- Tests/Helpers/Wallet.swift | 2 +- .../Issuance/IssuanceAuthorizationTest.swift | 7 +++--- Tests/Issuance/IssuanceBatchRequestTest.swift | 2 +- .../IssuanceDeferredRequestTest.swift | 2 +- Tests/Issuance/IssuanceEncryptionTest.swift | 4 +-- Tests/Issuance/IssuanceNotificationTest.swift | 4 +-- .../Issuance/IssuanceSingleRequestTest.swift | 4 +-- 12 files changed, 46 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 1a83697..74620e7 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ endpoints. OpenId4VCI specification defines several extension points to accommodate the differences across Credential formats. The current version of the library fully supports **ISO mDL** profile and gives some initial support for **IETF SD-JWT VC** profile. #### Proof Types -OpenId4VCI specification (draft 12) defines two types of proofs that can be included in a credential issuance request, JWT proof type and CWT proof type. Current version of the library supports only JWT proof types +OpenId4VCI specification (draft 14) defines proofs that can be included in a credential issuance request, JWT proof type in particular. The current version of the library supports JWT proof types. ## How to contribute diff --git a/Sources/Entities/CredentialIssuer/ProofType.swift b/Sources/Entities/CredentialIssuer/ProofType.swift index fe0ae76..d736c56 100644 --- a/Sources/Entities/CredentialIssuer/ProofType.swift +++ b/Sources/Entities/CredentialIssuer/ProofType.swift @@ -15,10 +15,10 @@ */ import Foundation -public enum ProofType: Codable { +public enum ProofType: Decodable { case jwt - case cwt case ldpVp + case unsupported private enum CodingKeys: String, CodingKey { case rawValue @@ -31,25 +31,10 @@ public enum ProofType: Codable { switch rawValue { case "JWT", "jwt": self = .jwt - case "CWT", "cwt": - self = .cwt case "LDP_VP", "ldp_vp": self = .ldpVp default: - throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid proof type") - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - switch self { - case .jwt: - try container.encode("JWT") - case .cwt: - try container.encode("CWT") - case .ldpVp: - try container.encode("LDP_VP") + self = .unsupported } } @@ -57,12 +42,10 @@ public enum ProofType: Codable { switch type { case "JWT", "jwt": self = .jwt - case "CWT", "cwt": - self = .cwt case "LDP_VP", "ldp_vp": self = .ldpVp default: - throw ValidationError.error(reason: "Invalid proof type: \(type)") + self = .unsupported } } } diff --git a/Sources/Entities/IssuanceFlows/AuthCodeFlowIssuance.swift b/Sources/Entities/IssuanceFlows/AuthCodeFlowIssuance.swift index c0c269f..d5dc1b7 100644 --- a/Sources/Entities/IssuanceFlows/AuthCodeFlowIssuance.swift +++ b/Sources/Entities/IssuanceFlows/AuthCodeFlowIssuance.swift @@ -17,15 +17,27 @@ import Foundation public enum AuthCodeFlowIssuance { // State denoting that the pushed authorization request has been placed successfully and response processed - case parRequested(getAuthorizationCodeURL: GetAuthorizationCodeURL, pkceVerifier: PKCEVerifier, state: String) + case parRequested( + getAuthorizationCodeURL: GetAuthorizationCodeURL, + pkceVerifier: PKCEVerifier, + state: String + ) // State denoting that the caller has followed the URL and response received from the authorization server and processed successfully - case authorized(authorizationCode: IssuanceAuthorization, pkceVerifier: PKCEVerifier) + case authorized( + authorizationCode: IssuanceAuthorization, + pkceVerifier: PKCEVerifier + ) // State denoting that the access token was requested from the authorization server and response received and processed successfully - case accessTokenRetrieved(token: IssuanceAccessToken) + case accessTokenRetrieved( + token: IssuanceAccessToken + ) // State denoting that the certificate issuance was requested and certificate issued and received successfully - case issued(issuedAt: Date, certificate: IssuedCertificate) + case issued( + issuedAt: Date, + certificate: IssuedCertificate + ) } diff --git a/Sources/Entities/Types/Types.swift b/Sources/Entities/Types/Types.swift index 1a2c0fa..6135a4f 100644 --- a/Sources/Entities/Types/Types.swift +++ b/Sources/Entities/Types/Types.swift @@ -40,14 +40,11 @@ public struct IssuanceResponseEncryptionSpec { public enum Proof: Codable { case jwt(JWT) - case cwt(String) public func type() -> ProofType { switch self { case .jwt: return .jwt - case .cwt: - return .cwt } } @@ -58,8 +55,6 @@ public enum Proof: Codable { if let jwt = try? container.decode(JWT.self) { self = .jwt(jwt) - } else if let cwt = try? container.decode(String.self) { - self = .cwt(cwt) } else { throw DecodingError.typeMismatch(Proof.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Invalid proof type")) } @@ -74,8 +69,6 @@ public enum Proof: Codable { "proof_type": "jwt", "jwt": jwt ]) - case .cwt(let cwt): - try container.encode(cwt) } } @@ -86,8 +79,6 @@ public enum Proof: Codable { "proof_type": "jwt", "jwt": jwt ] - case .cwt: - throw ValidationError.error(reason: "CWT not supported yet") } } } diff --git a/Sources/Issuers/Issuer.swift b/Sources/Issuers/Issuer.swift index d36e744..fce414c 100644 --- a/Sources/Issuers/Issuer.swift +++ b/Sources/Issuers/Issuer.swift @@ -34,7 +34,7 @@ public protocol IssuerType { authorizationCode: IssuanceAuthorization ) async -> Result - func requestAccessToken( + func authorizeWithAuthorizationCode( authorizationCode: UnauthorizedRequest ) async -> Result @@ -275,14 +275,14 @@ public actor Issuer: IssuerType { } } - public func requestAccessToken(authorizationCode: UnauthorizedRequest) async -> Result { + public func authorizeWithAuthorizationCode(authorizationCode: UnauthorizedRequest) async -> Result { switch authorizationCode { case .par: return .failure(ValidationError.error(reason: ".authorizationCode case is required")) case .authorizationCode(let request): switch request.authorizationCode { - case .authorizationCode(authorizationCode: let authorizationCode): + case .authorizationCode(let authorizationCode): do { let response: ( accessToken: IssuanceAccessToken, @@ -739,9 +739,16 @@ public extension Issuer { static func createResponseEncryptionSpec(_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? { switch issuerResponseEncryptionMetadata { case .notRequired: - return Self.createResponseEncryptionSpecFrom(algorithmsSupported: [.init(.RSA_OAEP_256)], encryptionMethodsSupported: [.init(.A128CBC_HS256)]) + return Self.createResponseEncryptionSpecFrom( + algorithmsSupported: [.init(.RSA_OAEP_256)], + encryptionMethodsSupported: [.init(.A128CBC_HS256)] + ) + case let .required(algorithmsSupported, encryptionMethodsSupported): - return Self.createResponseEncryptionSpecFrom(algorithmsSupported: algorithmsSupported, encryptionMethodsSupported: encryptionMethodsSupported) + return Self.createResponseEncryptionSpecFrom( + algorithmsSupported: algorithmsSupported, + encryptionMethodsSupported: encryptionMethodsSupported + ) } } diff --git a/Tests/Helpers/Wallet.swift b/Tests/Helpers/Wallet.swift index 42cd72a..ee9e747 100644 --- a/Tests/Helpers/Wallet.swift +++ b/Tests/Helpers/Wallet.swift @@ -439,7 +439,7 @@ extension Wallet { switch unAuthorized { case .success(let request): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: request) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: request) if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _, _) = authorized { print("--> [AUTHORIZATION] Authorization code exchanged with access token : \(token.accessToken)") diff --git a/Tests/Issuance/IssuanceAuthorizationTest.swift b/Tests/Issuance/IssuanceAuthorizationTest.swift index 6b64752..a7495e0 100644 --- a/Tests/Issuance/IssuanceAuthorizationTest.swift +++ b/Tests/Issuance/IssuanceAuthorizationTest.swift @@ -142,7 +142,7 @@ class IssuanceAuthorizationTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _, _) = authorized { @@ -195,7 +195,7 @@ class IssuanceAuthorizationTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) if case let .success(authorized) = authorizedRequest, case let .proofRequired(token, _, _, _, _) = authorized { XCTAssert(true, "Got access token: \(token)") @@ -247,7 +247,7 @@ class IssuanceAuthorizationTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) switch authorizedRequest { case .success: @@ -323,6 +323,7 @@ class IssuanceAuthorizationTest: XCTestCase { /// Replace the url string below with the one you can generate here: https://trial.authlete.net/api/offer/issue let urlString = """ + https://trial.authlete.net/api/offer/aCyfhLWvufb5T_BB_aCVhlk1GhnAqKA_tsz_m3v48jI """ if urlString.isEmpty { diff --git a/Tests/Issuance/IssuanceBatchRequestTest.swift b/Tests/Issuance/IssuanceBatchRequestTest.swift index 18e5656..a7ee101 100644 --- a/Tests/Issuance/IssuanceBatchRequestTest.swift +++ b/Tests/Issuance/IssuanceBatchRequestTest.swift @@ -100,7 +100,7 @@ class IssuanceBatchRequestTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _, _) = authorized { diff --git a/Tests/Issuance/IssuanceDeferredRequestTest.swift b/Tests/Issuance/IssuanceDeferredRequestTest.swift index 648b1a1..da7b27a 100644 --- a/Tests/Issuance/IssuanceDeferredRequestTest.swift +++ b/Tests/Issuance/IssuanceDeferredRequestTest.swift @@ -98,7 +98,7 @@ class IssuanceDeferredRequestTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _, _) = authorized { diff --git a/Tests/Issuance/IssuanceEncryptionTest.swift b/Tests/Issuance/IssuanceEncryptionTest.swift index ae87a0c..4161493 100644 --- a/Tests/Issuance/IssuanceEncryptionTest.swift +++ b/Tests/Issuance/IssuanceEncryptionTest.swift @@ -224,7 +224,7 @@ extension IssuanceEncryptionTest { } if case .authorizationCode = unAuthorized { - guard let authorizedRequest = try? await issuer.requestAccessToken(authorizationCode: unAuthorized).get() else { + guard let authorizedRequest = try? await issuer.authorizeWithAuthorizationCode(authorizationCode: unAuthorized).get() else { XCTAssert(false, "Could not get authorized request") return nil } @@ -284,7 +284,7 @@ extension IssuanceEncryptionTest { } if case .authorizationCode = unAuthorized { - guard let authorizedRequest = try? await issuer.requestAccessToken(authorizationCode: unAuthorized).get() else { + guard let authorizedRequest = try? await issuer.authorizeWithAuthorizationCode(authorizationCode: unAuthorized).get() else { XCTAssert(false, "Could not get authorized request") return nil } diff --git a/Tests/Issuance/IssuanceNotificationTest.swift b/Tests/Issuance/IssuanceNotificationTest.swift index 91a9cec..540068a 100644 --- a/Tests/Issuance/IssuanceNotificationTest.swift +++ b/Tests/Issuance/IssuanceNotificationTest.swift @@ -105,7 +105,7 @@ class IssuanceNotificationTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _, _) = authorized { @@ -248,7 +248,7 @@ class IssuanceNotificationTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _, _) = authorized { diff --git a/Tests/Issuance/IssuanceSingleRequestTest.swift b/Tests/Issuance/IssuanceSingleRequestTest.swift index b23cdf8..bec2f90 100644 --- a/Tests/Issuance/IssuanceSingleRequestTest.swift +++ b/Tests/Issuance/IssuanceSingleRequestTest.swift @@ -99,7 +99,7 @@ class IssuanceSingleRequestTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, _, _) = authorized { @@ -360,7 +360,7 @@ class IssuanceSingleRequestTest: XCTestCase { switch unAuthorized { case .success(let authorizationCode): - let authorizedRequest = await issuer.requestAccessToken(authorizationCode: authorizationCode) + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) if case let .success(authorized) = authorizedRequest, case let .noProofRequired(token, _, identifiers, _) = authorized { From af4bbbe39b065db1415f96080a4293a200818e67 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 19 Nov 2024 09:21:10 +0200 Subject: [PATCH 02/21] [fix] issued credentials as string or json --- Sources/Entities/Credential/Credential.swift | 49 ++++++++++++++++ .../IssuedCredential.swift | 10 ++-- .../Entities/Issuance/SubmittedRequest.swift | 35 ++++------- .../SingleIssuanceSuccessResponse.swift | 58 +++++++++++++++---- .../DeferredCredentialIssuanceResponse.swift | 14 +++-- Sources/Issuers/IssuanceRequester.swift | 27 +++++---- Tests/Helpers/Wallet.swift | 26 ++++----- .../Issuance/IssuanceAuthorizationTest.swift | 28 ++++++--- Tests/Issuance/IssuanceBatchRequestTest.swift | 6 +- .../IssuanceDeferredRequestTest.swift | 2 +- Tests/Issuance/IssuanceNotificationTest.swift | 4 +- .../Issuance/IssuanceSingleRequestTest.swift | 6 +- 12 files changed, 182 insertions(+), 83 deletions(-) create mode 100644 Sources/Entities/Credential/Credential.swift rename Sources/Entities/{Types => Credential}/IssuedCredential.swift (81%) diff --git a/Sources/Entities/Credential/Credential.swift b/Sources/Entities/Credential/Credential.swift new file mode 100644 index 0000000..0483a9c --- /dev/null +++ b/Sources/Entities/Credential/Credential.swift @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import SwiftyJSON + +public enum Credential: Codable { + case string(String) + case json(JSON) + + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let stringValue = try? container.decode(String.self) { + self = .string(stringValue) + } else if let jsonObject = try? container.decode(JSON.self) { + self = .json(jsonObject) + } else { + throw DecodingError.typeMismatch( + Credential.self, + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "Invalid Credential Type" + ) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case .string(let value): + try container.encode(value) + case .json(let jsonValue): + try container.encode(jsonValue) + } + } +} diff --git a/Sources/Entities/Types/IssuedCredential.swift b/Sources/Entities/Credential/IssuedCredential.swift similarity index 81% rename from Sources/Entities/Types/IssuedCredential.swift rename to Sources/Entities/Credential/IssuedCredential.swift index f485b0d..5049efe 100644 --- a/Sources/Entities/Types/IssuedCredential.swift +++ b/Sources/Entities/Credential/IssuedCredential.swift @@ -14,12 +14,14 @@ * limitations under the License. */ import Foundation +import SwiftyJSON -public enum IssuedCredential { +public enum IssuedCredential: Codable { case issued( - format: String, - credential: String, - notificationId: String + format: String?, + credential: Credential, + notificationId: String?, + additionalInfo: JSON? ) case deferred(transactionId: TransactionId) } diff --git a/Sources/Entities/Issuance/SubmittedRequest.swift b/Sources/Entities/Issuance/SubmittedRequest.swift index a68b6d8..a86c261 100644 --- a/Sources/Entities/Issuance/SubmittedRequest.swift +++ b/Sources/Entities/Issuance/SubmittedRequest.swift @@ -15,16 +15,14 @@ */ import Foundation -public struct CredentialIssuanceResponse: Codable { - public let credentialResponses: [Result] +public struct CredentialIssuanceResponse: Decodable { + public let credentialResponses: [IssuedCredential] public let cNonce: CNonce? - public enum Result: Codable { - case deferred(transactionId: TransactionId) - case issued(credential: String, notificationId: NotificationId?) - } - - public init(credentialResponses: [Result], cNonce: CNonce?) { + public init( + credentialResponses: [IssuedCredential], + cNonce: CNonce? + ) { self.credentialResponses = credentialResponses self.cNonce = cNonce } @@ -33,21 +31,8 @@ public struct CredentialIssuanceResponse: Codable { public enum SubmittedRequest { case success(response: CredentialIssuanceResponse) case failed(error: CredentialIssuanceError) - case invalidProof(cNonce: CNonce, errorDescription: String?) - - var credentials: [String] { - switch self { - case .success(let response): - response.credentialResponses.compactMap { result in - switch result { - case .issued(let credential, _): - credential - default: - nil - } - } - default: - [] - } - } + case invalidProof( + cNonce: CNonce, + errorDescription: String? + ) } diff --git a/Sources/Entities/Profiles/SingleIssuanceSuccessResponse.swift b/Sources/Entities/Profiles/SingleIssuanceSuccessResponse.swift index 6b10c8c..6fed85e 100644 --- a/Sources/Entities/Profiles/SingleIssuanceSuccessResponse.swift +++ b/Sources/Entities/Profiles/SingleIssuanceSuccessResponse.swift @@ -14,9 +14,11 @@ * limitations under the License. */ import Foundation +import SwiftyJSON public struct SingleIssuanceSuccessResponse: Codable { - public let credential: String? + public let credential: JSON? + public let credentials: JSON? public let transactionId: String? public let notificationId: String? public let cNonce: String? @@ -24,6 +26,7 @@ public struct SingleIssuanceSuccessResponse: Codable { enum CodingKeys: String, CodingKey { case credential + case credentials case transactionId = "transaction_id" case notificationId = "notification_id" case cNonce = "c_nonce" @@ -31,13 +34,15 @@ public struct SingleIssuanceSuccessResponse: Codable { } public init( - credential: String?, + credential: JSON?, + credentials: JSON?, transactionId: String?, notificationId: String?, cNonce: String?, cNonceExpiresInSeconds: Int? ) { self.credential = credential + self.credentials = credentials self.transactionId = transactionId self.notificationId = notificationId self.cNonce = cNonce @@ -49,17 +54,50 @@ public extension SingleIssuanceSuccessResponse { func toDomain() throws -> CredentialIssuanceResponse { if let transactionId = transactionId { - return CredentialIssuanceResponse( - credentialResponses: [.deferred(transactionId: try .init(value: transactionId))], - cNonce: CNonce(value: cNonce, expiresInSeconds: cNonceExpiresInSeconds) + return .init( + credentialResponses: [ + .deferred(transactionId: try .init(value: transactionId)) + ], + cNonce: .init( + value: cNonce, + expiresInSeconds: cNonceExpiresInSeconds + ) ) - } else if let credential = credential { - return CredentialIssuanceResponse( - credentialResponses: [.issued(credential: credential, notificationId: nil)], - cNonce: CNonce(value: cNonce, expiresInSeconds: cNonceExpiresInSeconds) + } else if let credential = credential, + let string = credential.string { + return .init( + credentialResponses: [ + .issued( + format: nil, + credential: .string(string), + notificationId: nil, + additionalInfo: nil + ) + ], + cNonce: .init( + value: cNonce, + expiresInSeconds: cNonceExpiresInSeconds + ) + ) + } else if let credentials = credentials, + let jsonObject = credentials.dictionary, + !jsonObject.isEmpty { + return .init( + credentialResponses: [ + .issued( + format: nil, + credential: .json(JSON(jsonObject)), + notificationId: nil, + additionalInfo: nil + ) + ], + cNonce: .init( + value: cNonce, + expiresInSeconds: cNonceExpiresInSeconds + ) ) } else { - throw ValidationError.error(reason: "CredentialIssuanceResponse unpareable") + throw ValidationError.error(reason: "CredentialIssuanceResponse unparseable") } } diff --git a/Sources/Entities/Types/DeferredCredentialIssuanceResponse.swift b/Sources/Entities/Types/DeferredCredentialIssuanceResponse.swift index 400399a..5404027 100644 --- a/Sources/Entities/Types/DeferredCredentialIssuanceResponse.swift +++ b/Sources/Entities/Types/DeferredCredentialIssuanceResponse.swift @@ -16,13 +16,14 @@ import Foundation public enum DeferredCredentialIssuanceResponse: Codable { - case issued(credential: String) + case issued(credential: Credential) case issuancePending(transactionId: TransactionId) case errored(error: String?, errorDescription: String?) private enum CodingKeys: String, CodingKey { case type case credential + case credentials case transactionId = "transaction_id" case error case errorDescription = "error_description" @@ -32,9 +33,14 @@ public enum DeferredCredentialIssuanceResponse: Codable { let container = try decoder.container(keyedBy: CodingKeys.self) if let transactionId = try? container.decode(String.self, forKey: .transactionId) { self = .issuancePending(transactionId: try .init(value: transactionId)) - } else if let credential = try? container.decode(String.self, forKey: .credential) { - self = .issued(credential: credential) - } else { + + } else if let credential = try? container.decode(Credential.self, forKey: .credential) { + self = .issued(credential: credential) + + } else if let credentials = try? container.decode(Credential.self, forKey: .credentials) { + self = .issued(credential: credentials) + + } else { self = .errored( error: try? container.decode(String.self, forKey: .error), errorDescription: try? container.decodeIfPresent(String.self, forKey: .errorDescription) diff --git a/Sources/Issuers/IssuanceRequester.swift b/Sources/Issuers/IssuanceRequester.swift index 53b74bf..ff03217 100644 --- a/Sources/Issuers/IssuanceRequester.swift +++ b/Sources/Issuers/IssuanceRequester.swift @@ -355,10 +355,21 @@ private extension IssuanceRequester { private extension SingleIssuanceSuccessResponse { func toSingleIssuanceResponse() throws -> CredentialIssuanceResponse { - if let credential = credential { + if let credential = credential, + let string = credential.string { return CredentialIssuanceResponse( - credentialResponses: [.issued(credential: credential, notificationId: nil)], - cNonce: CNonce(value: cNonce, expiresInSeconds: cNonceExpiresInSeconds) + credentialResponses: [ + .issued( + format: nil, + credential: .string(string), + notificationId: nil, + additionalInfo: nil + ) + ], + cNonce: .init( + value: cNonce, + expiresInSeconds: cNonceExpiresInSeconds + ) ) } else if let transactionId = transactionId { return CredentialIssuanceResponse( @@ -373,15 +384,9 @@ private extension SingleIssuanceSuccessResponse { private extension BatchIssuanceSuccessResponse { func toBatchIssuanceResponse() throws -> CredentialIssuanceResponse { - func mapResults() throws -> [CredentialIssuanceResponse.Result] { + func mapResults() throws -> [IssuedCredential] { return try credentialResponses.map { response in - if let transactionId = response.transactionId { - return .deferred(transactionId: try .init(value: transactionId)) - } else if let credential = response.credential { - return .issued(credential: credential, notificationId: nil) - } else { - throw CredentialIssuanceError.responseUnparsable("Got success response for issuance but response misses 'transaction_id' and 'certificate' parameters") - } + throw CredentialIssuanceError.responseUnparsable("Deprecated, will be removed") } } diff --git a/Tests/Helpers/Wallet.swift b/Tests/Helpers/Wallet.swift index ee9e747..4c1c44c 100644 --- a/Tests/Helpers/Wallet.swift +++ b/Tests/Helpers/Wallet.swift @@ -51,7 +51,7 @@ extension Wallet { func issueByCredentialIdentifier( _ identifier: String, claimSet: ClaimSet? = nil - ) async throws -> String { + ) async throws -> Credential { let credentialConfigurationIdentifier = try CredentialConfigurationIdentifier(value: identifier) let credentialIssuerIdentifier = try CredentialIssuerId(CREDENTIAL_ISSUER_PUBLIC_URL) @@ -99,7 +99,7 @@ extension Wallet { private func issueMultipleOfferedCredentialWithProof( offer: CredentialOffer, claimSet: ClaimSet? = nil - ) async throws -> [(String, String)] { + ) async throws -> [(String, Credential)] { let issuerMetadata = offer.credentialIssuerMetadata let issuer = try Issuer( @@ -161,7 +161,7 @@ extension Wallet { offer: CredentialOffer, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil - ) async throws -> String { + ) async throws -> Credential { let issuer = try Issuer( authorizationServerMetadata: offer.authorizationServerMetadata, @@ -204,7 +204,7 @@ extension Wallet { func issueByCredentialOfferUrlMultipleFormats( offerUri: String, claimSet: ClaimSet? = nil - ) async throws -> [(String, String)] { + ) async throws -> [(String, Credential)] { let resolver = CredentialOfferRequestResolver( fetcher: Fetcher(session: self.session), credentialIssuerMetadataResolver: CredentialIssuerMetadataResolver( @@ -237,7 +237,7 @@ extension Wallet { offerUri: String, scope: String, claimSet: ClaimSet? = nil - ) async throws -> String { + ) async throws -> Credential { let result = await CredentialOfferRequestResolver( fetcher: Fetcher(session: self.session), credentialIssuerMetadataResolver: CredentialIssuerMetadataResolver( @@ -269,7 +269,7 @@ extension Wallet { offerUri: String, scope: String, claimSet: ClaimSet? = nil - ) async throws -> String { + ) async throws -> Credential { let result = await CredentialOfferRequestResolver( fetcher: Fetcher(session: self.session), credentialIssuerMetadataResolver: CredentialIssuerMetadataResolver( @@ -301,7 +301,7 @@ extension Wallet { offer: CredentialOffer, scope: String, claimSet: ClaimSet? = nil - ) async throws -> String { + ) async throws -> Credential { let issuerMetadata = offer.credentialIssuerMetadata guard let credentialConfigurationIdentifier = issuerMetadata.credentialsSupported.keys.first(where: { $0.value == scope }) else { @@ -346,7 +346,7 @@ extension Wallet { offer: CredentialOffer, scope: String, claimSet: ClaimSet? = nil - ) async throws -> String { + ) async throws -> Credential { let issuerMetadata = offer.credentialIssuerMetadata guard let credentialConfigurationIdentifier = issuerMetadata.credentialsSupported.keys.first(where: { $0.value == scope }) else { @@ -465,7 +465,7 @@ extension Wallet { noProofRequiredState: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, claimSet: ClaimSet? = nil - ) async throws -> String { + ) async throws -> Credential { switch noProofRequiredState { case .noProofRequired: let payload: IssuanceRequestPayload = .configurationBased( @@ -490,7 +490,7 @@ extension Wallet { authorized: noProofRequiredState, transactionId: transactionId ) - case .issued(let credential, _): + case .issued(let format, let credential, _, _): return credential } } else { @@ -519,7 +519,7 @@ extension Wallet { authorized: AuthorizedRequest, credentialConfigurationIdentifier: CredentialConfigurationIdentifier?, claimSet: ClaimSet? = nil - ) async throws -> String { + ) async throws -> Credential { guard let credentialConfigurationIdentifier else { throw ValidationError.error(reason: "Credential configuration identifier not found") @@ -550,7 +550,7 @@ extension Wallet { authorized: authorized, transactionId: transactionId ) - case .issued(let credential, _): + case .issued(let format, let credential, _, _): return credential } } else { @@ -569,7 +569,7 @@ extension Wallet { issuer: Issuer, authorized: AuthorizedRequest, transactionId: TransactionId - ) async throws -> String { + ) async throws -> Credential { print("--> [ISSUANCE] Got a deferred issuance response from server with transaction_id \(transactionId.value). Retrying issuance...") let deferredRequestResponse = try await issuer.requestDeferredIssuance( diff --git a/Tests/Issuance/IssuanceAuthorizationTest.swift b/Tests/Issuance/IssuanceAuthorizationTest.swift index a7495e0..dfc6655 100644 --- a/Tests/Issuance/IssuanceAuthorizationTest.swift +++ b/Tests/Issuance/IssuanceAuthorizationTest.swift @@ -323,7 +323,6 @@ class IssuanceAuthorizationTest: XCTestCase { /// Replace the url string below with the one you can generate here: https://trial.authlete.net/api/offer/issue let urlString = """ - https://trial.authlete.net/api/offer/aCyfhLWvufb5T_BB_aCVhlk1GhnAqKA_tsz_m3v48jI """ if urlString.isEmpty { @@ -407,8 +406,13 @@ class IssuanceAuthorizationTest: XCTestCase { switch requestSingleResult { case .success(let request): - print(request.credentials.joined(separator: ", ")) - XCTAssertTrue(true) + switch request { + case .success(let response): + print(response.credentialResponses.map { try! $0.toDictionary() } ) + XCTAssertTrue(true) + default: + XCTAssert(false, "Unexpected request type") + } case .failure(let error): XCTAssert(false, error.localizedDescription) } @@ -500,8 +504,13 @@ class IssuanceAuthorizationTest: XCTestCase { switch requestSingleResult { case .success(let request): - print(request.credentials.joined(separator: ", ")) - XCTAssertTrue(true) + switch request { + case .success(let response): + print(response.credentialResponses.map { try! $0.toDictionary() } ) + XCTAssertTrue(true) + default: + XCTAssert(false, "Unexpected request type") + } case .failure(let error): XCTAssert(false, error.localizedDescription) } @@ -599,8 +608,13 @@ class IssuanceAuthorizationTest: XCTestCase { switch requestSingleResult { case .success(let request): - print(request.credentials.joined(separator: ", ")) - XCTAssertTrue(true) + switch request { + case .success(let response): + print(response.credentialResponses.map { try! $0.toDictionary() } ) + XCTAssertTrue(true) + default: + XCTAssert(false, "Unexpected request type") + } case .failure(let error): XCTAssert(false, error.localizedDescription) } diff --git a/Tests/Issuance/IssuanceBatchRequestTest.swift b/Tests/Issuance/IssuanceBatchRequestTest.swift index a7ee101..f9c8d72 100644 --- a/Tests/Issuance/IssuanceBatchRequestTest.swift +++ b/Tests/Issuance/IssuanceBatchRequestTest.swift @@ -152,7 +152,7 @@ class IssuanceBatchRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } @@ -307,7 +307,7 @@ class IssuanceBatchRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } @@ -350,7 +350,7 @@ class IssuanceBatchRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } diff --git a/Tests/Issuance/IssuanceDeferredRequestTest.swift b/Tests/Issuance/IssuanceDeferredRequestTest.swift index da7b27a..7b19e50 100644 --- a/Tests/Issuance/IssuanceDeferredRequestTest.swift +++ b/Tests/Issuance/IssuanceDeferredRequestTest.swift @@ -128,7 +128,7 @@ class IssuanceDeferredRequestTest: XCTestCase { case .deferred(let transactionId): XCTAssert(true, "transaction_id: \(transactionId)") return - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(false, "credential: \(credential)") } } else { diff --git a/Tests/Issuance/IssuanceNotificationTest.swift b/Tests/Issuance/IssuanceNotificationTest.swift index 540068a..c3ec44e 100644 --- a/Tests/Issuance/IssuanceNotificationTest.swift +++ b/Tests/Issuance/IssuanceNotificationTest.swift @@ -134,7 +134,7 @@ class IssuanceNotificationTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(true, "credential: \(credential)") let result = try await issuer.notify( @@ -277,7 +277,7 @@ class IssuanceNotificationTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(true, "credential: \(credential)") let result = try await issuer.notify( diff --git a/Tests/Issuance/IssuanceSingleRequestTest.swift b/Tests/Issuance/IssuanceSingleRequestTest.swift index bec2f90..ad12665 100644 --- a/Tests/Issuance/IssuanceSingleRequestTest.swift +++ b/Tests/Issuance/IssuanceSingleRequestTest.swift @@ -137,7 +137,7 @@ class IssuanceSingleRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } @@ -270,7 +270,7 @@ class IssuanceSingleRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } @@ -398,7 +398,7 @@ class IssuanceSingleRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let credential, _): + case .issued(let format, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } From d3c1447930d7dd1d2a3590f55beffdba92cc6c82 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 19 Nov 2024 09:44:31 +0200 Subject: [PATCH 03/21] [fix] remove batch issuance support --- README.md | 5 +- .../CredentialIssuerMetadata.swift | 6 - .../Errors/CredentialIssuanceError.swift | 3 - ...dentialIssuerMetadataValidationError.swift | 4 - .../CredentialIssuanceRequest.swift | 1 - Sources/Entities/Types/Types.swift | 18 - Sources/Issuers/IssuanceRequester.swift | 54 --- Sources/Issuers/Issuer.swift | 109 ----- .../europa/credential_issuer_metadata.json | 1 - ...redential_issuer_metadata_valid_algos.json | 1 - ...credential-issuer_encrypted_responses.json | 1 - ...penid-credential-issuer_no_encryption.json | 1 - Tests/Issuance/IssuanceBatchRequestTest.swift | 376 ------------------ .../CredentialOfferResolverTests.swift | 2 - 14 files changed, 2 insertions(+), 580 deletions(-) delete mode 100644 Tests/Issuance/IssuanceBatchRequestTest.swift diff --git a/README.md b/README.md index 74620e7..cb18e70 100644 --- a/README.md +++ b/README.md @@ -229,12 +229,11 @@ Specification defines ([section 6.2](https://openid.github.io/OpenID4VCI/openid- if `authorization_details` parameter is used in authorization endpoint. Current version of library is not parsing/utilizing this response attribute. ### Credential Request -Current version of the library implements integrations with issuer's [Crednetial Endpoint](https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#name-credential-endpoint), -[Batch Crednetial Endpoint](https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#name-batch-credential-endpoint) and +Current version of the library implements integrations with issuer's [Crednetial Endpoint](https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#name-credential-endpoint) and [Deferred Crednetial Endpoint](https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html#name-deferred-credential-endpoin) endpoints. -**NOTE:** Attribute `credential_identifier` of a credential request (single or batch) is not yet supported. +**NOTE:** Attribute `credential_identifier` of a credential request is not yet supported. #### Credential Format Profiles OpenId4VCI specification defines several extension points to accommodate the differences across Credential formats. The current version of the library fully supports **ISO mDL** profile and gives some initial support for **IETF SD-JWT VC** profile. diff --git a/Sources/Entities/CredentialIssuer/CredentialIssuerMetadata.swift b/Sources/Entities/CredentialIssuer/CredentialIssuerMetadata.swift index e22238c..771f283 100644 --- a/Sources/Entities/CredentialIssuer/CredentialIssuerMetadata.swift +++ b/Sources/Entities/CredentialIssuer/CredentialIssuerMetadata.swift @@ -21,7 +21,6 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { public let credentialIssuerIdentifier: CredentialIssuerId public let authorizationServers: [URL]? public let credentialEndpoint: CredentialIssuerEndpoint - public let batchCredentialEndpoint: CredentialIssuerEndpoint? public let deferredCredentialEndpoint: CredentialIssuerEndpoint? public let notificationEndpoint: CredentialIssuerEndpoint? public let credentialResponseEncryption: CredentialResponseEncryption @@ -34,7 +33,6 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { case credentialIssuerIdentifier = "credential_issuer" case authorizationServers = "authorization_servers" case credentialEndpoint = "credential_endpoint" - case batchCredentialEndpoint = "batch_credential_endpoint" case deferredCredentialEndpoint = "deferred_credential_endpoint" case notificationEndpoint = "notification_endpoint" case credentialResponseEncryptionAlgorithmsSupported = "credential_response_encryption_alg_values_supported" @@ -51,7 +49,6 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { credentialIssuerIdentifier: CredentialIssuerId, authorizationServers: [URL], credentialEndpoint: CredentialIssuerEndpoint, - batchCredentialEndpoint: CredentialIssuerEndpoint?, deferredCredentialEndpoint: CredentialIssuerEndpoint?, notificationEndpoint: CredentialIssuerEndpoint?, credentialResponseEncryption: CredentialResponseEncryption = .notRequired, @@ -64,7 +61,6 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { self.authorizationServers = authorizationServers self.credentialEndpoint = credentialEndpoint - self.batchCredentialEndpoint = batchCredentialEndpoint self.deferredCredentialEndpoint = deferredCredentialEndpoint self.notificationEndpoint = notificationEndpoint @@ -82,7 +78,6 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { credentialIssuerIdentifier: .init(Constants.url), authorizationServers: [], credentialEndpoint: .init(string: Constants.url), - batchCredentialEndpoint: nil, deferredCredentialEndpoint: deferredCredentialEndpoint, notificationEndpoint: nil, credentialConfigurationsSupported: [:], @@ -102,7 +97,6 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { authorizationServers = servers ?? [credentialIssuerIdentifier.url] credentialEndpoint = try container.decode(CredentialIssuerEndpoint.self, forKey: .credentialEndpoint) - batchCredentialEndpoint = try container.decodeIfPresent(CredentialIssuerEndpoint.self, forKey: .batchCredentialEndpoint) deferredCredentialEndpoint = try container.decodeIfPresent(CredentialIssuerEndpoint.self, forKey: .deferredCredentialEndpoint) notificationEndpoint = try container.decodeIfPresent(CredentialIssuerEndpoint.self, forKey: .notificationEndpoint) diff --git a/Sources/Entities/Errors/CredentialIssuanceError.swift b/Sources/Entities/Errors/CredentialIssuanceError.swift index 023d3f8..a162ddb 100644 --- a/Sources/Entities/Errors/CredentialIssuanceError.swift +++ b/Sources/Entities/Errors/CredentialIssuanceError.swift @@ -18,7 +18,6 @@ import Foundation public enum CredentialIssuanceError: Error, LocalizedError { case pushedAuthorizationRequestFailed(error: String, errorDescription: String?) case accessTokenRequestFailed(error: String, errorDescription: String?) - case issuerDoesNotSupportBatchIssuance case responseUnparsable(String) case invalidIssuanceRequest(String) case cryptographicSuiteNotSupported(String) @@ -47,8 +46,6 @@ public enum CredentialIssuanceError: Error, LocalizedError { .issuanceRequestFailed(_, let errorDescription), .invalidProof(_, _, let errorDescription): return errorDescription - case .issuerDoesNotSupportBatchIssuance: - return "Issuer does not support batch issuance" case .responseUnparsable(let details): return "Response is unparsable. Details: \(details)" case .invalidIssuanceRequest(let details): diff --git a/Sources/Entities/Errors/CredentialIssuerMetadataValidationError.swift b/Sources/Entities/Errors/CredentialIssuerMetadataValidationError.swift index c2dd35e..bbe7cab 100644 --- a/Sources/Entities/Errors/CredentialIssuerMetadataValidationError.swift +++ b/Sources/Entities/Errors/CredentialIssuerMetadataValidationError.swift @@ -29,10 +29,6 @@ public enum CredentialIssuerMetadataValidationError: Error { /// - Parameter reason: The reason for the invalidity. case invalidCredentialEndpoint(reason: String) - /// The batch credential endpoint is invalid. - /// - Parameter reason: The reason for the invalidity. - case invalidBatchCredentialEndpoint(reason: String) - /// The deferred credential endpoint is invalid. /// - Parameter reason: The reason for the invalidity. case invalidDeferredCredentialEndpoint(reason: String) diff --git a/Sources/Entities/IssuanceFlows/CredentialIssuanceRequest.swift b/Sources/Entities/IssuanceFlows/CredentialIssuanceRequest.swift index 02de37b..38e65bf 100644 --- a/Sources/Entities/IssuanceFlows/CredentialIssuanceRequest.swift +++ b/Sources/Entities/IssuanceFlows/CredentialIssuanceRequest.swift @@ -60,7 +60,6 @@ public extension MsoMdocClaims { public enum CredentialIssuanceRequest { case single(SingleCredential, IssuanceResponseEncryptionSpec?) - case batch([SingleCredential], IssuanceResponseEncryptionSpec?) } public struct DeferredCredentialRequest: Codable { diff --git a/Sources/Entities/Types/Types.swift b/Sources/Entities/Types/Types.swift index 6135a4f..b154061 100644 --- a/Sources/Entities/Types/Types.swift +++ b/Sources/Entities/Types/Types.swift @@ -151,24 +151,6 @@ public struct CNonce: Codable { } } -public struct BatchIssuanceSuccessResponse: Codable { - public let credentialResponses: [CertificateIssuanceResponse] - public let cNonce: String? - public let cNonceExpiresInSeconds: Int? - - enum CodingKeys: String, CodingKey { - case credentialResponses = "credentials" - case cNonce = "c_nonce" - case cNonceExpiresInSeconds = "c_nonce_expires_in" - } - - public init(credentialResponses: [CertificateIssuanceResponse], cNonce: String?, cNonceExpiresInSeconds: Int?) { - self.credentialResponses = credentialResponses - self.cNonce = cNonce - self.cNonceExpiresInSeconds = cNonceExpiresInSeconds - } -} - public struct Claim: Codable { public let mandatory: Bool? public let valueType: String? diff --git a/Sources/Issuers/IssuanceRequester.swift b/Sources/Issuers/IssuanceRequester.swift index ff03217..dfaa116 100644 --- a/Sources/Issuers/IssuanceRequester.swift +++ b/Sources/Issuers/IssuanceRequester.swift @@ -26,11 +26,6 @@ public protocol IssuanceRequesterType { request: SingleCredential ) async throws -> Result - func placeBatchIssuanceRequest( - accessToken: IssuanceAccessToken, - request: [SingleCredential] - ) async throws -> Result - func placeDeferredCredentialRequest( accessToken: IssuanceAccessToken, transactionId: TransactionId, @@ -181,40 +176,6 @@ public actor IssuanceRequester: IssuanceRequesterType { } } - public func placeBatchIssuanceRequest( - accessToken: IssuanceAccessToken, - request: [SingleCredential] - ) async throws -> Result { - guard - let endpoint = issuerMetadata.batchCredentialEndpoint?.url - else { - throw CredentialIssuanceError.issuerDoesNotSupportBatchIssuance - } - - do { - let authorizationHeader: [String: Any] = try accessToken.dPoPOrBearerAuthorizationHeader( - dpopConstructor: dpopConstructor, - endpoint: endpoint - ) - - let encodedRequest: [JSON] = try request - .map { try $0.toDictionary() } - - let merged = authorizationHeader.merging(["credential_requests": encodedRequest]) { (_, new) in new } - - let response: BatchIssuanceSuccessResponse = try await service.formPost( - poster: poster, - url: endpoint, - headers: [:], - body: merged - ) - return .success(try response.toBatchIssuanceResponse()) - - } catch { - return .failure(ValidationError.error(reason: error.localizedDescription)) - } - } - public func placeDeferredCredentialRequest( accessToken: IssuanceAccessToken, transactionId: TransactionId, @@ -381,18 +342,3 @@ private extension SingleIssuanceSuccessResponse { throw CredentialIssuanceError.responseUnparsable("Got success response for issuance but response misses 'transaction_id' and 'certificate' parameters") } } - -private extension BatchIssuanceSuccessResponse { - func toBatchIssuanceResponse() throws -> CredentialIssuanceResponse { - func mapResults() throws -> [IssuedCredential] { - return try credentialResponses.map { response in - throw CredentialIssuanceError.responseUnparsable("Deprecated, will be removed") - } - } - - return CredentialIssuanceResponse( - credentialResponses: try mapResults(), - cNonce: CNonce(value: cNonce, expiresInSeconds: cNonceExpiresInSeconds) - ) - } -} diff --git a/Sources/Issuers/Issuer.swift b/Sources/Issuers/Issuer.swift index fce414c..5619203 100644 --- a/Sources/Issuers/Issuer.swift +++ b/Sources/Issuers/Issuer.swift @@ -60,19 +60,6 @@ public protocol IssuerType { authorizedRequest: AuthorizedRequest, notificationId: NotificationObject ) async throws -> Result - - func requestBatch( - noProofRequest: AuthorizedRequest, - requestPayload: [IssuanceRequestPayload], - responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? - ) async throws -> Result - - func requestBatch( - proofRequest: AuthorizedRequest, - bindingKey: BindingKey, - requestPayload: [IssuanceRequestPayload], - responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? - ) async throws -> Result } public actor Issuer: IssuerType { @@ -473,86 +460,6 @@ public actor Issuer: IssuerType { ) } } - - public func requestBatch( - noProofRequest: AuthorizedRequest, - requestPayload: [IssuanceRequestPayload], - responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? - ) async throws -> Result { - switch noProofRequest { - case .noProofRequired(let token, _, _, _): - return try await requestIssuance(token: token) { - let credentialRequests: [CredentialIssuanceRequest] = try requestPayload.map { identifier in - guard let supportedCredential = issuerMetadata - .credentialsSupported[identifier.credentialConfigurationIdentifier] else { - throw ValidationError.error(reason: "Invalid Supported credential for requestBatch") - } - return try supportedCredential.toIssuanceRequest( - requester: issuanceRequester, - claimSet: identifier.claimSet, - responseEncryptionSpecProvider: responseEncryptionSpecProvider - ) - } - - let batch: [SingleCredential] = credentialRequests.compactMap { credentialIssuanceRequest in - switch credentialIssuanceRequest { - case .single(let credential, _): - return credential - default: - return nil - } - } - return .batch( - batch, - deferredResponseEncryptionSpec - ) - } - default: return .failure(ValidationError.error(reason: ".noProofRequired is required")) - } - } - - public func requestBatch( - proofRequest: AuthorizedRequest, - bindingKey: BindingKey, - requestPayload: [IssuanceRequestPayload], - responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? - ) async throws -> Result { - switch proofRequest { - case .proofRequired(let token, _, let cNonce, _, _): - return try await requestIssuance(token: token) { - let credentialRequests: [CredentialIssuanceRequest] = try requestPayload.map { identifier in - guard let supportedCredential = issuerMetadata - .credentialsSupported[identifier.credentialConfigurationIdentifier] else { - throw ValidationError.error(reason: "Invalid Supported credential for requestBatch") - } - return try supportedCredential.toIssuanceRequest( - requester: issuanceRequester, - claimSet: identifier.claimSet, - proof: bindingKey.toSupportedProof( - issuanceRequester: issuanceRequester, - credentialSpec: supportedCredential, - cNonce: cNonce.value - ), - responseEncryptionSpecProvider: responseEncryptionSpecProvider - ) - } - - let batch: [SingleCredential] = credentialRequests.compactMap { credentialIssuanceRequest in - switch credentialIssuanceRequest { - case .single(let credential, _): - return credential - default: - return nil - } - } - return .batch( - batch, - deferredResponseEncryptionSpec - ) - } - default: return .failure(ValidationError.error(reason: ".noProofRequired is required")) - } - } } private extension Issuer { @@ -575,22 +482,6 @@ private extension Issuer { case .failure(let error): return handleIssuanceError(error) } - case .batch(let credentials, let encryptionSpec): - self.deferredResponseEncryptionSpec = encryptionSpec - let result = try await issuanceRequester.placeBatchIssuanceRequest( - accessToken: token, - request: credentials - ) - switch result { - case .success(let response): - return .success( - .success( - response: response - ) - ) - case .failure(let error): - throw ValidationError.error(reason: error.localizedDescription) - } } } diff --git a/Sources/Resources/europa/credential_issuer_metadata.json b/Sources/Resources/europa/credential_issuer_metadata.json index 8cd52d0..871eab9 100644 --- a/Sources/Resources/europa/credential_issuer_metadata.json +++ b/Sources/Resources/europa/credential_issuer_metadata.json @@ -2,7 +2,6 @@ "credential_issuer": "https://credential-issuer.example.com", "authorization_servers": ["https://keycloak-eudi.netcompany-intrasoft.com/realms/pid-issuer-realm"], "credential_endpoint": "https://credential-issuer.example.com/credentials", - "batch_credential_endpoint": "https://credential-issuer.example.com/credentials/batch", "deferred_credential_endpoint": "https://credential-issuer.example.com/credentials/deferred", "notification_endpoint": "https://credential-issuer.example.com/notification", "credential_response_encryption": { diff --git a/Sources/Resources/europa/credential_issuer_metadata_valid_algos.json b/Sources/Resources/europa/credential_issuer_metadata_valid_algos.json index fa28e20..13e54c1 100644 --- a/Sources/Resources/europa/credential_issuer_metadata_valid_algos.json +++ b/Sources/Resources/europa/credential_issuer_metadata_valid_algos.json @@ -2,7 +2,6 @@ "credential_issuer": "https://credential-issuer.example.com", "authorization_servers": ["https://keycloak-eudi.netcompany-intrasoft.com/realms/pid-issuer-realm"], "credential_endpoint": "https://credential-issuer.example.com/credentials", - "batch_credential_endpoint": "https://credential-issuer.example.com/credentials/batch", "deferred_credential_endpoint": "https://credential-issuer.example.com/credentials/deferred", "notification_endpoint": "https://credential-issuer.example.com/notification", "credential_response_encryption": { diff --git a/Sources/Resources/well-known/openid-credential-issuer_encrypted_responses.json b/Sources/Resources/well-known/openid-credential-issuer_encrypted_responses.json index 5420540..1c95423 100644 --- a/Sources/Resources/well-known/openid-credential-issuer_encrypted_responses.json +++ b/Sources/Resources/well-known/openid-credential-issuer_encrypted_responses.json @@ -2,7 +2,6 @@ "credential_issuer": "https://credential-issuer.example.com", "authorization_servers": ["https://auth-server.example.com"], "credential_endpoint": "https://credential-issuer.example.com/credentials", - "batch_credential_endpoint": "https://credential-issuer.example.com/credentials/batch", "deferred_credential_endpoint": "https://credential-issuer.example.com/credentials/deferred", "notification_endpoint": "https://credential-issuer.example.com/notification", "credential_response_encryption_alg_values_supported": [ diff --git a/Sources/Resources/well-known/openid-credential-issuer_no_encryption.json b/Sources/Resources/well-known/openid-credential-issuer_no_encryption.json index 203c40b..f2ebba9 100644 --- a/Sources/Resources/well-known/openid-credential-issuer_no_encryption.json +++ b/Sources/Resources/well-known/openid-credential-issuer_no_encryption.json @@ -2,7 +2,6 @@ "credential_issuer": "https://credential-issuer.example.com", "authorization_servers": ["https://auth-server.example.com"], "credential_endpoint": "https://credential-issuer.example.com/credentials", - "batch_credential_endpoint": "https://credential-issuer.example.com/credentials/batch", "deferred_credential_endpoint": "https://credential-issuer.example.com/credentials/deferred", "notification_endpoint": "https://credential-issuer.example.com/notification", "credential_response_encryption": { diff --git a/Tests/Issuance/IssuanceBatchRequestTest.swift b/Tests/Issuance/IssuanceBatchRequestTest.swift deleted file mode 100644 index f9c8d72..0000000 --- a/Tests/Issuance/IssuanceBatchRequestTest.swift +++ /dev/null @@ -1,376 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import Foundation -import XCTest -import JOSESwift -import SwiftyJSON - -@testable import OpenID4VCI - -class IssuanceBatchRequestTest: XCTestCase { - - let config: OpenId4VCIConfig = .init( - clientId: "wallet-dev", - authFlowRedirectionURI: URL(string: "urn:ietf:wg:oauth:2.0:oob")!, - authorizeIssuanceConfig: .favorScopes - ) - - override func setUp() async throws { - try await super.setUp() - } - - override func tearDown() { - super.tearDown() - } - - func testGivenMockDataBatchCredentialIssuance() async throws { - - // Given - guard let offer = await TestsConstants.createMockCredentialOfferValidEncryption() else { - XCTAssert(false, "Unable to resolve credential offer") - return - } - - // Given - let privateKey = try KeyController.generateRSAPrivateKey() - let publicKey = try KeyController.generateRSAPublicKey(from: privateKey) - - let alg = JWSAlgorithm(.RS256) - let publicKeyJWK = try RSAPublicKey( - publicKey: publicKey, - additionalParameters: [ - "alg": alg.name, - "use": "enc", - "kid": UUID().uuidString - ]) - - let spec = IssuanceResponseEncryptionSpec( - jwk: publicKeyJWK, - privateKey: privateKey, - algorithm: .init(.RSA_OAEP_256), - encryptionMethod: .init(.A128CBC_HS256) - ) - - // When - let issuer = try Issuer( - authorizationServerMetadata: offer.authorizationServerMetadata, - issuerMetadata: offer.credentialIssuerMetadata, - config: config, - parPoster: Poster( - session: NetworkingMock( - path: "pushed_authorization_request_response", - extension: "json" - ) - ), - tokenPoster: Poster( - session: NetworkingMock( - path: "access_token_request_response_no_proof", - extension: "json" - ) - ), - requesterPoster: Poster( - session: NetworkingMock( - path: "batch_issuance_success_response_credential", - extension: "json" - ) - ) - ) - - let authorizationCode = "MZqG9bsQ8UALhsGNlY39Yw==" - let request: UnauthorizedRequest = TestsConstants.unAuthorizedRequest - - let issuanceAuthorization: IssuanceAuthorization = .authorizationCode(authorizationCode: authorizationCode) - let unAuthorized = await issuer.handleAuthorizationCode( - parRequested: request, - authorizationCode: issuanceAuthorization - ) - - switch unAuthorized { - case .success(let authorizationCode): - let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) - - if case let .success(authorized) = authorizedRequest, - case let .noProofRequired(token, _, _, _) = authorized { - XCTAssert(true, "Got access token: \(token)") - XCTAssert(true, "Is no proof required") - - do { - - let claimSetMsoMdoc = MsoMdocFormat.MsoMdocClaimSet( - claims: [ - ("org.iso.18013.5.1", "given_name"), - ("org.iso.18013.5.1", "family_name"), - ("org.iso.18013.5.1", "birth_date") - ] - ) - - let claimSetSDJWTVC = GenericClaimSet(claims: [ - "given_name", - "family_name", - "birth_date", - ]) - - let msoMdocPayload: IssuanceRequestPayload = .configurationBased( - credentialConfigurationIdentifier: try .init(value: PID_MsoMdoc_config_id), - claimSet: .msoMdoc(claimSetMsoMdoc) - ) - - let sdJwtVCPayload: IssuanceRequestPayload = .configurationBased( - credentialConfigurationIdentifier: try .init(value: PID_SdJwtVC_config_id), - claimSet: .generic(claimSetSDJWTVC) - ) - - - let result = try await issuer.requestBatch( - noProofRequest: authorized, - requestPayload: [ - msoMdocPayload, - sdJwtVCPayload - ], - responseEncryptionSpecProvider: { _ in - spec - }) - - switch result { - case .success(let request): - switch request { - case .success(let response): - if let result = response.credentialResponses.first { - switch result { - case .deferred: - XCTAssert(false, "Unexpected deferred") - case .issued(let format, let credential, _, _): - XCTAssert(true, "credential: \(credential)") - return - } - } else { - break - } - case .failed(let error): - XCTAssert(false, error.localizedDescription) - - case .invalidProof(_, let errorDescription): - XCTAssert(false, errorDescription!) - } - XCTAssert(false, "Unexpected request") - case .failure(let error): - XCTAssert(false, error.localizedDescription) - } - } catch { - XCTAssert(false, error.localizedDescription) - } - - return - } - - case .failure(let error): - XCTAssert(false, error.localizedDescription) - } - - XCTAssert(false, "Unable to get access token") - } - - func testBatchCredentialIssuance() async throws { - - // Given - let privateKey = try KeyController.generateRSAPrivateKey() - let publicKey = try KeyController.generateRSAPublicKey(from: privateKey) - - let alg = JWSAlgorithm(.RS256) - let publicKeyJWK = try RSAPublicKey( - publicKey: publicKey, - additionalParameters: [ - "alg": alg.name, - "use": "enc", - "kid": UUID().uuidString - ]) - - let bindingKey: BindingKey = .jwk( - algorithm: alg, - jwk: publicKeyJWK, - privateKey: privateKey - ) - - let wallet = Wallet( - actingUser: .init( - username: "tneal", - password: "password" - ), - bindingKey: bindingKey, - dPoPConstructor: nil, - session: Wallet.walletSession - ) - - let url = "\(CREDENTIAL_ISSUER_PUBLIC_URL)/credentialoffer?credential_offer=\(SdJwtVC_CredentialOffer)" - - guard let offer = try? await CredentialOfferRequestResolver( - fetcher: Fetcher(session: wallet.session), - credentialIssuerMetadataResolver: CredentialIssuerMetadataResolver( - fetcher: Fetcher(session: wallet.session) - ), - authorizationServerMetadataResolver: AuthorizationServerMetadataResolver( - oidcFetcher: Fetcher(session: wallet.session), - oauthFetcher: Fetcher(session: wallet.session) - ) - ).resolve( - source: try .init( - urlString: url - ) - ).get() else { - XCTAssert(false, "Unable to resolve credential offer") - return - } - - let spec = IssuanceResponseEncryptionSpec( - jwk: publicKeyJWK, - privateKey: privateKey, - algorithm: .init(.RSA_OAEP_256), - encryptionMethod: .init(.A128CBC_HS256) - ) - - // When - let issuer = try Issuer( - authorizationServerMetadata: offer.authorizationServerMetadata, - issuerMetadata: offer.credentialIssuerMetadata, - config: config, - parPoster: Poster(session: wallet.session), - tokenPoster: Poster(session: wallet.session), - requesterPoster: Poster(session: wallet.session), - deferredRequesterPoster: Poster(session: wallet.session), - notificationPoster: Poster(session: wallet.session) - ) - - let authorized = try await wallet.authorizeRequestWithAuthCodeUseCase( - issuer: issuer, - offer: offer - ) - - let claimSetMsoMdoc = MsoMdocFormat.MsoMdocClaimSet( - claims: [ - ("org.iso.18013.5.1", "given_name"), - ("org.iso.18013.5.1", "family_name") - ] - ) - - let claimSetSDJWTVC = GenericClaimSet(claims: [ - "given_name", - "family_name" - ]) - - let msoMdocPayload: IssuanceRequestPayload = .configurationBased( - credentialConfigurationIdentifier: try .init( - value: PID_MsoMdoc_config_id - ), - claimSet: .msoMdoc(claimSetMsoMdoc) - ) - - let sdJwtVCPayload: IssuanceRequestPayload = .configurationBased( - credentialConfigurationIdentifier: try .init( - value: PID_SdJwtVC_config_id - ), - claimSet: .generic(claimSetSDJWTVC) - ) - - switch authorized { - - case .noProofRequired: - do { - - let result = try await issuer.requestBatch( - noProofRequest: authorized, - requestPayload: [ - msoMdocPayload, - sdJwtVCPayload - ], - responseEncryptionSpecProvider: { _ in - spec - }) - - switch result { - case .success(let request): - switch request { - case .success(let response): - if let result = response.credentialResponses.first { - switch result { - case .deferred: - XCTAssert(false, "Unexpected deferred") - case .issued(let format, let credential, _, _): - XCTAssert(true, "credential: \(credential)") - return - } - } else { - break - } - case .failed(let error): - XCTAssert(false, error.localizedDescription) - - case .invalidProof(_, let errorDescription): - XCTAssert(false, errorDescription!) - } - XCTAssert(false, "Unexpected request") - case .failure(let error): - XCTAssert(false, error.localizedDescription) - } - } catch { - XCTExpectFailure() - XCTAssert(false, error.localizedDescription) - } - case .proofRequired: - do { - - let result = try await issuer.requestBatch( - proofRequest: authorized, - bindingKey: bindingKey, - requestPayload: [ - msoMdocPayload, - sdJwtVCPayload - ], - responseEncryptionSpecProvider: { _ in - spec - }) - - switch result { - case .success(let request): - switch request { - case .success(let response): - if let result = response.credentialResponses.first { - switch result { - case .deferred: - XCTAssert(false, "Unexpected deferred") - case .issued(let format, let credential, _, _): - XCTAssert(true, "credential: \(credential)") - return - } - } else { - break - } - case .failed(let error): - XCTAssert(false, error.localizedDescription) - - case .invalidProof(_, let errorDescription): - XCTAssert(false, errorDescription!) - } - XCTAssert(false, "Unexpected request") - case .failure(let error): - XCTAssert(false, error.localizedDescription) - } - } catch { - XCTExpectFailure() - XCTAssert(false, error.localizedDescription) - } - } - } -} diff --git a/Tests/Resolver/CredentialOfferResolverTests.swift b/Tests/Resolver/CredentialOfferResolverTests.swift index 3318899..3a530c8 100644 --- a/Tests/Resolver/CredentialOfferResolverTests.swift +++ b/Tests/Resolver/CredentialOfferResolverTests.swift @@ -67,7 +67,6 @@ class CredentialOfferResolverTests: XCTestCase { switch result { case .success(let result): XCTAssert(result.credentialIssuerIdentifier.url.absoluteString == "https://credential-issuer.example.com") - XCTAssert(result.credentialIssuerMetadata.batchCredentialEndpoint?.url.absoluteString == "https://credential-issuer.example.com/credentials/batch") XCTAssert(result.credentialIssuerMetadata.deferredCredentialEndpoint?.url.absoluteString == "https://credential-issuer.example.com/credentials/deferred") case .failure(let error): @@ -114,7 +113,6 @@ class CredentialOfferResolverTests: XCTestCase { switch result { case .success(let result): XCTAssert(result.credentialIssuerIdentifier.url.absoluteString == "https://credential-issuer.example.com") - XCTAssert(result.credentialIssuerMetadata.batchCredentialEndpoint?.url.absoluteString == "https://credential-issuer.example.com/credentials/batch") XCTAssert(result.credentialIssuerMetadata.deferredCredentialEndpoint?.url.absoluteString == "https://credential-issuer.example.com/credentials/deferred") case .failure(let error): From 8f3cdd0a311da28bd6b4d4c2f6b42d7614e1b3ce Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 13:02:31 +0200 Subject: [PATCH 04/21] [fix] batch credential issuance model --- .../Entities/BatchCredentialIssuance.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 Sources/Entities/BatchCredentialIssuance.swift diff --git a/Sources/Entities/BatchCredentialIssuance.swift b/Sources/Entities/BatchCredentialIssuance.swift new file mode 100644 index 0000000..fc38de0 --- /dev/null +++ b/Sources/Entities/BatchCredentialIssuance.swift @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation + +public struct BatchCredentialIssuance: Codable { + public let batchSize: Int + + enum CodingKeys: String, CodingKey { + case batchSize = "batch_size" + } + + public init(batchSize: Int) throws { + if batchSize <= 0 { + throw ValidationError.invalidBatchSize(batchSize) + } + self.batchSize = batchSize + } +} From 98bfa1bbc3124ef8612eb9af9e960460027fa55e Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 13:03:37 +0200 Subject: [PATCH 05/21] [fix] batch issuance new tests --- Tests/Issuance/IssuanceBatchRequestTest.swift | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 Tests/Issuance/IssuanceBatchRequestTest.swift diff --git a/Tests/Issuance/IssuanceBatchRequestTest.swift b/Tests/Issuance/IssuanceBatchRequestTest.swift new file mode 100644 index 0000000..3f6156a --- /dev/null +++ b/Tests/Issuance/IssuanceBatchRequestTest.swift @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import XCTest +import JOSESwift +import SwiftyJSON + +@testable import OpenID4VCI + +class IssuanceBatchRequestTest: XCTestCase { + + let config: OpenId4VCIConfig = .init( + clientId: "wallet-dev", + authFlowRedirectionURI: URL(string: "urn:ietf:wg:oauth:2.0:oob")!, + authorizeIssuanceConfig: .favorScopes + ) + + override func setUp() async throws { + try await super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testGivenMockDataBatchCredentialIssuance() async throws { + + // Given + guard let offer = await TestsConstants.createMockCredentialOfferValidEncryptionWithBatchLimit() else { + XCTAssert(false, "Unable to resolve credential offer") + return + } + + // Given + let privateKey = try KeyController.generateRSAPrivateKey() + let publicKey = try KeyController.generateRSAPublicKey(from: privateKey) + + let alg = JWSAlgorithm(.RS256) + let publicKeyJWK = try RSAPublicKey( + publicKey: publicKey, + additionalParameters: [ + "alg": alg.name, + "use": "enc", + "kid": UUID().uuidString + ]) + + let spec = IssuanceResponseEncryptionSpec( + jwk: publicKeyJWK, + privateKey: privateKey, + algorithm: .init(.RSA_OAEP_256), + encryptionMethod: .init(.A128CBC_HS256) + ) + + // When + let issuer = try Issuer( + authorizationServerMetadata: offer.authorizationServerMetadata, + issuerMetadata: offer.credentialIssuerMetadata, + config: config, + parPoster: Poster( + session: NetworkingMock( + path: "pushed_authorization_request_response", + extension: "json" + ) + ), + tokenPoster: Poster( + session: NetworkingMock( + path: "access_token_request_response_no_proof", + extension: "json" + ) + ), + requesterPoster: Poster( + session: NetworkingMock( + path: "batch_issuance_success_response_credential", + extension: "json" + ) + ) + ) + + let authorizationCode = "MZqG9bsQ8UALhsGNlY39Yw==" + let request: UnauthorizedRequest = TestsConstants.unAuthorizedRequest + + let issuanceAuthorization: IssuanceAuthorization = .authorizationCode(authorizationCode: authorizationCode) + let unAuthorized = await issuer.handleAuthorizationCode( + parRequested: request, + authorizationCode: issuanceAuthorization + ) + + switch unAuthorized { + case .success(let authorizationCode): + let authorizedRequest = await issuer.authorizeWithAuthorizationCode(authorizationCode: authorizationCode) + + if case let .success(authorized) = authorizedRequest, + case let .noProofRequired(token, _, _, _) = authorized { + XCTAssert(true, "Got access token: \(token)") + XCTAssert(true, "Is no proof required") + + do { + + let claimSetMsoMdoc = MsoMdocFormat.MsoMdocClaimSet( + claims: [ + ("org.iso.18013.5.1", "given_name"), + ("org.iso.18013.5.1", "family_name"), + ("org.iso.18013.5.1", "birth_date") + ] + ) + + let msoMdocPayload: IssuanceRequestPayload = .configurationBased( + credentialConfigurationIdentifier: try .init(value: PID_MsoMdoc_config_id), + claimSet: .msoMdoc(claimSetMsoMdoc) + ) + + let result = try await issuer.request( + noProofRequest: authorized, + requestPayload: msoMdocPayload, + responseEncryptionSpecProvider: { _ in + spec + }) + + switch result { + case .success(let request): + switch request { + case .success(let response): + if let result = response.credentialResponses.first { + switch result { + case .deferred: + XCTAssert(false, "Unexpected deferred") + case .issued(_, let credential, _, _): + XCTAssert(true, "credential: \(credential)") + return + } + } else { + break + } + case .failed(let error): + XCTAssert(false, error.localizedDescription) + + case .invalidProof(_, let errorDescription): + XCTAssert(false, errorDescription!) + } + XCTAssert(false, "Unexpected request") + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + } catch { + XCTAssert(false, error.localizedDescription) + } + + return + } + + case .failure(let error): + XCTAssert(false, error.localizedDescription) + } + + XCTAssert(false, "Unable to get access token") + } +} From ecff5565d243065c80f6fa7c65458808eee66692 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 13:03:53 +0200 Subject: [PATCH 06/21] [fix] new resource json for batch --- ...credential-issuer_no_encryption_batch.json | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 Sources/Resources/well-known/openid-credential-issuer_no_encryption_batch.json diff --git a/Sources/Resources/well-known/openid-credential-issuer_no_encryption_batch.json b/Sources/Resources/well-known/openid-credential-issuer_no_encryption_batch.json new file mode 100644 index 0000000..3839097 --- /dev/null +++ b/Sources/Resources/well-known/openid-credential-issuer_no_encryption_batch.json @@ -0,0 +1,168 @@ +{ + "credential_issuer": "https://credential-issuer.example.com", + "authorization_servers": ["https://auth-server.example.com"], + "credential_endpoint": "https://credential-issuer.example.com/credentials", + "deferred_credential_endpoint": "https://credential-issuer.example.com/credentials/deferred", + "notification_endpoint": "https://credential-issuer.example.com/notification", + "credential_response_encryption": { + "encryption_required": false + }, + "batch_credential_issuance": { + "batch_size": 3 + }, + "credential_identifiers_supported": true, + "credential_configurations_supported": { + "eu.europa.ec.eudi.pid_vc_sd_jwt": { + "format": "vc+sd-jwt", + "scope": "eu.europa.ec.eudi.pid_vc_sd_jwt", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "credential_signing_alg_values_supported": [ + "RS256" + ], + "proof_types_supported": { + "jwt": { + "proof_signing_alg_values_supported": [ + "RS256", + "ES256" + ] + } + }, + "credential_definition": { + "type": "eu.europa.ec.eudi.pid.1", + "claims": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + } + }, + "display": [ + { + "name": "Personal Identification Data ", + "locale": "en-US", + "logo": { + "uri": "https://examplestate.com/public/pid.png", + "alt_text": "a square logo of a university" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ] + }, + "eu.europa.ec.eudi.pid_mso_mdoc": { + "format": "mso_mdoc", + "scope": "eu.europa.ec.eudi.pid_mso_mdoc", + "doctype": "org.iso.18013.5.1.PID", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "credential_signing_alg_values_supported": [ + "RS256" + ], + "display": [ + { + "name": "Personal Identification Data", + "locale": "en-US", + "logo": { + "uri": "https://examplestate.com/public/pid.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + }, + "UniversityDegree_mso_mdoc": { + "format": "mso_mdoc", + "scope": "UniversityDegree", + "doctype": "org.iso.18013.5.1.Degree", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "credential_signing_alg_values_supported": [ + "RS256" + ], + "display": [ + { + "name": "Mobile Driving License", + "locale": "en-US", + "logo": { + "uri": "https://examplestate.com/public/mdl.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + } + }, + "display": [ + { + "name": "credential-issuer.example.com", + "locale": "en-US" + } + ] +} From a749ab0a43b219ce14d1bc642b44a3a30fe0a84c Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 13:08:14 +0200 Subject: [PATCH 07/21] [fix] issue document request in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb18e70..a0c24c3 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,7 @@ let payload: IssuanceRequestPayload = .configurationBased( credentialConfigurationIdentifier: ... ) -let requestOutcome = try await issuer.requestSingle( +let requestOutcome = try await issuer.request( proofRequest: ..., bindingKey: ..., requestPayload: payload, From eb9d881d38f0c6d39a0e2510734948a871f37582 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 13:08:54 +0200 Subject: [PATCH 08/21] [fix] updated issuer metadata with batch size support --- .../CredentialIssuer/CredentialIssuerMetadata.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/Entities/CredentialIssuer/CredentialIssuerMetadata.swift b/Sources/Entities/CredentialIssuer/CredentialIssuerMetadata.swift index 771f283..645ccea 100644 --- a/Sources/Entities/CredentialIssuer/CredentialIssuerMetadata.swift +++ b/Sources/Entities/CredentialIssuer/CredentialIssuerMetadata.swift @@ -28,6 +28,7 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { public let credentialIdentifiersSupported: Bool? public let signedMetadata: String? public let display: [Display] + public let batchCredentialIssuance: BatchCredentialIssuance? public enum CodingKeys: String, CodingKey { case credentialIssuerIdentifier = "credential_issuer" @@ -43,6 +44,7 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { case credentialResponseEncryption = "credential_response_encryption" case signedMetadata = "signed_metadata" case credentialIdentifiersSupported = "credential_identifiers_supported" + case batchCredentialIssuance = "batch_credential_issuance" } public init( @@ -55,7 +57,8 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { credentialConfigurationsSupported: [CredentialConfigurationIdentifier: CredentialSupported], signedMetadata: String?, display: [Display]?, - credentialIdentifiersSupported: Bool? = nil + credentialIdentifiersSupported: Bool? = nil, + batchCredentialIssuance: BatchCredentialIssuance? = nil ) { self.credentialIssuerIdentifier = credentialIssuerIdentifier self.authorizationServers = authorizationServers @@ -71,6 +74,7 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { self.display = display ?? [] self.credentialIdentifiersSupported = credentialIdentifiersSupported + self.batchCredentialIssuance = batchCredentialIssuance } public init(deferredCredentialEndpoint: CredentialIssuerEndpoint?) throws { @@ -141,6 +145,8 @@ public struct CredentialIssuerMetadata: Decodable, Equatable { signedMetadata = try? container.decodeIfPresent(String.self, forKey: .signedMetadata) credentialIdentifiersSupported = try? container.decodeIfPresent(Bool.self, forKey: .credentialIdentifiersSupported) ?? false + + batchCredentialIssuance = try? container.decodeIfPresent(BatchCredentialIssuance.self, forKey: .batchCredentialIssuance) } public static func == (lhs: CredentialIssuerMetadata, rhs: CredentialIssuerMetadata) -> Bool { From 125725d11f1d1b1889a5742a3cf807fe299db7b8 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 13:13:37 +0200 Subject: [PATCH 09/21] [fix] added some new errors --- Sources/Entities/Errors/ValidationError.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Entities/Errors/ValidationError.swift b/Sources/Entities/Errors/ValidationError.swift index a0cc158..da59cdc 100644 --- a/Sources/Entities/Errors/ValidationError.swift +++ b/Sources/Entities/Errors/ValidationError.swift @@ -21,6 +21,8 @@ public enum ValidationError: Error, LocalizedError { case nonHttpsUrl(String) case invalidUrl(String) case response(GenericErrorResponse) + case invalidBatchSize(Int) + case issuerBatchSizeLimitExceeded(Int) public var errorDescription: String? { switch self { @@ -34,6 +36,10 @@ public enum ValidationError: Error, LocalizedError { return "ValidationError:invalidUrl: \(url)" case .response(let response): return "ValidationError:response: \(response.errorDescription ?? "")" + case .invalidBatchSize(let size): + return "ValidationError:invalidBatchSize: \(size)" + case .issuerBatchSizeLimitExceeded(let size): + return "ValidationError:issuerBatchSizeLimitExceeded: \(size)" } } } From fdea067e5f3d7e7995b42cb7bdda43df8eb5b63a Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 13:14:02 +0200 Subject: [PATCH 10/21] [fix] added proof to --- Sources/Entities/Types/Types.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Sources/Entities/Types/Types.swift b/Sources/Entities/Types/Types.swift index b154061..c09aa7b 100644 --- a/Sources/Entities/Types/Types.swift +++ b/Sources/Entities/Types/Types.swift @@ -102,7 +102,7 @@ public struct Scope: Codable { public enum ContentType: String { case form = "application/x-www-form-urlencoded" case json = "application/json" - + public static let key = "Content-Type" } @@ -239,3 +239,18 @@ public enum InputModeTO: String, Codable { case text = "text" case numeric = "numeric" } + +public struct ProofsTO: Codable { + public let jwtProofs: [String]? + + public enum CodingKeys: String, CodingKey { + case jwtProofs = "jwt" + } + + public init(jwtProofs: [String]? = nil) { + guard !(jwtProofs?.isEmpty ?? true) else { + fatalError("jwtProofs must be non-empty.") + } + self.jwtProofs = jwtProofs + } +} From 6ba0620f6fedd95cdba2c37b0c3033ea01bfe2a5 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 13:16:11 +0200 Subject: [PATCH 11/21] [fix] multiple proofs support --- .../CredentialSupported.swift | 6 +- .../CredentialIssuanceRequest.swift | 228 +++++++++--------- Sources/Entities/Profiles/MsoMdocFormat.swift | 12 +- Sources/Entities/Profiles/SdJwtVcFormat.swift | 12 +- .../SingleIssuanceSuccessResponse.swift | 34 ++- .../W3CJsonLdDataIntegrityFormat.swift | 2 +- .../Profiles/W3CJsonLdSignedJwtFormat.swift | 2 +- .../Profiles/W3CSignedJwtFormat.swift | 2 +- Sources/Issuers/IssuanceRequester.swift | 17 ++ Sources/Issuers/Issuer.swift | 93 +++++-- Tests/Constants/TestsConstants.swift | 33 +++ Tests/Helpers/Wallet.swift | 16 +- .../Issuance/IssuanceAuthorizationTest.swift | 12 +- .../IssuanceDeferredRequestTest.swift | 2 +- Tests/Issuance/IssuanceEncryptionTest.swift | 6 +- Tests/Issuance/IssuanceNotificationTest.swift | 4 +- .../Issuance/IssuanceSingleRequestTest.swift | 14 +- Tests/Wallet/VCIFlowNoOffer.swift | 95 +++++++- Tests/Wallet/VCIFlowWithOffer.swift | 12 +- 19 files changed, 406 insertions(+), 196 deletions(-) diff --git a/Sources/Entities/CredentialSupported/CredentialSupported.swift b/Sources/Entities/CredentialSupported/CredentialSupported.swift index c7680ee..338d20c 100644 --- a/Sources/Entities/CredentialSupported/CredentialSupported.swift +++ b/Sources/Entities/CredentialSupported/CredentialSupported.swift @@ -46,7 +46,7 @@ public extension CredentialSupported { func toIssuanceRequest( requester: IssuanceRequesterType, claimSet: ClaimSet? = nil, - proof: Proof? = nil, + proofs: [Proof] = [], credentialIdentifier: CredentialIdentifier? = nil, responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? ) throws -> CredentialIssuanceRequest { @@ -75,7 +75,7 @@ public extension CredentialSupported { return try credentialConfiguration.toIssuanceRequest( responseEncryptionSpec: issuerEncryption.notRequired ? nil : responseEncryptionSpec, claimSet: claimSet, - proof: proof + proofs: proofs ) case .sdJwtVc(let credentialConfiguration): @@ -102,7 +102,7 @@ public extension CredentialSupported { return try credentialConfiguration.toIssuanceRequest( responseEncryptionSpec: issuerEncryption.notRequired ? nil : responseEncryptionSpec, claimSet: claimSet, - proof: proof + proofs: proofs ) default: throw ValidationError.error(reason: "Unsupported profile for issuance request") diff --git a/Sources/Entities/IssuanceFlows/CredentialIssuanceRequest.swift b/Sources/Entities/IssuanceFlows/CredentialIssuanceRequest.swift index 38e65bf..aed2884 100644 --- a/Sources/Entities/IssuanceFlows/CredentialIssuanceRequest.swift +++ b/Sources/Entities/IssuanceFlows/CredentialIssuanceRequest.swift @@ -76,23 +76,25 @@ public extension SingleCredential { func toDictionary() throws -> JSON { switch self { case .msoMdoc(let credential): + let proofOrProofs = credential.proofs.proofOrProofs() switch credential.requestedCredentialResponseEncryption { case .notRequested: if let identifier = credential.credentialIdentifier { - let dictionary = [ - "credential_identifier": identifier, - "proof": credential.proof != nil ? (try? credential.proof.toDictionary()) : nil - ] as [String : Any?] - return JSON(dictionary.filter { $0.value != nil }) - + return try JSON.createFrom( + proofOrProofs: proofOrProofs, + dictionary: [ + "credential_identifier": identifier + ] + ) } else { - let dictionary = [ - "format": MsoMdocFormat.FORMAT, - "doctype": credential.docType, - "claims": credential.claimSet?.toDictionary(), - "proof": credential.proof != nil ? (try? credential.proof.toDictionary()) : nil - ] as [String : Any?] - return JSON(dictionary.filter { $0.value != nil }) + return try JSON.createFrom( + proofOrProofs: proofOrProofs, + dictionary: [ + "format": MsoMdocFormat.FORMAT, + "doctype": credential.docType, + "claims": credential.claimSet?.toDictionary() + ] + ) } case .requested( let encryptionJwk, @@ -101,50 +103,55 @@ public extension SingleCredential { let responseEncryptionMethod ): if let identifier = credential.credentialIdentifier { - let dictionary = [ - "proof": credential.proof != nil ? (try? credential.proof.toDictionary()) : nil, - "credential_identifier": identifier, - "credential_response_encryption": [ - "jwk": try encryptionJwk.toDictionary(), - "alg": responseEncryptionAlg.name, - "enc": responseEncryptionMethod.name + return try JSON.createFrom( + proofOrProofs: proofOrProofs, + dictionary: [ + "credential_identifier": identifier, + "credential_response_encryption": [ + "jwk": try encryptionJwk.toDictionary(), + "alg": responseEncryptionAlg.name, + "enc": responseEncryptionMethod.name + ] ] - ] as [String : Any?] - return JSON(dictionary.filter { $0.value != nil }) + ) } else { - let dictionary = [ - "format": MsoMdocFormat.FORMAT, - "proof": credential.proof != nil ? (try? credential.proof.toDictionary()) : nil, - "doctype": credential.docType, - "credential_response_encryption": [ - "jwk": try encryptionJwk.toDictionary(), - "alg": responseEncryptionAlg.name, - "enc": responseEncryptionMethod.name - ], - "claims": credential.claimSet?.toDictionary() - ] as [String : Any?] - return JSON(dictionary.filter { $0.value != nil }) + return try JSON.createFrom( + proofOrProofs: proofOrProofs, + dictionary: [ + "format": MsoMdocFormat.FORMAT, + "doctype": credential.docType, + "credential_response_encryption": [ + "jwk": try encryptionJwk.toDictionary(), + "alg": responseEncryptionAlg.name, + "enc": responseEncryptionMethod.name + ], + "claims": credential.claimSet?.toDictionary() + ] + ) } } case .sdJwtVc(let credential): + let proofOrProofs = credential.proofs.proofOrProofs() switch credential.requestedCredentialResponseEncryption { case .notRequested: if let identifier = credential.credentialIdentifier { - let dictionary = [ - "proof": credential.proof != nil ? (try? credential.proof.toDictionary()) : nil, - "credential_identifier": identifier - ] as [String : Any?] - return JSON(dictionary.filter { $0.value != nil }) + return try JSON.createFrom( + proofOrProofs: proofOrProofs, + dictionary: [ + "credential_identifier": identifier + ] + ) } else { - let dictionary = [ - "vct": credential.vct ?? credential.credentialDefinition.type, - "proof": credential.proof != nil ? (try? credential.proof.toDictionary()) : nil, - "format": SdJwtVcFormat.FORMAT, - "claims": credential.credentialDefinition.claims?.toDictionary() - ] as [String : Any?] - return JSON(dictionary.filter { $0.value != nil }) + return try JSON.createFrom( + proofOrProofs: proofOrProofs, + dictionary: [ + "vct": credential.vct ?? credential.credentialDefinition.type, + "format": SdJwtVcFormat.FORMAT, + "claims": credential.credentialDefinition.claims?.toDictionary() + ] + ) } case .requested( let encryptionJwk, @@ -153,88 +160,75 @@ public extension SingleCredential { let responseEncryptionMethod ): if let identifier = credential.credentialIdentifier { - let dictionary = [ - "credential_identifier": identifier, - "proof": credential.proof != nil ? (try? credential.proof.toDictionary()) : nil, - "credential_response_encryption": [ - "jwk": try encryptionJwk.toDictionary(), - "alg": responseEncryptionAlg.name, - "enc": responseEncryptionMethod.name + return try JSON.createFrom( + proofOrProofs: proofOrProofs, + dictionary: [ + "credential_identifier": identifier, + "credential_response_encryption": [ + "jwk": try encryptionJwk.toDictionary(), + "alg": responseEncryptionAlg.name, + "enc": responseEncryptionMethod.name + ] ] - ] as [String : Any?] - return JSON(dictionary.filter { $0.value != nil }) + ) } else { - let dictionary = [ - "vct": credential.vct ?? credential.credentialDefinition.type, - "format": SdJwtVcFormat.FORMAT, - "proof": credential.proof != nil ? (try? credential.proof.toDictionary()) : nil, - "credential_response_encryption": [ - "jwk": try encryptionJwk.toDictionary(), - "alg": responseEncryptionAlg.name, - "enc": responseEncryptionMethod.name - ], - "claims": credential.credentialDefinition.claims?.toDictionary() - ] as [String : Any?] - return JSON(dictionary.filter { $0.value != nil }) + return try JSON.createFrom( + proofOrProofs: proofOrProofs, + dictionary: [ + "vct": credential.vct ?? credential.credentialDefinition.type, + "format": SdJwtVcFormat.FORMAT, + "credential_response_encryption": [ + "jwk": try encryptionJwk.toDictionary(), + "alg": responseEncryptionAlg.name, + "enc": responseEncryptionMethod.name + ], + "claims": credential.credentialDefinition.claims?.toDictionary() + ] + ) } } } } } -public struct MsoMdocIssuanceRequest { - public let format: String - public let proof: ProofType? - public let credentialEncryptionJwk: JWK? - public let credentialResponseEncryptionAlg: JWEAlgorithm? - public let credentialResponseEncryptionMethod: JOSEEncryptionMethod? - public let doctype: String - public let claims: [Namespace: [ClaimName: CredentialSupported]] - - public init( - format: String, - proof: ProofType?, - credentialEncryptionJwk: JWK?, - credentialResponseEncryptionAlg: JWEAlgorithm?, - credentialResponseEncryptionMethod: JOSEEncryptionMethod?, - doctype: String, - claims: [Namespace: [ClaimName: CredentialSupported]] - ) { - self.format = format - self.proof = proof - self.credentialEncryptionJwk = credentialEncryptionJwk - self.credentialResponseEncryptionAlg = credentialResponseEncryptionAlg - self.credentialResponseEncryptionMethod = credentialResponseEncryptionMethod - self.doctype = doctype - self.claims = claims - } - - static func create( - proof: ProofType?, - credentialEncryptionJwk: JWK?, - credentialResponseEncryptionAlg: JWEAlgorithm?, - credentialResponseEncryptionMethod: JOSEEncryptionMethod?, - doctype: String, - claims: [Namespace: [ClaimName: CredentialSupported]] - ) -> MsoMdocIssuanceRequest { - var encryptionMethod = credentialResponseEncryptionMethod - if credentialResponseEncryptionAlg != nil && credentialResponseEncryptionMethod == nil { - encryptionMethod = JOSEEncryptionMethod(.A128CBC_HS256) +private extension Array where Element == Proof { + func proofOrProofs() -> (Proof?, ProofsTO?) { + if self.isEmpty { + return (nil, nil) + + } else if self.count == 1 { + return (self.first, nil) - } else if credentialResponseEncryptionAlg == nil && credentialResponseEncryptionMethod != nil { - fatalError("Credential response encryption algorithm must be specified if Credential response encryption method is provided") + } else { + let jwtProofs = self.compactMap { proof in + switch proof { + case .jwt(let jwt): + return jwt + } + } + let proofsTO = ProofsTO(jwtProofs: jwtProofs) + return (nil, proofsTO) } - - return MsoMdocIssuanceRequest( - format: "mso_mdoc", - proof: proof, - credentialEncryptionJwk: credentialEncryptionJwk, - credentialResponseEncryptionAlg: credentialResponseEncryptionAlg, - credentialResponseEncryptionMethod: encryptionMethod, - doctype: doctype, - claims: claims - ) } } +private extension JSON { + static func toJSON(_ tuple: (Proof?, ProofsTO?)) -> JSON? { + if let proof = tuple.0 { + return try? .init(["proof": proof.toDictionary()]) + } else if let proofs = tuple.1 { + return try? .init(["proofs": proofs.toDictionary()]) + } else { + return nil + } + } + + static func createFrom(proofOrProofs: (Proof?, ProofsTO?), dictionary: [String: Any?]) throws -> JSON { + var json = Self.toJSON(proofOrProofs) + try json?.merge(with: JSON( + dictionary.compactMapValues { $0 } + )) + return json ?? JSON([:]) + } +} diff --git a/Sources/Entities/Profiles/MsoMdocFormat.swift b/Sources/Entities/Profiles/MsoMdocFormat.swift index bbd6650..b1d6fc3 100644 --- a/Sources/Entities/Profiles/MsoMdocFormat.swift +++ b/Sources/Entities/Profiles/MsoMdocFormat.swift @@ -38,7 +38,7 @@ public extension MsoMdocFormat { struct MsoMdocSingleCredential: Codable { public let docType: String - public let proof: Proof? + public let proofs: [Proof] public let credentialEncryptionJwk: JWK? public let credentialEncryptionKey: SecKey? public let credentialResponseEncryptionAlg: JWEAlgorithm? @@ -59,7 +59,7 @@ public extension MsoMdocFormat { public init( docType: String, - proof: Proof? = nil, + proofs: [Proof] = [], credentialEncryptionJwk: JWK? = nil, credentialEncryptionKey: SecKey? = nil, credentialResponseEncryptionAlg: JWEAlgorithm? = nil, @@ -68,7 +68,7 @@ public extension MsoMdocFormat { credentialIdentifier: CredentialIdentifier? ) throws { self.docType = docType - self.proof = proof + self.proofs = proofs self.credentialEncryptionJwk = credentialEncryptionJwk self.credentialEncryptionKey = credentialEncryptionKey self.credentialResponseEncryptionAlg = credentialResponseEncryptionAlg @@ -90,7 +90,7 @@ public extension MsoMdocFormat { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(proof, forKey: .proof) + try container.encode(proofs, forKey: .proof) if let credentialEncryptionJwk = credentialEncryptionJwk as? RSAPublicKey { try container.encode(credentialEncryptionJwk, forKey: .credentialEncryptionJwk) @@ -333,13 +333,13 @@ public extension MsoMdocFormat { responseEncryptionSpec: IssuanceResponseEncryptionSpec?, credentialIdentifier: CredentialIdentifier? = nil, claimSet: ClaimSet?, - proof: Proof? + proofs: [Proof] ) throws -> CredentialIssuanceRequest { try .single( .msoMdoc( .init( docType: docType, - proof: proof, + proofs: proofs, credentialEncryptionJwk: responseEncryptionSpec?.jwk, credentialEncryptionKey: responseEncryptionSpec?.privateKey, credentialResponseEncryptionAlg: responseEncryptionSpec?.algorithm, diff --git a/Sources/Entities/Profiles/SdJwtVcFormat.swift b/Sources/Entities/Profiles/SdJwtVcFormat.swift index 625788b..948593c 100644 --- a/Sources/Entities/Profiles/SdJwtVcFormat.swift +++ b/Sources/Entities/Profiles/SdJwtVcFormat.swift @@ -38,7 +38,7 @@ public struct SdJwtVcFormat: FormatProfile { public extension SdJwtVcFormat { struct SdJwtVcSingleCredential: Codable { - public let proof: Proof? + public let proofs: [Proof] public let format: String = SdJwtVcFormat.FORMAT public let vct: String? public let credentialEncryptionJwk: JWK? @@ -59,7 +59,7 @@ public extension SdJwtVcFormat { } public init( - proof: Proof?, + proofs: [Proof], vct: String?, credentialEncryptionJwk: JWK? = nil, credentialEncryptionKey: SecKey? = nil, @@ -68,7 +68,7 @@ public extension SdJwtVcFormat { credentialDefinition: CredentialDefinition, credentialIdentifier: CredentialIdentifier? ) throws { - self.proof = proof + self.proofs = proofs self.vct = vct self.credentialEncryptionJwk = credentialEncryptionJwk self.credentialEncryptionKey = credentialEncryptionKey @@ -94,7 +94,7 @@ public extension SdJwtVcFormat { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(proof, forKey: .proof) + try container.encode(proofs, forKey: .proof) if let credentialEncryptionJwk = credentialEncryptionJwk as? RSAPublicKey { try container.encode(credentialEncryptionJwk, forKey: .credentialEncryptionJwk) @@ -325,12 +325,12 @@ public extension SdJwtVcFormat { responseEncryptionSpec: IssuanceResponseEncryptionSpec?, credentialIdentifier: CredentialIdentifier? = nil, claimSet: ClaimSet?, - proof: Proof? + proofs: [Proof] ) throws -> CredentialIssuanceRequest { try .single( .sdJwtVc( .init( - proof: proof, + proofs: proofs, vct: vct, credentialEncryptionJwk: responseEncryptionSpec?.jwk, credentialEncryptionKey: responseEncryptionSpec?.privateKey, diff --git a/Sources/Entities/Profiles/SingleIssuanceSuccessResponse.swift b/Sources/Entities/Profiles/SingleIssuanceSuccessResponse.swift index 6fed85e..27356b5 100644 --- a/Sources/Entities/Profiles/SingleIssuanceSuccessResponse.swift +++ b/Sources/Entities/Profiles/SingleIssuanceSuccessResponse.swift @@ -25,8 +25,8 @@ public struct SingleIssuanceSuccessResponse: Codable { public let cNonceExpiresInSeconds: Int? enum CodingKeys: String, CodingKey { - case credential - case credentials + case credential = "credential" + case credentials = "credentials" case transactionId = "transaction_id" case notificationId = "notification_id" case cNonce = "c_nonce" @@ -48,6 +48,34 @@ public struct SingleIssuanceSuccessResponse: Codable { self.cNonce = cNonce self.cNonceExpiresInSeconds = cNonceExpiresInSeconds } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + // Decode fields + credential = try container.decodeIfPresent(JSON.self, forKey: .credential) + credentials = try container.decodeIfPresent(JSON.self, forKey: .credentials) + transactionId = try container.decodeIfPresent(String.self, forKey: .transactionId) + notificationId = try container.decodeIfPresent(String.self, forKey: .notificationId) + cNonce = try container.decodeIfPresent(String.self, forKey: .cNonce) + cNonceExpiresInSeconds = try container.decodeIfPresent(Int.self, forKey: .cNonceExpiresInSeconds) + + if transactionId == nil && (credential == nil && credentials == nil) { + throw DecodingError.dataCorruptedError( + forKey: .credential, + in: container, + debugDescription: "At least one of 'credential' or 'credentials' must be non-nil." + ) + } + + if notificationId != nil && (credential == nil && credentials == nil) { + throw DecodingError.dataCorruptedError( + forKey: .notificationId, + in: container, + debugDescription: "'notificationId' must not be present if 'credential' is not present." + ) + } + } } public extension SingleIssuanceSuccessResponse { @@ -80,7 +108,7 @@ public extension SingleIssuanceSuccessResponse { ) ) } else if let credentials = credentials, - let jsonObject = credentials.dictionary, + let jsonObject = credentials.array, !jsonObject.isEmpty { return .init( credentialResponses: [ diff --git a/Sources/Entities/Profiles/W3CJsonLdDataIntegrityFormat.swift b/Sources/Entities/Profiles/W3CJsonLdDataIntegrityFormat.swift index 04ddf60..0c0061e 100644 --- a/Sources/Entities/Profiles/W3CJsonLdDataIntegrityFormat.swift +++ b/Sources/Entities/Profiles/W3CJsonLdDataIntegrityFormat.swift @@ -272,7 +272,7 @@ public extension W3CJsonLdDataIntegrityFormat { func toIssuanceRequest( claimSet: ClaimSet?, - proof: Proof? + proofs: [Proof] ) throws -> CredentialIssuanceRequest { throw ValidationError.error(reason: "Not yet implemented") } diff --git a/Sources/Entities/Profiles/W3CJsonLdSignedJwtFormat.swift b/Sources/Entities/Profiles/W3CJsonLdSignedJwtFormat.swift index aef57f8..212df90 100644 --- a/Sources/Entities/Profiles/W3CJsonLdSignedJwtFormat.swift +++ b/Sources/Entities/Profiles/W3CJsonLdSignedJwtFormat.swift @@ -258,7 +258,7 @@ public extension W3CJsonLdSignedJwtFormat { func toIssuanceRequest( claimSet: ClaimSet?, - proof: Proof? + proofs: [Proof] ) throws -> CredentialIssuanceRequest { throw ValidationError.error(reason: "Not yet implemented") } diff --git a/Sources/Entities/Profiles/W3CSignedJwtFormat.swift b/Sources/Entities/Profiles/W3CSignedJwtFormat.swift index 59b9bd7..3082383 100644 --- a/Sources/Entities/Profiles/W3CSignedJwtFormat.swift +++ b/Sources/Entities/Profiles/W3CSignedJwtFormat.swift @@ -228,7 +228,7 @@ public extension W3CSignedJwtFormat { func toIssuanceRequest( claimSet: ClaimSet?, - proof: Proof? + proofs: [Proof] ) throws -> CredentialIssuanceRequest { throw ValidationError.error(reason: "Not yet implemented") } diff --git a/Sources/Issuers/IssuanceRequester.swift b/Sources/Issuers/IssuanceRequester.swift index dfaa116..545c9be 100644 --- a/Sources/Issuers/IssuanceRequester.swift +++ b/Sources/Issuers/IssuanceRequester.swift @@ -332,6 +332,23 @@ private extension SingleIssuanceSuccessResponse { expiresInSeconds: cNonceExpiresInSeconds ) ) + } else if let credentials = credentials, + let jsonObject = credentials.array, + !jsonObject.isEmpty { + return .init( + credentialResponses: [ + .issued( + format: nil, + credential: .json(JSON(jsonObject)), + notificationId: nil, + additionalInfo: nil + ) + ], + cNonce: .init( + value: cNonce, + expiresInSeconds: cNonceExpiresInSeconds + ) + ) } else if let transactionId = transactionId { return CredentialIssuanceResponse( credentialResponses: [.deferred(transactionId: try .init(value: transactionId))], diff --git a/Sources/Issuers/Issuer.swift b/Sources/Issuers/Issuer.swift index 5619203..912050b 100644 --- a/Sources/Issuers/Issuer.swift +++ b/Sources/Issuers/Issuer.swift @@ -38,15 +38,15 @@ public protocol IssuerType { authorizationCode: UnauthorizedRequest ) async -> Result - func requestSingle( + func request( noProofRequest: AuthorizedRequest, requestPayload: IssuanceRequestPayload, responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? ) async throws -> Result - func requestSingle( + func request( proofRequest: AuthorizedRequest, - bindingKey: BindingKey, + bindingKeys: [BindingKey], requestPayload: IssuanceRequestPayload, responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? ) async throws -> Result @@ -388,7 +388,7 @@ public actor Issuer: IssuerType { } } - public func requestSingle( + public func request( noProofRequest: AuthorizedRequest, requestPayload: IssuanceRequestPayload, responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? @@ -404,6 +404,7 @@ public actor Issuer: IssuerType { let credentialIdentifier ): return try await identifierBasedRequest( + authorizedRequest: noProofRequest, token: token, credentialIdentifier: credentialIdentifier, credentialConfigurationIdentifier: credentialConfigurationIdentifier, @@ -415,7 +416,8 @@ public actor Issuer: IssuerType { let claimSet ): return try await formatBasedRequest( - token: token, + authorizedRequest: noProofRequest, + token: token, claimSet: claimSet, credentialConfigurationIdentifier: credentialConfigurationIdentifier, responseEncryptionSpecProvider: responseEncryptionSpecProvider @@ -423,9 +425,9 @@ public actor Issuer: IssuerType { } } - public func requestSingle( + public func request( proofRequest: AuthorizedRequest, - bindingKey: BindingKey, + bindingKeys: [BindingKey], requestPayload: IssuanceRequestPayload, responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? ) async throws -> Result { @@ -439,8 +441,9 @@ public actor Issuer: IssuerType { let credentialIdentifier ): return try await identifierBasedRequest( + authorizedRequest: proofRequest, token: token, - bindingKey: bindingKey, + bindingKeys: bindingKeys, credentialIdentifier: credentialIdentifier, credentialConfigurationIdentifier: credentialConfigurationIdentifier, responseEncryptionSpecProvider: responseEncryptionSpecProvider @@ -451,9 +454,10 @@ public actor Issuer: IssuerType { let claimSet ): return try await formatBasedRequest( + authorizedRequest: proofRequest, token: token, claimSet: claimSet, - bindingKey: bindingKey, + bindingKeys: bindingKeys, cNonce: cNonce(from: proofRequest), credentialConfigurationIdentifier: credentialConfigurationIdentifier, responseEncryptionSpecProvider: responseEncryptionSpecProvider @@ -478,7 +482,11 @@ private extension Issuer { ) switch result { case .success(let response): - return .success(.success(response: response)) + return .success( + .success( + response: response + ) + ) case .failure(let error): return handleIssuanceError(error) } @@ -548,9 +556,10 @@ private extension Issuer { } func formatBasedRequest( + authorizedRequest: AuthorizedRequest, token: IssuanceAccessToken, claimSet: ClaimSet?, - bindingKey: BindingKey? = nil, + bindingKeys: [BindingKey] = [], cNonce: CNonce? = nil, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, responseEncryptionSpecProvider: (_ issuerResponseEncryptionMetadata: CredentialResponseEncryption) -> IssuanceResponseEncryptionSpec? @@ -561,23 +570,28 @@ private extension Issuer { throw ValidationError.error(reason: "Invalid Supported credential for requestSingle") } + let proofs = try obtainProofs( + authorizedRequest: authorizedRequest, + batchCredentialIssuance: issuerMetadata.batchCredentialIssuance, + bindingKeys: bindingKeys, + supportedCredential: supportedCredential, + cNonce: cNonce + ) + return try await requestIssuance(token: token) { return try supportedCredential.toIssuanceRequest( requester: issuanceRequester, claimSet: claimSet, - proof: bindingKey?.toSupportedProof( - issuanceRequester: issuanceRequester, - credentialSpec: supportedCredential, - cNonce: cNonce?.value - ), + proofs: proofs, responseEncryptionSpecProvider: responseEncryptionSpecProvider ) } } func identifierBasedRequest( + authorizedRequest: AuthorizedRequest, token: IssuanceAccessToken, - bindingKey: BindingKey? = nil, + bindingKeys: [BindingKey] = [], cNonce: CNonce? = nil, credentialIdentifier: CredentialIdentifier, credentialConfigurationIdentifier: CredentialConfigurationIdentifier, @@ -589,19 +603,54 @@ private extension Issuer { throw ValidationError.error(reason: "Invalid Supported credential for requestSingle") } + let proofs = try obtainProofs( + authorizedRequest: authorizedRequest, + batchCredentialIssuance: issuerMetadata.batchCredentialIssuance, + bindingKeys: bindingKeys, + supportedCredential: supportedCredential, + cNonce: cNonce + ) + return try await requestIssuance(token: token) { return try supportedCredential.toIssuanceRequest( requester: issuanceRequester, - proof: bindingKey?.toSupportedProof( - issuanceRequester: issuanceRequester, - credentialSpec: supportedCredential, - cNonce: cNonce?.value - ), + proofs: proofs, credentialIdentifier: credentialIdentifier, responseEncryptionSpecProvider: responseEncryptionSpecProvider ) } } + + func obtainProofs( + authorizedRequest: AuthorizedRequest, + batchCredentialIssuance: BatchCredentialIssuance?, + bindingKeys: [BindingKey], + supportedCredential: CredentialSupported, + cNonce: CNonce? + ) throws -> [Proof] { + let proofs = (try? bindingKeys.compactMap { try $0.toSupportedProof( + issuanceRequester: issuanceRequester, + credentialSpec: supportedCredential, + cNonce: cNonce?.value + )}) ?? [] + switch proofs.count { + case 0: + switch authorizedRequest { + case .noProofRequired: + return proofs + case .proofRequired: + throw ValidationError.error(reason: "At least one binding is required in AuthorizedRequest.proofRequired") + } + case 1: + return proofs + default: + if let batchSize = batchCredentialIssuance?.batchSize, + proofs.count > batchSize { + throw ValidationError.issuerBatchSizeLimitExceeded(batchSize) + } + return proofs + } + } } public extension Issuer { diff --git a/Tests/Constants/TestsConstants.swift b/Tests/Constants/TestsConstants.swift index a9d048b..5392e1a 100644 --- a/Tests/Constants/TestsConstants.swift +++ b/Tests/Constants/TestsConstants.swift @@ -205,6 +205,39 @@ struct TestsConstants { ).get() } + static func createMockCredentialOfferValidEncryptionWithBatchLimit() async -> CredentialOffer? { + let credentialIssuerMetadataResolver = CredentialIssuerMetadataResolver( + fetcher: Fetcher(session: NetworkingMock( + path: "openid-credential-issuer_no_encryption_batch", + extension: "json" + ) + )) + + let authorizationServerMetadataResolver = AuthorizationServerMetadataResolver( + oidcFetcher: Fetcher(session: NetworkingMock( + path: "oidc_authorization_server_metadata", + extension: "json" + )), + oauthFetcher: Fetcher(session: NetworkingMock( + path: "test", + extension: "json" + )) + ) + + let credentialOfferRequestResolver = CredentialOfferRequestResolver( + fetcher: Fetcher(session: NetworkingMock( + path: "credential_offer_with_blank_pre_authorized_code", + extension: "json" + )), + credentialIssuerMetadataResolver: credentialIssuerMetadataResolver, + authorizationServerMetadataResolver: authorizationServerMetadataResolver + ) + + return try? await credentialOfferRequestResolver.resolve( + source: .fetchByReference(url: .stub()) + ).get() + } + static func createMockPreAuthCredentialOffer() async -> CredentialOffer? { let credentialIssuerMetadataResolver = CredentialIssuerMetadataResolver( fetcher: Fetcher(session: NetworkingMock( diff --git a/Tests/Helpers/Wallet.swift b/Tests/Helpers/Wallet.swift index 4c1c44c..dbcce84 100644 --- a/Tests/Helpers/Wallet.swift +++ b/Tests/Helpers/Wallet.swift @@ -19,18 +19,18 @@ import Foundation struct Wallet { let actingUser: ActingUser - let bindingKey: BindingKey + let bindingKeys: [BindingKey] let dPoPConstructor: DPoPConstructorType? let session: Networking init( actingUser: ActingUser, - bindingKey: BindingKey, + bindingKeys: [BindingKey], dPoPConstructor: DPoPConstructorType?, session: Networking = Self.walletSession ) { self.actingUser = actingUser - self.bindingKey = bindingKey + self.bindingKeys = bindingKeys self.dPoPConstructor = dPoPConstructor self.session = session } @@ -473,7 +473,7 @@ extension Wallet { claimSet: claimSet ) let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) } - let requestOutcome = try await issuer.requestSingle( + let requestOutcome = try await issuer.request( noProofRequest: noProofRequiredState, requestPayload: payload, responseEncryptionSpecProvider: responseEncryptionSpecProvider @@ -490,7 +490,7 @@ extension Wallet { authorized: noProofRequiredState, transactionId: transactionId ) - case .issued(let format, let credential, _, _): + case .issued(_, let credential, _, _): return credential } } else { @@ -531,9 +531,9 @@ extension Wallet { ) let responseEncryptionSpecProvider = { Issuer.createResponseEncryptionSpec($0) } - let requestOutcome = try await issuer.requestSingle( + let requestOutcome = try await issuer.request( proofRequest: authorized, - bindingKey: bindingKey, + bindingKeys: bindingKeys, requestPayload: payload, responseEncryptionSpecProvider: responseEncryptionSpecProvider ) @@ -550,7 +550,7 @@ extension Wallet { authorized: authorized, transactionId: transactionId ) - case .issued(let format, let credential, _, _): + case .issued(_, let credential, _, _): return credential } } else { diff --git a/Tests/Issuance/IssuanceAuthorizationTest.swift b/Tests/Issuance/IssuanceAuthorizationTest.swift index dfc6655..c06a6c2 100644 --- a/Tests/Issuance/IssuanceAuthorizationTest.swift +++ b/Tests/Issuance/IssuanceAuthorizationTest.swift @@ -395,9 +395,9 @@ class IssuanceAuthorizationTest: XCTestCase { claimSet: nil ) - let requestSingleResult = try await issuer.requestSingle( + let requestSingleResult = try await issuer.request( proofRequest: request, - bindingKey: bindingKey, + bindingKeys: [bindingKey], requestPayload: payload, responseEncryptionSpecProvider: { Issuer.createResponseEncryptionSpec($0) @@ -494,9 +494,9 @@ class IssuanceAuthorizationTest: XCTestCase { claimSet: nil ) - let requestSingleResult = try await issuer.requestSingle( + let requestSingleResult = try await issuer.request( proofRequest: request.handleInvalidProof(cNonce: .init(value: UUID().uuidString)!), - bindingKey: bindingKey, + bindingKeys: [bindingKey], requestPayload: payload, responseEncryptionSpecProvider: { Issuer.createResponseEncryptionSpec($0) @@ -598,9 +598,9 @@ class IssuanceAuthorizationTest: XCTestCase { claimSet: nil ) - let requestSingleResult = try await issuer.requestSingle( + let requestSingleResult = try await issuer.request( proofRequest: request, - bindingKey: bindingKey, + bindingKeys: [bindingKey], requestPayload: payload, responseEncryptionSpecProvider: { Issuer.createResponseEncryptionSpec($0) diff --git a/Tests/Issuance/IssuanceDeferredRequestTest.swift b/Tests/Issuance/IssuanceDeferredRequestTest.swift index 7b19e50..6599187 100644 --- a/Tests/Issuance/IssuanceDeferredRequestTest.swift +++ b/Tests/Issuance/IssuanceDeferredRequestTest.swift @@ -112,7 +112,7 @@ class IssuanceDeferredRequestTest: XCTestCase { ), claimSet: nil ) - let result = try await issuer.requestSingle( + let result = try await issuer.request( noProofRequest: authorized, requestPayload: payload, responseEncryptionSpecProvider: { _ in diff --git a/Tests/Issuance/IssuanceEncryptionTest.swift b/Tests/Issuance/IssuanceEncryptionTest.swift index 4161493..ac3e846 100644 --- a/Tests/Issuance/IssuanceEncryptionTest.swift +++ b/Tests/Issuance/IssuanceEncryptionTest.swift @@ -71,7 +71,7 @@ class IssuanceEncryptionTest: XCTestCase { ), claimSet: nil ) - _ = try await issuer.requestSingle( + _ = try await issuer.request( noProofRequest: authorizedRequest, requestPayload: payload, responseEncryptionSpecProvider: { _ in @@ -124,7 +124,7 @@ class IssuanceEncryptionTest: XCTestCase { ), claimSet: nil ) - _ = try await issuer.requestSingle( + _ = try await issuer.request( noProofRequest: authorizedRequest, requestPayload: payload, responseEncryptionSpecProvider: { _ in @@ -163,7 +163,7 @@ class IssuanceEncryptionTest: XCTestCase { ), claimSet: nil ) - _ = try await issuer.requestSingle( + _ = try await issuer.request( noProofRequest: authorizedRequest, requestPayload: payload, responseEncryptionSpecProvider: { _ in diff --git a/Tests/Issuance/IssuanceNotificationTest.swift b/Tests/Issuance/IssuanceNotificationTest.swift index c3ec44e..91afbbf 100644 --- a/Tests/Issuance/IssuanceNotificationTest.swift +++ b/Tests/Issuance/IssuanceNotificationTest.swift @@ -119,7 +119,7 @@ class IssuanceNotificationTest: XCTestCase { ), claimSet: nil ) - let result = try await issuer.requestSingle( + let result = try await issuer.request( noProofRequest: authorized, requestPayload: payload, responseEncryptionSpecProvider: { _ in @@ -262,7 +262,7 @@ class IssuanceNotificationTest: XCTestCase { ), claimSet: nil ) - let result = try await issuer.requestSingle( + let result = try await issuer.request( noProofRequest: authorized, requestPayload: payload, responseEncryptionSpecProvider: { _ in diff --git a/Tests/Issuance/IssuanceSingleRequestTest.swift b/Tests/Issuance/IssuanceSingleRequestTest.swift index ad12665..5f2b9e3 100644 --- a/Tests/Issuance/IssuanceSingleRequestTest.swift +++ b/Tests/Issuance/IssuanceSingleRequestTest.swift @@ -122,7 +122,7 @@ class IssuanceSingleRequestTest: XCTestCase { ), claimSet: .msoMdoc(claimSetMsoMdoc) ) - let result = try await issuer.requestSingle( + let result = try await issuer.request( noProofRequest: authorized, requestPayload: payload, responseEncryptionSpecProvider: { _ in @@ -137,7 +137,7 @@ class IssuanceSingleRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let format, let credential, _, _): + case .issued(_, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } @@ -171,7 +171,7 @@ class IssuanceSingleRequestTest: XCTestCase { func testPreAuthWhenIssuerRespondsSingleCredentialThenCredentialExists() async throws { // Given - guard let offer = await TestsConstants.createMockCredentialOfferValidEncryption() else { + guard let offer = await TestsConstants.createMockCredentialOfferValidEncryptionWithBatchLimit() else { XCTAssert(false, "Unable to resolve credential offer") return } @@ -255,7 +255,7 @@ class IssuanceSingleRequestTest: XCTestCase { ), claimSet: .msoMdoc(claimSetMsoMdoc) ) - let result = try await issuer.requestSingle( + let result = try await issuer.request( noProofRequest: authorized, requestPayload: payload, responseEncryptionSpecProvider: { _ in @@ -270,7 +270,7 @@ class IssuanceSingleRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let format, let credential, _, _): + case .issued(_, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } @@ -385,7 +385,7 @@ class IssuanceSingleRequestTest: XCTestCase { credentialIdentifier: credentialIdentifier ) - let result = try await issuer.requestSingle( + let result = try await issuer.request( noProofRequest: authorized, requestPayload: payload, responseEncryptionSpecProvider: { _ in spec }) @@ -398,7 +398,7 @@ class IssuanceSingleRequestTest: XCTestCase { switch result { case .deferred: XCTAssert(false, "Unexpected deferred") - case .issued(let format, let credential, _, _): + case .issued(_, let credential, _, _): XCTAssert(true, "credential: \(credential)") return } diff --git a/Tests/Wallet/VCIFlowNoOffer.swift b/Tests/Wallet/VCIFlowNoOffer.swift index f8bc9d7..e14483b 100644 --- a/Tests/Wallet/VCIFlowNoOffer.swift +++ b/Tests/Wallet/VCIFlowNoOffer.swift @@ -65,7 +65,7 @@ class VCIFlowNoOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: nil ) @@ -110,7 +110,7 @@ class VCIFlowNoOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: nil ) @@ -154,7 +154,7 @@ class VCIFlowNoOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: nil ) @@ -179,6 +179,95 @@ class VCIFlowNoOffer: XCTestCase { XCTAssert(true) } + + func testNoOfferMdocDraft14() async throws { + + let privateKey = try KeyController.generateECDHPrivateKey() + let publicKey = try KeyController.generateECDHPublicKey(from: privateKey) + + let alg = JWSAlgorithm(.ES256) + let publicKeyJWK = try ECPublicKey( + publicKey: publicKey, + additionalParameters: [ + "alg": alg.name, + "use": "sig", + "kid": UUID().uuidString + ]) + + let bindingKey: BindingKey = .jwk( + algorithm: alg, + jwk: publicKeyJWK, + privateKey: privateKey + ) + + let user = ActingUser( + username: "tneal", + password: "password" + ) + + let wallet = Wallet( + actingUser: user, + bindingKeys: [bindingKey, bindingKey], + dPoPConstructor: nil + ) + + do { + try await walletInitiatedIssuanceNoOfferMdoc( + wallet: wallet + ) + } catch { + + XCTExpectFailure() + XCTAssert(false, error.localizedDescription) + } + + XCTAssert(true) + } + + func testNoOfferSdJWTDraft14() async throws { + + let privateKey = try KeyController.generateECDHPrivateKey() + let publicKey = try KeyController.generateECDHPublicKey(from: privateKey) + + let alg = JWSAlgorithm(.ES256) + let publicKeyJWK = try ECPublicKey( + publicKey: publicKey, + additionalParameters: [ + "alg": alg.name, + "use": "sig", + "kid": UUID().uuidString + ]) + + let bindingKey: BindingKey = .jwk( + algorithm: alg, + jwk: publicKeyJWK, + privateKey: privateKey + ) + + let user = ActingUser( + username: "tneal", + password: "password" + ) + + let wallet = Wallet( + actingUser: user, + bindingKeys: [bindingKey], + dPoPConstructor: nil + ) + + do { + try await walletInitiatedIssuanceNoOfferSdJwt( + wallet: wallet + ) + + } catch { + + XCTExpectFailure() + XCTAssert(false, error.localizedDescription) + } + + XCTAssert(true) + } } private func walletInitiatedIssuanceNoOfferSdJwt( diff --git a/Tests/Wallet/VCIFlowWithOffer.swift b/Tests/Wallet/VCIFlowWithOffer.swift index 9e0db2f..2bd8cd6 100644 --- a/Tests/Wallet/VCIFlowWithOffer.swift +++ b/Tests/Wallet/VCIFlowWithOffer.swift @@ -56,7 +56,7 @@ class VCIFlowWithOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: nil, session: Wallet.walletSession ) @@ -101,7 +101,7 @@ class VCIFlowWithOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: nil ) @@ -145,7 +145,7 @@ class VCIFlowWithOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: nil ) @@ -189,7 +189,7 @@ class VCIFlowWithOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: nil, session: Wallet.walletSession ) @@ -234,7 +234,7 @@ class VCIFlowWithOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: nil ) @@ -279,7 +279,7 @@ class VCIFlowWithOffer: XCTestCase { let wallet = Wallet( actingUser: user, - bindingKey: bindingKey, + bindingKeys: [bindingKey], dPoPConstructor: DPoPConstructor( algorithm: alg, jwk: publicKeyJWK, From 0d6a1d939778af4602fb29158c391f5cd3e8406b Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 21 Nov 2024 14:54:31 +0200 Subject: [PATCH 12/21] [fix] updated tests constants --- Tests/Constants/TestsConstants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Constants/TestsConstants.swift b/Tests/Constants/TestsConstants.swift index 5392e1a..f840d52 100644 --- a/Tests/Constants/TestsConstants.swift +++ b/Tests/Constants/TestsConstants.swift @@ -22,7 +22,7 @@ let PID_MsoMdoc_config_id = "eu.europa.ec.eudi.pid_mso_mdoc" let PID_SdJwtVC_config_id = "eu.europa.ec.eudi.pid_vc_sd_jwt" //let CREDENTIAL_ISSUER_PUBLIC_URL = "https://dev.issuer.eudiw.dev" -//let PID_SdJwtVC_config_id = "eu.europa.ec.eudi.mdl_jwt_vc_json" +//let PID_SdJwtVC_config_id = "eu.europa.ec.eudi.pid_jwt_vc_json" //let PID_MsoMdoc_config_id = "eu.europa.ec.eudi.pid_mdoc" //let MDL_config_id = "eu.europa.ec.eudi.mdl_mdoc" From 9db44e15f7d852df585b361811c9957487cd7f23 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 22 Nov 2024 11:08:59 +0200 Subject: [PATCH 13/21] [fix] moved auth details --- .../AuthorizationDetails.swift | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 Sources/Entities/AuthorizationDetails/AuthorizationDetails.swift diff --git a/Sources/Entities/AuthorizationDetails/AuthorizationDetails.swift b/Sources/Entities/AuthorizationDetails/AuthorizationDetails.swift new file mode 100644 index 0000000..6bb41c9 --- /dev/null +++ b/Sources/Entities/AuthorizationDetails/AuthorizationDetails.swift @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation + +// MARK: - OidCredentialAuthorizationDetail + +public protocol OidCredentialAuthorizationDetail {} + +// MARK: - ByCredentialConfiguration + +public struct ByCredentialConfiguration: Codable, OidCredentialAuthorizationDetail { + public let credentialConfigurationId: CredentialConfigurationIdentifier + public let credentialIdentifiers: [CredentialIdentifier]? + + public init(credentialConfigurationId: CredentialConfigurationIdentifier, credentialIdentifiers: [CredentialIdentifier]? = nil) { + self.credentialConfigurationId = credentialConfigurationId + self.credentialIdentifiers = credentialIdentifiers + } +} + +// MARK: - ByFormat + +public enum ByFormat: Codable, OidCredentialAuthorizationDetail { + case msoMdocAuthorizationDetails(MsoMdocAuthorizationDetails) + case sdJwtVcAuthorizationDetails(SdJwtVcAuthorizationDetails) + + public enum CodingKeys: String, CodingKey { + case type, details + } + + public enum ByFormatType: String, Codable { + case msoMdocAuthorizationDetails + case sdJwtVcAuthorizationDetails + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(ByFormatType.self, forKey: .type) + let nestedDecoder = try container.superDecoder(forKey: .details) + + switch type { + case .msoMdocAuthorizationDetails: + let details = try MsoMdocAuthorizationDetails(from: nestedDecoder) + self = .msoMdocAuthorizationDetails(details) + case .sdJwtVcAuthorizationDetails: + let details = try SdJwtVcAuthorizationDetails(from: nestedDecoder) + self = .sdJwtVcAuthorizationDetails(details) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .msoMdocAuthorizationDetails(let details): + try container.encode(ByFormatType.msoMdocAuthorizationDetails, forKey: .type) + try details.encode(to: container.superEncoder(forKey: .details)) + case .sdJwtVcAuthorizationDetails(let details): + try container.encode(ByFormatType.sdJwtVcAuthorizationDetails, forKey: .type) + try details.encode(to: container.superEncoder(forKey: .details)) + } + } +} + +// MARK: - MsoMdocAuthorizationDetails + +public struct MsoMdocAuthorizationDetails: Codable { + public let doctype: String + + public init(doctype: String) { + self.doctype = doctype + } +} + +// MARK: - SdJwtVcAuthorizationDetails + +public struct SdJwtVcAuthorizationDetails: Codable { + public let vct: String + + public init(vct: String) { + self.vct = vct + } +} From 3a145871a186dd4634087c719ab2dd6ed7354397 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 22 Nov 2024 11:09:26 +0200 Subject: [PATCH 14/21] [fix] added auth details for token request enum --- .../AuthDetailsForTokenRequest.swift | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Sources/Entities/AuthorizationDetails/AuthDetailsForTokenRequest.swift diff --git a/Sources/Entities/AuthorizationDetails/AuthDetailsForTokenRequest.swift b/Sources/Entities/AuthorizationDetails/AuthDetailsForTokenRequest.swift new file mode 100644 index 0000000..e55d5b3 --- /dev/null +++ b/Sources/Entities/AuthorizationDetails/AuthDetailsForTokenRequest.swift @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation + +public enum AuthorizationDetailsInTokenRequest { + case doNotInclude + case include(filter: (CredentialConfigurationIdentifier) -> Bool) +} + From acaf6f733a331baf1423486b178ff17ce7895e68 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 22 Nov 2024 11:10:05 +0200 Subject: [PATCH 15/21] [fix] added new constant --- Sources/Utilities/Constants.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Utilities/Constants.swift b/Sources/Utilities/Constants.swift index c258350..5f7d1d8 100644 --- a/Sources/Utilities/Constants.swift +++ b/Sources/Utilities/Constants.swift @@ -23,7 +23,7 @@ public struct Constants { public static let CLIENT_ID_PARAM = "client_id" public static let CODE_VERIFIER_PARAM = "code_verifier" public static let AUTHORIZATION_CODE_PARAM = "code" - + public static let AUTHORIZATION_DETAILS = "authorization_details" public static let USER_PIN_PARAM = "user_pin" public static let PRE_AUTHORIZED_CODE_PARAM = "pre-authorized_code" From 9bcb4e616ad2acf9bf5996b9afc59ec637bb7f6d Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 22 Nov 2024 11:10:26 +0200 Subject: [PATCH 16/21] [fix] updated tests constants --- Tests/Constants/TestsConstants.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/Constants/TestsConstants.swift b/Tests/Constants/TestsConstants.swift index f840d52..c29c2af 100644 --- a/Tests/Constants/TestsConstants.swift +++ b/Tests/Constants/TestsConstants.swift @@ -135,7 +135,8 @@ struct TestsConstants { pkceVerifier: (try? .init( codeVerifier: "GVaOE~J~xQmkE4aCKm4RNYviYW5QaFiFOxVv-8enIDL", codeVerifierMethod: "S256"))!, - state: "5A201471-D088-4544-B1E9-5476E5935A95" + state: "5A201471-D088-4544-B1E9-5476E5935A95", + configurationIds: [try! .init(value: "my_credential_configuration_id")] ) ) From eeb9f69ec7e554b0edaedd89af049402973ea83c Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 22 Nov 2024 11:11:55 +0200 Subject: [PATCH 17/21] [fix] updated authorization detail --- .../AccessManagement/AuthorizationDetail.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/Entities/AccessManagement/AuthorizationDetail.swift b/Sources/Entities/AccessManagement/AuthorizationDetail.swift index c475c00..264c08e 100644 --- a/Sources/Entities/AccessManagement/AuthorizationDetail.swift +++ b/Sources/Entities/AccessManagement/AuthorizationDetail.swift @@ -16,11 +16,11 @@ import Foundation public struct AuthorizationType: Codable { - public let type: String - - public init(type: String) { - self.type = type - } + public let type: String + + public init(type: String) { + self.type = type + } } public struct AuthorizationDetail: Codable { From 123b01f79c82aa96d0dbf1fad55d92c6a2b1eff6 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 22 Nov 2024 13:35:17 +0200 Subject: [PATCH 18/21] [fix] authorisation details in token endpoint --- Sources/Entities/AuthorizationDetails.swift | 96 -------------- .../Issuance/UnauthorizedRequest.swift | 10 +- .../Request/AuthorizationRequest.swift | 1 - Sources/Issuers/Issuer.swift | 49 +++++-- .../AuthorizationServerClient.swift | 125 +++++++++++++----- 5 files changed, 140 insertions(+), 141 deletions(-) delete mode 100644 Sources/Entities/AuthorizationDetails.swift diff --git a/Sources/Entities/AuthorizationDetails.swift b/Sources/Entities/AuthorizationDetails.swift deleted file mode 100644 index 6bb41c9..0000000 --- a/Sources/Entities/AuthorizationDetails.swift +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import Foundation - -// MARK: - OidCredentialAuthorizationDetail - -public protocol OidCredentialAuthorizationDetail {} - -// MARK: - ByCredentialConfiguration - -public struct ByCredentialConfiguration: Codable, OidCredentialAuthorizationDetail { - public let credentialConfigurationId: CredentialConfigurationIdentifier - public let credentialIdentifiers: [CredentialIdentifier]? - - public init(credentialConfigurationId: CredentialConfigurationIdentifier, credentialIdentifiers: [CredentialIdentifier]? = nil) { - self.credentialConfigurationId = credentialConfigurationId - self.credentialIdentifiers = credentialIdentifiers - } -} - -// MARK: - ByFormat - -public enum ByFormat: Codable, OidCredentialAuthorizationDetail { - case msoMdocAuthorizationDetails(MsoMdocAuthorizationDetails) - case sdJwtVcAuthorizationDetails(SdJwtVcAuthorizationDetails) - - public enum CodingKeys: String, CodingKey { - case type, details - } - - public enum ByFormatType: String, Codable { - case msoMdocAuthorizationDetails - case sdJwtVcAuthorizationDetails - } - - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let type = try container.decode(ByFormatType.self, forKey: .type) - let nestedDecoder = try container.superDecoder(forKey: .details) - - switch type { - case .msoMdocAuthorizationDetails: - let details = try MsoMdocAuthorizationDetails(from: nestedDecoder) - self = .msoMdocAuthorizationDetails(details) - case .sdJwtVcAuthorizationDetails: - let details = try SdJwtVcAuthorizationDetails(from: nestedDecoder) - self = .sdJwtVcAuthorizationDetails(details) - } - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - switch self { - case .msoMdocAuthorizationDetails(let details): - try container.encode(ByFormatType.msoMdocAuthorizationDetails, forKey: .type) - try details.encode(to: container.superEncoder(forKey: .details)) - case .sdJwtVcAuthorizationDetails(let details): - try container.encode(ByFormatType.sdJwtVcAuthorizationDetails, forKey: .type) - try details.encode(to: container.superEncoder(forKey: .details)) - } - } -} - -// MARK: - MsoMdocAuthorizationDetails - -public struct MsoMdocAuthorizationDetails: Codable { - public let doctype: String - - public init(doctype: String) { - self.doctype = doctype - } -} - -// MARK: - SdJwtVcAuthorizationDetails - -public struct SdJwtVcAuthorizationDetails: Codable { - public let vct: String - - public init(vct: String) { - self.vct = vct - } -} diff --git a/Sources/Entities/Issuance/UnauthorizedRequest.swift b/Sources/Entities/Issuance/UnauthorizedRequest.swift index 5f47371..f2a4bff 100644 --- a/Sources/Entities/Issuance/UnauthorizedRequest.swift +++ b/Sources/Entities/Issuance/UnauthorizedRequest.swift @@ -21,17 +21,20 @@ public struct ParRequested { public let getAuthorizationCodeURL: GetAuthorizationCodeURL public let pkceVerifier: PKCEVerifier public let state: String + public let configurationIds: [CredentialConfigurationIdentifier] public init( credentials: [CredentialIdentifier], getAuthorizationCodeURL: GetAuthorizationCodeURL, pkceVerifier: PKCEVerifier, - state: String + state: String, + configurationIds: [CredentialConfigurationIdentifier] ) { self.credentials = credentials self.getAuthorizationCodeURL = getAuthorizationCodeURL self.pkceVerifier = pkceVerifier self.state = state + self.configurationIds = configurationIds } } @@ -41,11 +44,13 @@ public struct AuthorizationCodeRetrieved { public let credentials: [CredentialIdentifier] public let authorizationCode: IssuanceAuthorization public let pkceVerifier: PKCEVerifier + public let configurationIds: [CredentialConfigurationIdentifier] public init( credentials: [CredentialIdentifier], authorizationCode: IssuanceAuthorization, - pkceVerifier: PKCEVerifier + pkceVerifier: PKCEVerifier, + configurationIds: [CredentialConfigurationIdentifier] ) throws { guard case .authorizationCode = authorizationCode else { @@ -55,6 +60,7 @@ public struct AuthorizationCodeRetrieved { self.credentials = credentials self.authorizationCode = authorizationCode self.pkceVerifier = pkceVerifier + self.configurationIds = configurationIds } } diff --git a/Sources/Entities/Request/AuthorizationRequest.swift b/Sources/Entities/Request/AuthorizationRequest.swift index c7a146f..c9b8f1e 100644 --- a/Sources/Entities/Request/AuthorizationRequest.swift +++ b/Sources/Entities/Request/AuthorizationRequest.swift @@ -95,7 +95,6 @@ public struct AuthorizationRequest: Codable { self.prompt = prompt self.dpopJkt = dpopJkt self.trustChain = trustChain - self.issuerState = issuerState } } diff --git a/Sources/Issuers/Issuer.swift b/Sources/Issuers/Issuer.swift index 912050b..ef6c2bd 100644 --- a/Sources/Issuers/Issuer.swift +++ b/Sources/Issuers/Issuer.swift @@ -26,7 +26,8 @@ public protocol IssuerType { credentialOffer: CredentialOffer, authorizationCode: IssuanceAuthorization, clientId: String, - transactionCode: String? + transactionCode: String?, + authorizationDetailsInTokenRequest: AuthorizationDetailsInTokenRequest ) async -> Result func handleAuthorizationCode( @@ -35,7 +36,8 @@ public protocol IssuerType { ) async -> Result func authorizeWithAuthorizationCode( - authorizationCode: UnauthorizedRequest + authorizationCode: UnauthorizedRequest, + authorizationDetailsInTokenRequest: AuthorizationDetailsInTokenRequest ) async -> Result func request( @@ -156,7 +158,8 @@ public actor Issuer: IssuerType { credentials: try credentials.map { try CredentialIdentifier(value: $0.value) }, getAuthorizationCodeURL: result.code, pkceVerifier: result.verifier, - state: state + state: state, + configurationIds: credentialConfogurationIdentifiers ) ) ) @@ -181,7 +184,8 @@ public actor Issuer: IssuerType { credentials: try credentials.map { try CredentialIdentifier(value: $0.value) }, getAuthorizationCodeURL: result.code, pkceVerifier: result.verifier, - state: state + state: state, + configurationIds: credentialConfogurationIdentifiers ) ) ) @@ -196,7 +200,8 @@ public actor Issuer: IssuerType { credentialOffer: CredentialOffer, authorizationCode: IssuanceAuthorization, clientId: String, - transactionCode: String? + transactionCode: String?, + authorizationDetailsInTokenRequest: AuthorizationDetailsInTokenRequest = .doNotInclude ) async -> Result { switch authorizationCode { @@ -212,11 +217,17 @@ public actor Issuer: IssuerType { } } + let credConfigIdsAsAuthDetails: [CredentialConfigurationIdentifier] = switch authorizationDetailsInTokenRequest { + case .doNotInclude: [] + case .include(let filter): credentialOffer.credentialConfigurationIdentifiers.filter(filter) + } + let response = try await authorizer.requestAccessTokenPreAuthFlow( preAuthorizedCode: authorisation, txCode: txCode, clientId: clientId, - transactionCode: transactionCode + transactionCode: transactionCode, + identifiers: credConfigIdsAsAuthDetails ) switch response { @@ -262,15 +273,25 @@ public actor Issuer: IssuerType { } } - public func authorizeWithAuthorizationCode(authorizationCode: UnauthorizedRequest) async -> Result { + public func authorizeWithAuthorizationCode( + authorizationCode: UnauthorizedRequest, + authorizationDetailsInTokenRequest: AuthorizationDetailsInTokenRequest = .doNotInclude + ) async -> Result { switch authorizationCode { case .par: - return .failure(ValidationError.error(reason: ".authorizationCode case is required")) + return .failure( + ValidationError.error(reason: ".authorizationCode case is required") + ) case .authorizationCode(let request): switch request.authorizationCode { case .authorizationCode(let authorizationCode): do { + let credConfigIdsAsAuthDetails: [CredentialConfigurationIdentifier] = switch authorizationDetailsInTokenRequest { + case .doNotInclude: [] + case .include(let filter): request.configurationIds.filter(filter) + } + let response: ( accessToken: IssuanceAccessToken, nonce: CNonce?, @@ -279,7 +300,8 @@ public actor Issuer: IssuerType { expiresIn: Int? ) = try await authorizer.requestAccessTokenAuthFlow( authorizationCode: authorizationCode, - codeVerifier: request.pkceVerifier.codeVerifier + codeVerifier: request.pkceVerifier.codeVerifier, + identifiers: credConfigIdsAsAuthDetails ).get() if let cNonce = response.nonce { @@ -313,7 +335,8 @@ public actor Issuer: IssuerType { } catch { return .failure(ValidationError.error(reason: error.localizedDescription)) } - default: return .failure(ValidationError.error(reason: ".authorizationCode case is required")) + default: return .failure( + ValidationError.error(reason: ".authorizationCode case is required")) } } } @@ -330,7 +353,8 @@ public actor Issuer: IssuerType { try .init( credentials: request.credentials, authorizationCode: try IssuanceAuthorization(authorizationCode: code), - pkceVerifier: request.pkceVerifier + pkceVerifier: request.pkceVerifier, + configurationIds: request.configurationIds ) ) ) @@ -356,7 +380,8 @@ public actor Issuer: IssuerType { try .init( credentials: request.credentials, authorizationCode: try IssuanceAuthorization(authorizationCode: authorizationCode), - pkceVerifier: request.pkceVerifier + pkceVerifier: request.pkceVerifier, + configurationIds: request.configurationIds ) ) ) diff --git a/Sources/Main/Authorisers/AuthorizationServerClient.swift b/Sources/Main/Authorisers/AuthorizationServerClient.swift index 6b67796..6e3dd08 100644 --- a/Sources/Main/Authorisers/AuthorizationServerClient.swift +++ b/Sources/Main/Authorisers/AuthorizationServerClient.swift @@ -37,14 +37,16 @@ public protocol AuthorizationServerClientType { func requestAccessTokenAuthFlow( authorizationCode: String, - codeVerifier: String + codeVerifier: String, + identifiers: [CredentialConfigurationIdentifier] ) async throws -> Result<(IssuanceAccessToken, CNonce?, AuthorizationDetailsIdentifiers?, TokenType?, Int?), ValidationError> func requestAccessTokenPreAuthFlow( preAuthorizedCode: String, txCode: TxCode?, clientId: String, - transactionCode: String? + transactionCode: String?, + identifiers: [CredentialConfigurationIdentifier] ) async throws -> Result<(IssuanceAccessToken, CNonce?, AuthorizationDetailsIdentifiers?, Int?), ValidationError> } @@ -191,7 +193,7 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { } let codeVerifier = PKCEGenerator.codeVerifier() ?? "" - let authzRequest = AuthorizationRequest( + let authRequest = AuthorizationRequest( responseType: Self.responseType, clientId: config.clientId, redirectUri: config.authFlowRedirectionURI.absoluteString, @@ -211,7 +213,7 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { let response: PushedAuthorizationRequestResponse = try await service.formPost( poster: parPoster, url: parEndpoint, - request: authzRequest + request: authRequest ) switch response { @@ -251,7 +253,8 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { public func requestAccessTokenAuthFlow( authorizationCode: String, - codeVerifier: String + codeVerifier: String, + identifiers: [CredentialConfigurationIdentifier] ) async throws -> Result<( IssuanceAccessToken, CNonce?, @@ -260,18 +263,19 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { Int? ), ValidationError> { - let parameters: [String: String] = authCodeFlow( + let parameters: JSON = authCodeFlow( authorizationCode: authorizationCode, redirectionURI: redirectionURI, clientId: clientId, - codeVerifier: codeVerifier + codeVerifier: codeVerifier, + identifiers: identifiers ) - + let response: AccessTokenRequestResponse = try await service.formPost( poster: tokenPoster, - url: tokenEndpoint, + url: tokenEndpoint, headers: try tokenEndPointHeaders(), - parameters: parameters + parameters: parameters.toDictionary().convertToDictionaryOfStrings() ) switch response { @@ -297,13 +301,15 @@ public actor AuthorizationServerClient: AuthorizationServerClientType { preAuthorizedCode: String, txCode: TxCode?, clientId: String, - transactionCode: String? + transactionCode: String?, + identifiers: [CredentialConfigurationIdentifier] ) async throws -> Result<(IssuanceAccessToken, CNonce?, AuthorizationDetailsIdentifiers?, Int?), ValidationError> { let parameters: JSON = try await preAuthCodeFlow( preAuthorizedCode: preAuthorizedCode, txCode: txCode, clientId: clientId, - transactionCode: transactionCode + transactionCode: transactionCode, + identifiers: identifiers ) let response: AccessTokenRequestResponse = try await service.formPost( @@ -363,39 +369,98 @@ private extension AuthorizationServerClient { authorizationCode: String, redirectionURI: URL, clientId: String, - codeVerifier: String - ) -> [String: String] { + codeVerifier: String, + identifiers: [CredentialConfigurationIdentifier] + ) -> JSON { - [ + var params: [String: String?] = [ Constants.GRANT_TYPE_PARAM: Self.grantAuthorizationCode, Constants.AUTHORIZATION_CODE_PARAM: authorizationCode, Constants.REDIRECT_URI_PARAM: redirectionURI.absoluteString, Constants.CLIENT_ID_PARAM: clientId, Constants.CODE_VERIFIER_PARAM: codeVerifier, ] + + appendAuthorizationDetailsIfValid( + to: ¶ms, + identifiers: identifiers, + type: .init(type: OPENID_CREDENTIAL) + ) + + return JSON(params.filter { $0.value != nil }) } func preAuthCodeFlow( preAuthorizedCode: String, txCode: TxCode?, clientId: String, - transactionCode: String? + transactionCode: String?, + identifiers: [CredentialConfigurationIdentifier] ) async throws -> JSON { + var params: [String: String?] = [ + Constants.CLIENT_ID_PARAM: clientId, + Constants.GRANT_TYPE_PARAM: Constants.GRANT_TYPE_PARAM_VALUE, + Constants.PRE_AUTHORIZED_CODE_PARAM: preAuthorizedCode + ] + if txCode != nil { - let dictionary: [String: Any?] = [ - Constants.CLIENT_ID_PARAM: clientId, - Constants.GRANT_TYPE_PARAM: Constants.GRANT_TYPE_PARAM_VALUE, - Constants.PRE_AUTHORIZED_CODE_PARAM: preAuthorizedCode, - Constants.TX_CODE_PARAM: transactionCode - ].filter { $0.value != nil } - return JSON(dictionary) - - } else { - return [ - Constants.CLIENT_ID_PARAM: clientId, - Constants.GRANT_TYPE_PARAM: Constants.GRANT_TYPE_PARAM_VALUE, - Constants.PRE_AUTHORIZED_CODE_PARAM: preAuthorizedCode - ] + params[Constants.TX_CODE_PARAM] = transactionCode + } + + appendAuthorizationDetailsIfValid( + to: ¶ms, + identifiers: identifiers, + type: .init(type: OPENID_CREDENTIAL) + ) + + return JSON(params.filter { $0.value != nil }) + } + + func appendAuthorizationDetailsIfValid( + to params: inout [String: String?], + identifiers: [CredentialConfigurationIdentifier], + type: AuthorizationType + ) { + + guard !identifiers.isEmpty else { return } + + let formParameterString = identifiers + .convertToAuthorizationDetails(withType: type) + .toFormParameterString() + + if let formParameterString = formParameterString, !formParameterString.isEmpty { + params[Constants.AUTHORIZATION_DETAILS] = formParameterString + } + } +} + +extension Array where Element == CredentialConfigurationIdentifier { + func convertToAuthorizationDetails(withType type: AuthorizationType) -> [AuthorizationDetail] { + return self.map { identifier in + AuthorizationDetail( + type: type, + locations: [], + credentialConfigurationId: identifier.value + ) } } } + +extension Array where Element == AuthorizationDetail { + func toFormParameterString() -> String? { + let encoder = JSONEncoder() + encoder.outputFormatting = .sortedKeys + + do { + let jsonData = try encoder.encode(self) + if let jsonString = String(data: jsonData, encoding: .utf8) { + // URL encode the JSON string + if let urlEncoded = jsonString.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + return urlEncoded + } + } + } catch {} + + return nil + } +} From 8277e300b18c8b705388b9ef45db6e1f80b941cf Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 25 Nov 2024 09:12:48 +0200 Subject: [PATCH 19/21] [fix] minor readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0c24c3..ca46907 100644 --- a/README.md +++ b/README.md @@ -204,7 +204,7 @@ let payload: IssuanceRequestPayload = .configurationBased( let requestOutcome = try await issuer.request( proofRequest: ..., - bindingKey: ..., + bindingKeys: ..., // BindingKey array requestPayload: payload, responseEncryptionSpecProvider: { Issuer.createResponseEncryptionSpec($0) From 599665941deac47137470cd8a1e39c1ef3ce3f0c Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 2 Dec 2024 10:47:46 +0200 Subject: [PATCH 20/21] [fix] updated batch tests --- Sources/Entities/Types/ClaimSet.swift | 1 - ...edential_issuance_success_response_credentials.json | 10 ++++++++++ Sources/Utilities/Functions/Functions.swift | 1 - Tests/Issuance/IssuanceBatchRequestTest.swift | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 Sources/Resources/responses/batch_credential_issuance_success_response_credentials.json diff --git a/Sources/Entities/Types/ClaimSet.swift b/Sources/Entities/Types/ClaimSet.swift index bd1217d..9cf9569 100644 --- a/Sources/Entities/Types/ClaimSet.swift +++ b/Sources/Entities/Types/ClaimSet.swift @@ -110,7 +110,6 @@ public enum ClaimSet: Codable { if !claims.isEmpty { for claimName in c { if !claims.contains(where: { $0 == claimName}) { - print(claimName) throw ValidationError.error(reason: "Requested claim name \(claimName) is not supported by issuer") } } diff --git a/Sources/Resources/responses/batch_credential_issuance_success_response_credentials.json b/Sources/Resources/responses/batch_credential_issuance_success_response_credentials.json new file mode 100644 index 0000000..497a599 --- /dev/null +++ b/Sources/Resources/responses/batch_credential_issuance_success_response_credentials.json @@ -0,0 +1,10 @@ +{ + "iss": "https://dev.issuer-backend.eudiw.dev", + "notification_id": "b7a3ae54-4bac-4e74-b665-ebbd6905cb6b", + "c_nonce": "8118455e-4f73-4019-90a1-8f2484e530c1", + "credentials": [ + "first_credential", + "secondcredential" + ], + "iat": 1733126673 +} diff --git a/Sources/Utilities/Functions/Functions.swift b/Sources/Utilities/Functions/Functions.swift index 31af7a4..0499a12 100644 --- a/Sources/Utilities/Functions/Functions.swift +++ b/Sources/Utilities/Functions/Functions.swift @@ -21,7 +21,6 @@ public func convertToJsonString(dictionary: [String: Any]) -> String? { let jsonString = String(data: jsonData, encoding: .utf8) return jsonString } catch { - print("Error converting dictionary to JSON string: \(error)") return nil } } diff --git a/Tests/Issuance/IssuanceBatchRequestTest.swift b/Tests/Issuance/IssuanceBatchRequestTest.swift index 3f6156a..af7bd93 100644 --- a/Tests/Issuance/IssuanceBatchRequestTest.swift +++ b/Tests/Issuance/IssuanceBatchRequestTest.swift @@ -83,7 +83,7 @@ class IssuanceBatchRequestTest: XCTestCase { ), requesterPoster: Poster( session: NetworkingMock( - path: "batch_issuance_success_response_credential", + path: "batch_credential_issuance_success_response_credentials", extension: "json" ) ) From 699a87e3280661fc1dc0bdc0ad431616f640c70a Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 2 Dec 2024 12:58:09 +0200 Subject: [PATCH 21/21] [fix] updated mock data --- ...credential-issuer_no_encryption_batch.json | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/Sources/Resources/well-known/openid-credential-issuer_no_encryption_batch.json b/Sources/Resources/well-known/openid-credential-issuer_no_encryption_batch.json index 3839097..1f10e47 100644 --- a/Sources/Resources/well-known/openid-credential-issuer_no_encryption_batch.json +++ b/Sources/Resources/well-known/openid-credential-issuer_no_encryption_batch.json @@ -111,6 +111,53 @@ } } }, + "eu.europa.ec.eudi.pid_mdoc": { + "format": "mso_mdoc", + "scope": "eu.europa.ec.eudi.pid_mso_mdoc", + "doctype": "org.iso.18013.5.1.PID", + "cryptographic_binding_methods_supported": [ + "jwk" + ], + "credential_signing_alg_values_supported": [ + "RS256" + ], + "display": [ + { + "name": "Personal Identification Data", + "locale": "en-US", + "logo": { + "uri": "https://examplestate.com/public/pid.png", + "alt_text": "a square figure of a mobile driving license" + }, + "background_color": "#12107c", + "text_color": "#FFFFFF" + } + ], + "claims": { + "org.iso.18013.5.1": { + "given_name": { + "display": [ + { + "name": "Given Name", + "locale": "en-US" + } + ] + }, + "family_name": { + "display": [ + { + "name": "Surname", + "locale": "en-US" + } + ] + }, + "birth_date": {} + }, + "org.iso.18013.5.1.aamva": { + "organ_donor": {} + } + } + }, "UniversityDegree_mso_mdoc": { "format": "mso_mdoc", "scope": "UniversityDegree",