diff --git a/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift b/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift index 0c4f5827..94e0257c 100644 --- a/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift +++ b/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift @@ -128,3 +128,95 @@ public extension Pollux { ) } } + +public enum OperationResult { + case credential(Credential) + case forward(type: String, format: String?, payload: Data) + case verification(verified: Bool) + + public var credential: Credential? { + switch self { + case .credential(let credential): + return credential + default: + return nil + } + } + + public var forwardType: String? { + switch self { + case .forward(type: let type, format: _, payload: _): + return type + default: + return nil + } + } + + public var forwardPayload: Data? { + switch self { + case .forward(type: _, format: _, payload: let payload): + return payload + default: + return nil + } + } + + public var isVerified: Bool? { + switch self { + case .verification(verified: let verified): + return verified + default: + return nil + } + } +} + +public protocol PolluxPlugin { + var version: String { get } + var supportedOperations: [String] { get } + + func requiredOptions(operation: String) -> [CredentialOperationsOptions] + + func operation(type: String, format: String?, payload: Data?, options: [CredentialOperationsOptions]) async throws -> OperationResult +} + +public protocol CredentialPlugin: PolluxPlugin { + var credentialType: String { get } + + func createCredential(_ credentialData: Data) async throws -> Credential + func credential(_ imported: Data) async throws -> Credential +} + +public protocol ProtocolPlugin: PolluxPlugin { + var supportedCredentialTypes: [String] { get } +} + +public protocol ProtocolCreateIssuancePlugin: ProtocolPlugin { + var protocolType: String { get } + var version: String { get } + + // This is just a mock still to define + func issueOffer(withClaims: [ClaimFilter], issuer: DID, subject: DID) async throws -> OperationResult + // This is just a mock still to define + func issueCredential(withClaims: [ClaimFilter], issuer: DID, subject: DID) async throws -> OperationResult +} + +public protocol ProtocolCreatePresentationPlugin: ProtocolPlugin { + var protocolType: String { get } + var version: String { get } + + // This needs to be mocked still + func requestPresentation(withClaims: [ClaimFilter]) async throws -> OperationResult +} + +extension ProtocolPlugin { + var credentialIssuance: ProtocolCreateIssuancePlugin? { + return self as? ProtocolCreateIssuancePlugin + } +} + +extension ProtocolPlugin { + var credentialPresentation: ProtocolCreatePresentationPlugin? { + return self as? ProtocolCreatePresentationPlugin + } +} diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift index d49879cc..9ad13bf0 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift @@ -146,15 +146,22 @@ public extension DIDCommAgent { throw EdgeAgentError.invalidAttachmentFormat(nil) } - let credential = try await pollux.parseCredential( - type: format, - credentialPayload: jsonData, + guard let plugin = edgeAgent.credentialPlugins.first( where: { $0.supportedOperations.contains(message.type) + }) else { + throw EdgeAgentError.invalidAttachmentFormat(nil) + } + guard let credential = try await plugin.operation( + type: message.type, + format: attachment.format, + payload: jsonData, options: [ .linkSecret(id: "", secret: linkSecretString), .credentialDefinitionDownloader(downloader: downloader), .schemaDownloader(downloader: downloader) ] - ) + ).credential else { + throw EdgeAgentError.invalidAttachmentFormat(nil) + } guard let storableCredential = credential.storable else { return credential diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift index 4acaf83f..e1ce1853 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent.swift @@ -15,6 +15,7 @@ public class EdgeAgent { public let castor: Castor public let pluto: Pluto public let pollux: Pollux & CredentialImporter + public let credentialPlugins: [PolluxPlugin] public static func setupLogging(logLevels: [LogComponent: LogLevel]) { SDKLogger.logLevels = logLevels @@ -35,12 +36,14 @@ public class EdgeAgent { castor: Castor, pluto: Pluto, pollux: Pollux & CredentialImporter, + credentialPlugins: [PolluxPlugin] = [], seed: Seed? = nil ) { self.apollo = apollo self.castor = castor self.pluto = pluto self.pollux = pollux + self.credentialPlugins = credentialPlugins self.seed = seed ?? apollo.createRandomSeed().seed } diff --git a/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/DIDCommPlugin.swift b/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/DIDCommPlugin.swift new file mode 100644 index 00000000..9e1a8355 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/DIDCommPlugin.swift @@ -0,0 +1,62 @@ +import Domain +import Foundation + +struct DIDCommPlugin: ProtocolPlugin { + let version: String = "0.1" + let supportedOperations: [String] = [ + "https://didcomm.org/issue-credential/3.0/offer-credential", + "https://didcomm.org/issue-credential/3.0/issue-credential" + ] + var supportedCredentialTypes: [String] { + credentialPlugins.map(\.credentialType) + } + private let supportProtocols: [ProtocolPlugin] + private let credentialPlugins: [CredentialPlugin] + + func requiredOptions(operation: String) -> [Domain.CredentialOperationsOptions] { + [] + } + func operation( + type: String, + format: String?, + payload: Data?, + options: [Domain.CredentialOperationsOptions] + ) async throws -> Domain.OperationResult { + guard let format else { throw PolluxError.unsupportedIssuedMessage } + switch type { + case "https://didcomm.org/issue-credential/3.0/offer-credential": + guard + let supportProtocol = supportProtocols + .first(where: { + $0.supportedOperations.contains("offer-credential") && $0.supportedCredentialTypes.contains(format) + }) + else { + throw PolluxError.unsupportedIssuedMessage + } + return try await supportProtocol.operation( + type: "offer-credential", + format: format, + payload: payload, + options: options + ) + case "https://didcomm.org/issue-credential/3.0/issue-credential": + guard + let supportProtocol = supportProtocols + .first(where: { + $0.supportedOperations.contains("issue-credential") + && $0.supportedCredentialTypes.contains(format) + }) + else { + throw PolluxError.unsupportedIssuedMessage + } + return try await supportProtocol.operation( + type: "issue-credential", + format: format, + payload: payload, + options: options + ) + default: + return .verification(verified: false) + } + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/JWTCredentialPlugin.swift b/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/JWTCredentialPlugin.swift new file mode 100644 index 00000000..869a6a5d --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/JWTCredentialPlugin.swift @@ -0,0 +1,77 @@ +import Domain +import Foundation +import JSONWebKey +import JSONWebToken +import JSONWebSignature + +struct JWTCredentialPlugin: CredentialPlugin { + let version = "0.1" + let credentialType = "jwt" + let supportedOperations = [ + "offer", + "offer-credential", + "issue", + "issue-credential" + ] + + func requiredOptions(operation: String) -> [Domain.CredentialOperationsOptions] { + [] + } + + func operation( + type: String, + format: String?, + payload: Data?, + options: [Domain.CredentialOperationsOptions] + ) async throws -> Domain.OperationResult { + guard let payload else { throw PolluxError.invalidJWTCredential } + switch type { + case "offer", "offer-credential": + let processedJWTCredentialRequest = try await processJWTCredentialRequest( + offerData: payload, + options: options + ) + return try .forward( + type: "request-credential", + format: format, + payload: processedJWTCredentialRequest.tryToData() + ) + case "issue", "issue-credential": + return try await .credential(createCredential(payload)) + default: + throw PolluxError.unsupportedIssuedMessage + } + } + + func createCredential(_ credentialData: Data) async throws -> Credential { + try JWTCredential(data: credentialData) + } + + func credential(_ imported: Data) async throws -> Credential { + try JWTCredential(data: imported) + } + + private func processJWTCredentialRequest(offerData: Data, options: [CredentialOperationsOptions]) async throws -> String { + guard + let subjectDIDOption = options.first(where: { + if case .subjectDID = $0 { return true } + return false + }), + case let CredentialOperationsOptions.subjectDID(did) = subjectDIDOption + else { + throw PolluxError.invalidPrismDID + } + + guard + let exportableKeyOption = options.first(where: { + if case .exportableKey = $0 { return true } + return false + }), + case let CredentialOperationsOptions.exportableKey(exportableKey) = exportableKeyOption + else { + throw PolluxError.requiresExportableKeyForOperation(operation: "Create Credential Request") + } + + return try await CreateJWTCredentialRequest.create(didStr: did.string, key: exportableKey, offerData: offerData) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/PrismJWTCredentialPlugin.swift b/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/PrismJWTCredentialPlugin.swift new file mode 100644 index 00000000..29cf92da --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/PrismJWTCredentialPlugin.swift @@ -0,0 +1,38 @@ +import Domain +import Foundation +import JSONWebKey +import JSONWebToken +import JSONWebSignature + +struct PrismJWTCredentialPlugin: CredentialPlugin { + let credentialType = "prismJWT" + var version: String { jwtPlugin.version } + var supportedOperations: [String] { jwtPlugin.supportedOperations } + private let jwtPlugin = JWTCredentialPlugin() + + func createCredential(_ credentialData: Data) async throws -> Credential { + try await jwtPlugin.createCredential(credentialData) + } + + func credential(_ imported: Data) async throws -> Credential { + try await jwtPlugin.credential(imported) + } + + func requiredOptions(operation: String) -> [Domain.CredentialOperationsOptions] { + jwtPlugin.requiredOptions(operation: operation) + } + + func operation( + type: String, + format: String?, + payload: Data?, + options: [Domain.CredentialOperationsOptions] + ) async throws -> Domain.OperationResult { + try await jwtPlugin.operation( + type: type, + format: format, + payload: payload, + options: options + ) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/SDJWTCredentialPlugin.swift b/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/SDJWTCredentialPlugin.swift new file mode 100644 index 00000000..0638604f --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Plugins/DIDCommPlugin/SDJWTCredentialPlugin.swift @@ -0,0 +1,80 @@ +import Domain +import Foundation +import JSONWebKey +import JSONWebToken +import JSONWebSignature + +struct SDJWTCredentialPlugin: CredentialPlugin { + let version: String = "0.1" + var supportedOperations: [String] { + [ + "offer", + "offer-credential", + "issue", + "issue-credential" + ] + } + + let credentialType = "vc+sd-jwt" + + func createCredential(_ credentialData: Data) async throws -> Credential { + try SDJWTCredential(sdjwtString: credentialData.tryToString()) + } + + func credential(_ imported: Data) async throws -> Credential { + try SDJWTCredential(sdjwtString: imported.tryToString()) + } + + func requiredOptions(operation: String) -> [Domain.CredentialOperationsOptions] { + [] + } + + func operation( + type: String, + format: String?, + payload: Data?, + options: [Domain.CredentialOperationsOptions] + ) async throws -> Domain.OperationResult { + guard let payload else { throw PolluxError.invalidJWTCredential } + switch type { + case "offer", "offer-credential": + let processedJWTCredentialRequest = try await processSDJWTCredentialRequest( + offerData: payload, + options: options + ) + return try .forward( + type: "request-credential", + format: format, + payload: processedJWTCredentialRequest.tryToData() + ) + case "issue", "issue-credential": + return try await .credential(createCredential(payload)) + default: + throw PolluxError.unsupportedIssuedMessage + } + } + + private func processSDJWTCredentialRequest(offerData: Data, options: [CredentialOperationsOptions]) async throws -> String { + guard + let subjectDIDOption = options.first(where: { + if case .subjectDID = $0 { return true } + return false + }), + case let CredentialOperationsOptions.subjectDID(did) = subjectDIDOption + else { + throw PolluxError.invalidPrismDID + } + + guard + let exportableKeyOption = options.first(where: { + if case .exportableKey = $0 { return true } + return false + }), + case let CredentialOperationsOptions.exportableKey(exportableKey) = exportableKeyOption + else { + throw PolluxError.requiresExportableKeyForOperation(operation: "Create Credential Request") + } + + return try await CreateJWTCredentialRequest.create(didStr: did.string, key: exportableKey, offerData: offerData) + } +} diff --git a/Package.swift b/Package.swift index 672f92cb..2c6bce10 100644 --- a/Package.swift +++ b/Package.swift @@ -129,6 +129,20 @@ let package = Package( ], path: "EdgeAgentSDK/Pollux/Sources" ), +// .target( +// name: "JWTPlugin", +// dependencies: [ +// "Domain", +// "Core", +// "jose-swift", +//// "Sextant", +//// "eudi-lib-sdjwt-swift", +// .product(name: "Gzip", package: "GzipSwift"), +//// .product(name: "AnoncredsSwift", package: "anoncreds-rs"), +//// .product(name: "JSONSchema", package: "JSONSchema.swift") +// ], +// path: "EdgeAgentSDK/Pollux/Sources" +// ), .testTarget( name: "PolluxTests", dependencies: ["Pollux", "Apollo", "Castor", "EdgeAgent"], @@ -173,6 +187,7 @@ let package = Package( "Domain", "Builders", "Core", + "jose-swift", .product(name: "OpenID4VCI", package: "eudi-lib-ios-openid4vci-swift") ], path: "EdgeAgentSDK/EdgeAgent/Sources" diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj index ae9f1207..656f1f17 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj @@ -115,6 +115,11 @@ EEBC938F29C7311C0015A36E /* CredentialListViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBC938E29C7311C0015A36E /* CredentialListViewState.swift */; }; EEBC939529C735910015A36E /* CredentialListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBC939429C735910015A36E /* CredentialListViewModel.swift */; }; EEBC939729C737DE0015A36E /* CredentialListRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEBC939629C737DE0015A36E /* CredentialListRouter.swift */; }; + EEC2226A2CF77437004F2CA5 /* Apollo in Frameworks */ = {isa = PBXBuildFile; productRef = EEC222692CF77437004F2CA5 /* Apollo */; }; + EEC2226C2CF77437004F2CA5 /* Authenticate in Frameworks */ = {isa = PBXBuildFile; productRef = EEC2226B2CF77437004F2CA5 /* Authenticate */; }; + EEC2226E2CF77437004F2CA5 /* Builders in Frameworks */ = {isa = PBXBuildFile; productRef = EEC2226D2CF77437004F2CA5 /* Builders */; }; + EEC222702CF77437004F2CA5 /* Castor in Frameworks */ = {isa = PBXBuildFile; productRef = EEC2226F2CF77437004F2CA5 /* Castor */; }; + EEC222722CF77437004F2CA5 /* Domain in Frameworks */ = {isa = PBXBuildFile; productRef = EEC222712CF77437004F2CA5 /* Domain */; }; EEC5A8392BEA5E3A00AED928 /* EdgeAgent in Frameworks */ = {isa = PBXBuildFile; productRef = EEC5A8382BEA5E3A00AED928 /* EdgeAgent */; }; EEE61FBA2937CA280053AE52 /* AtalaPrismWalletDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE61FB92937CA280053AE52 /* AtalaPrismWalletDemoApp.swift */; }; EEE61FBC2937CA280053AE52 /* FuncionalitiesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEE61FBB2937CA280053AE52 /* FuncionalitiesList.swift */; }; @@ -282,11 +287,16 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EEC222702CF77437004F2CA5 /* Castor in Frameworks */, EE38135A2938D5B100A3A710 /* Mercury in Frameworks */, EE3813582938D5B100A3A710 /* Domain in Frameworks */, EE38135E2938D5B100A3A710 /* Pollux in Frameworks */, EEC5A8392BEA5E3A00AED928 /* EdgeAgent in Frameworks */, EE38135C2938D5B100A3A710 /* Pluto in Frameworks */, + EEC2226C2CF77437004F2CA5 /* Authenticate in Frameworks */, + EEC2226E2CF77437004F2CA5 /* Builders in Frameworks */, + EEC222722CF77437004F2CA5 /* Domain in Frameworks */, + EEC2226A2CF77437004F2CA5 /* Apollo in Frameworks */, EE3813542938D5B100A3A710 /* Builders in Frameworks */, EE3813502938D5B100A3A710 /* Apollo in Frameworks */, EE3813562938D5B100A3A710 /* Castor in Frameworks */, @@ -842,6 +852,11 @@ EE38135B2938D5B100A3A710 /* Pluto */, EE38135D2938D5B100A3A710 /* Pollux */, EEC5A8382BEA5E3A00AED928 /* EdgeAgent */, + EEC222692CF77437004F2CA5 /* Apollo */, + EEC2226B2CF77437004F2CA5 /* Authenticate */, + EEC2226D2CF77437004F2CA5 /* Builders */, + EEC2226F2CF77437004F2CA5 /* Castor */, + EEC222712CF77437004F2CA5 /* Domain */, ); productName = AtalaPrismWalletDemo; productReference = EEE61FB62937CA280053AE52 /* AtalaPrismWalletDemo.app */; @@ -872,7 +887,7 @@ ); mainGroup = EEE61FAD2937CA280053AE52; packageReferences = ( - EE86C3C82CF5EC750072BEB7 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */, + EEC222682CF77437004F2CA5 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */, ); productRefGroup = EEE61FB72937CA280053AE52 /* Products */; projectDirPath = ""; @@ -1235,12 +1250,12 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - EE86C3C82CF5EC750072BEB7 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */ = { + EEC222682CF77437004F2CA5 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/hyperledger/identus-edge-agent-sdk-swift"; requirement = { - kind = exactVersion; - version = 7.0.0; + kind = upToNextMajorVersion; + minimumVersion = 7.0.1; }; }; /* End XCRemoteSwiftPackageReference section */ @@ -1278,6 +1293,31 @@ isa = XCSwiftPackageProductDependency; productName = Pollux; }; + EEC222692CF77437004F2CA5 /* Apollo */ = { + isa = XCSwiftPackageProductDependency; + package = EEC222682CF77437004F2CA5 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */; + productName = Apollo; + }; + EEC2226B2CF77437004F2CA5 /* Authenticate */ = { + isa = XCSwiftPackageProductDependency; + package = EEC222682CF77437004F2CA5 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */; + productName = Authenticate; + }; + EEC2226D2CF77437004F2CA5 /* Builders */ = { + isa = XCSwiftPackageProductDependency; + package = EEC222682CF77437004F2CA5 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */; + productName = Builders; + }; + EEC2226F2CF77437004F2CA5 /* Castor */ = { + isa = XCSwiftPackageProductDependency; + package = EEC222682CF77437004F2CA5 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */; + productName = Castor; + }; + EEC222712CF77437004F2CA5 /* Domain */ = { + isa = XCSwiftPackageProductDependency; + package = EEC222682CF77437004F2CA5 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */; + productName = Domain; + }; EEC5A8382BEA5E3A00AED928 /* EdgeAgent */ = { isa = XCSwiftPackageProductDependency; productName = EdgeAgent;