From 9d24490e91da8b5026c133e70ec271d9c1a0821e Mon Sep 17 00:00:00 2001 From: goncalo-frade-iohk Date: Thu, 20 Feb 2025 10:50:34 +0000 Subject: [PATCH] feat(castor): now castor and agents can create a did with any keys Signed-off-by: goncalo-frade-iohk --- .../Castor/Sources/CastorImpl+Public.swift | 30 +++++- .../DID/PrismDID/PrismDIDPublicKey.swift | 42 +++++--- .../Operations/CreatePeerDIDOperation.swift | 15 ++- .../Operations/CreatePrismDIDOperation.swift | 59 +++++++---- .../Resolvers/LongFormPrismDIDResolver.swift | 47 +++++---- EdgeAgentSDK/Domain/Sources/BBs/Castor.swift | 22 +++++ EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift | 1 + .../Domain/Sources/Models/Errors.swift | 7 ++ .../DIDCommAgent+Credentials.swift | 24 ++--- .../DIDCommAgent/DIDCommAgent+DIDs.swift | 2 + .../Sources/EdgeAgent+Credentials.swift | 8 +- .../EdgeAgent+DIDHigherFucntions.swift | 97 ++++++++++++++++--- .../Sources/OIDCAgent/OIDCAgent+DIDs.swift | 1 + .../EdgeAgent/Tests/BackupWalletTests.swift | 4 +- EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift | 2 + .../Tests/Helper/Castor+Testing.swift | 5 + .../SDJWT/CreateSDJWTCrendentialRequest.swift | 59 +++++++++++ .../PolluxImpl+CredentialRequest.swift | 17 ++-- .../Pollux/Tests/Mocks/MockCastor.swift | 5 + .../project.pbxproj | 88 ++++++++++++++--- .../CredentialListViewModel.swift | 8 +- .../MediatorPage/MediatorPageView.swift | 2 +- 22 files changed, 435 insertions(+), 110 deletions(-) create mode 100644 EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/CreateSDJWTCrendentialRequest.swift diff --git a/EdgeAgentSDK/Castor/Sources/CastorImpl+Public.swift b/EdgeAgentSDK/Castor/Sources/CastorImpl+Public.swift index f554cccb..d9ad5960 100644 --- a/EdgeAgentSDK/Castor/Sources/CastorImpl+Public.swift +++ b/EdgeAgentSDK/Castor/Sources/CastorImpl+Public.swift @@ -29,6 +29,28 @@ extension CastorImpl: Castor { // ).compute() // } + public func createDID( + method: DIDMethod, + keys: [(KeyPurpose, any PublicKey)], + services: [DIDDocument.Service] + ) throws -> DID { + switch method { + case "prism": + return try CreatePrismDIDOperation( + apollo: apollo, + keys: keys, + services: services + ).compute() + case "peer": + return try CreatePeerDIDOperation( + keys: keys, + services: services + ).compute() + default: + throw CastorError.noResolversAvailableForDIDMethod(method: method) + } + } + /// createPrismDID creates a DID for a prism (a device or server that acts as a DID owner and controller) using a given master public key and list of services. This function may throw an error if the master public key or services are invalid. /// /// - Parameters: @@ -42,7 +64,7 @@ extension CastorImpl: Castor { ) throws -> DID { try CreatePrismDIDOperation( apollo: apollo, - masterPublicKey: masterPublicKey, + keys: [(KeyPurpose.master, masterPublicKey)], services: services ).compute() } @@ -61,8 +83,10 @@ extension CastorImpl: Castor { services: [DIDDocument.Service] ) throws -> DID { try CreatePeerDIDOperation( - autenticationPublicKey: authenticationPublicKey, - agreementPublicKey: keyAgreementPublicKey, + keys: [ + (KeyPurpose.authentication, authenticationPublicKey), + (KeyPurpose.agreement, keyAgreementPublicKey) + ], services: services ).compute() } diff --git a/EdgeAgentSDK/Castor/Sources/DID/PrismDID/PrismDIDPublicKey.swift b/EdgeAgentSDK/Castor/Sources/DID/PrismDID/PrismDIDPublicKey.swift index 11aa4218..1dafd48a 100644 --- a/EdgeAgentSDK/Castor/Sources/DID/PrismDID/PrismDIDPublicKey.swift +++ b/EdgeAgentSDK/Castor/Sources/DID/PrismDID/PrismDIDPublicKey.swift @@ -44,15 +44,15 @@ struct PrismDIDPublicKey { case .issuingKey: return "issuing\(index)" case .capabilityDelegationKey: - return "capabilityDelegationKey\(index)" + return "capability-delegationKey\(index)" case .capabilityInvocationKey: - return "capabilityInvocationKey\(index)" + return "capability-invocationKey\(index)" case .authenticationKey: return "authentication\(index)" case .revocationKey: return "revocation\(index)" case .keyAgreementKey: - return "keyAgreement\(index)" + return "key-agreement\(index)" case .unknownKey: return "unknown\(index)" } @@ -102,22 +102,34 @@ struct PrismDIDPublicKey { var protoKey = Io_Iohk_Atala_Prism_Protos_PublicKey() protoKey.id = id protoKey.usage = usage.toProto() - guard - let pointXStr = keyData.getProperty(.curvePointX), - let pointYStr = keyData.getProperty(.curvePointY), - let pointX = Data(base64URLEncoded: pointXStr), - let pointY = Data(base64URLEncoded: pointYStr) - else { + switch curve { + case "Ed25519", "X25519": + var protoEC = Io_Iohk_Atala_Prism_Protos_CompressedECKeyData() + protoEC.data = keyData.raw + protoEC.curve = curve + protoKey.keyData = .compressedEcKeyData(protoEC) + case "secp256k1": + guard + let pointXStr = keyData.getProperty(.curvePointX), + let pointYStr = keyData.getProperty(.curvePointY), + let pointX = Data(base64URLEncoded: pointXStr), + let pointY = Data(base64URLEncoded: pointYStr) + else { + throw ApolloError.missingKeyParameters(missing: [ + KeyProperties.curvePointX.rawValue, + KeyProperties.curvePointY.rawValue + ]) + } + var protoEC = Io_Iohk_Atala_Prism_Protos_ECKeyData() + protoEC.x = pointX + protoEC.y = pointY + protoEC.curve = curve + protoKey.keyData = .ecKeyData(protoEC) + default: throw ApolloError.missingKeyParameters(missing: [ - KeyProperties.curvePointX.rawValue, KeyProperties.curvePointY.rawValue ]) } - var protoEC = Io_Iohk_Atala_Prism_Protos_ECKeyData() - protoEC.x = pointX - protoEC.y = pointY - protoEC.curve = curve - protoKey.keyData = .ecKeyData(protoEC) return protoKey } } diff --git a/EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift b/EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift index 2c28285f..6057bece 100644 --- a/EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift +++ b/EdgeAgentSDK/Castor/Sources/Operations/CreatePeerDIDOperation.swift @@ -7,14 +7,21 @@ import PeerDID struct CreatePeerDIDOperation { private let method: DIDMethod = "peer" - let autenticationPublicKey: PublicKey - let agreementPublicKey: PublicKey + let keys: [(KeyPurpose, PublicKey)] let services: [Domain.DIDDocument.Service] func compute() throws -> Domain.DID { + let authenticationKeys = try keys + .filter { $0.0 == .authentication } + .map(\.1) + .map(authenticationFromPublicKey(publicKey:)) + let agreementKeys = try keys + .filter { $0.0 == .agreement } + .map(\.1) + .map(keyAgreementFromPublicKey(publicKey:)) let did = try PeerDIDHelper.createAlgo2( - authenticationKeys: [authenticationFromPublicKey(publicKey: autenticationPublicKey)], - agreementKeys: [keyAgreementFromPublicKey(publicKey: agreementPublicKey)], + authenticationKeys: authenticationKeys, + agreementKeys: agreementKeys, services: services.flatMap { service in service.serviceEndpoint.map { AnyCodable(dictionaryLiteral: diff --git a/EdgeAgentSDK/Castor/Sources/Operations/CreatePrismDIDOperation.swift b/EdgeAgentSDK/Castor/Sources/Operations/CreatePrismDIDOperation.swift index 2255c1f4..c9aee29f 100644 --- a/EdgeAgentSDK/Castor/Sources/Operations/CreatePrismDIDOperation.swift +++ b/EdgeAgentSDK/Castor/Sources/Operations/CreatePrismDIDOperation.swift @@ -5,29 +5,33 @@ import Foundation struct CreatePrismDIDOperation { private let method: DIDMethod = "prism" let apollo: Apollo - let masterPublicKey: PublicKey + let keys: [(KeyPurpose, PublicKey)] let services: [DIDDocument.Service] func compute() throws -> DID { var operation = Io_Iohk_Atala_Prism_Protos_AtalaOperation() - guard let masterKeyCurve = masterPublicKey.getProperty(.curve) else { - throw CastorError.invalidPublicKeyCoding(didMethod: "prism", curve: "no curve") + guard keys.count(where: { $0.0 == .master} ) == 1 else { + throw CastorError.requiresOneAndJustOneMasterKey } + let groupByPurpose = Dictionary(grouping: keys, by: { $0.0 }) operation.createDid = try createDIDAtalaOperation( - publicKeys: [PrismDIDPublicKey( - apollo: apollo, - id: PrismDIDPublicKey.Usage.authenticationKey.defaultId, - curve: masterKeyCurve, - usage: .authenticationKey, - keyData: masterPublicKey - ), - PrismDIDPublicKey( - apollo: apollo, - id: PrismDIDPublicKey.Usage.masterKey.defaultId, - curve: masterKeyCurve, - usage: .masterKey, - keyData: masterPublicKey - )], + publicKeys: groupByPurpose.flatMap { (key, value) in + try value + .sorted(by: { $0.1.identifier < $1.1.identifier } ) + .enumerated() + .map { + guard let curve = $0.element.1.getProperty(.curve) else { + throw CastorError.invalidPublicKeyCoding(didMethod: "prism", curve: "no curve") + } + return PrismDIDPublicKey( + apollo: apollo, + id: key.toPrismDIDKeyPurpose().id(index: $0.offset), + curve: curve, + usage: key.toPrismDIDKeyPurpose(), + keyData: $0.element.1 + ) + } + }, services: services ) return try createLongFormFromOperation(method: method, atalaOperation: operation) @@ -68,3 +72,24 @@ struct CreatePrismDIDOperation { return DID(method: method, methodId: methodSpecificId.description) } } + +extension KeyPurpose { + func toPrismDIDKeyPurpose() -> PrismDIDPublicKey.Usage { + switch self { + case .master: + return .masterKey + case .issue: + return .issuingKey + case .authentication: + return .authenticationKey + case .capabilityDelegation: + return .capabilityDelegationKey + case .capabilityInvocation: + return .capabilityInvocationKey + case .agreement: + return .keyAgreementKey + case .revocation: + return .revocationKey + } + } +} diff --git a/EdgeAgentSDK/Castor/Sources/Resolvers/LongFormPrismDIDResolver.swift b/EdgeAgentSDK/Castor/Sources/Resolvers/LongFormPrismDIDResolver.swift index 3e04a07b..8d3e9a5d 100644 --- a/EdgeAgentSDK/Castor/Sources/Resolvers/LongFormPrismDIDResolver.swift +++ b/EdgeAgentSDK/Castor/Sources/Resolvers/LongFormPrismDIDResolver.swift @@ -112,25 +112,34 @@ struct LongFormPrismDIDResolver: DIDResolverDomain { serviceEndpoint: $0.serviceEndpoint.map { .init(uri: $0) } ) } - - let decodedPublicKeys = publicKeys.enumerated().map { - let didUrl = DIDUrl( - did: did, - fragment: $0.element.usage.id(index: $0.offset - 1) - ) - - let method = DIDDocument.VerificationMethod( - id: didUrl, - controller: did, - type: $0.element.keyData.getProperty(.curve) ?? "", - publicKeyMultibase: $0.element.keyData.raw.base64EncodedString() - ) - - return PublicKeyDecoded( - id: didUrl.string, - keyType: .init(usage: $0.element.usage), - method: method - ) + let groupByPurpose = Dictionary( + // Per specification master keys and revocation keys should not be in the did document + grouping: publicKeys.filter { $0.usage != .masterKey && $0.usage != .revocationKey }, + by: { $0.usage } + ) + let decodedPublicKeys = groupByPurpose.flatMap { (key, value) in + value + .sorted(by: { $0.id < $1.id } ) + .enumerated() + .map { + let didUrl = DIDUrl( + did: did, + fragment: $0.element.usage.id(index: $0.offset) + ) + + let method = DIDDocument.VerificationMethod( + id: didUrl, + controller: did, + type: $0.element.keyData.getProperty(.curve) ?? "", + publicKeyMultibase: $0.element.keyData.raw.base64EncodedString() + ) + + return PublicKeyDecoded( + id: didUrl.string, + keyType: .init(usage: $0.element.usage), + method: method + ) + } } return (decodedPublicKeys, services) diff --git a/EdgeAgentSDK/Domain/Sources/BBs/Castor.swift b/EdgeAgentSDK/Domain/Sources/BBs/Castor.swift index 26e5bad7..4f636edf 100644 --- a/EdgeAgentSDK/Domain/Sources/BBs/Castor.swift +++ b/EdgeAgentSDK/Domain/Sources/BBs/Castor.swift @@ -1,5 +1,15 @@ import Foundation +public enum KeyPurpose: String, Hashable, Equatable, CaseIterable { + case master + case issue + case capabilityDelegation + case capabilityInvocation + case authentication + case revocation + case agreement +} + /// The Castor protocol defines the set of decentralized identifier (DID) operations that are used in the Atala PRISM architecture. It provides a way for users to create, manage, and control their DIDs and associated cryptographic keys. public protocol Castor { /// parseDID parses a string representation of a Decentralized Identifier (DID) into a DID object. This function may throw an error if the string is not a valid DID. @@ -8,6 +18,18 @@ public protocol Castor { /// - Returns: The DID object /// - Throws: An error if the string is not a valid DID func parseDID(str: String) throws -> DID + + /// createDID creates a DID for a method using a given an array of public keys and list of services. This function may throw an error. + /// - Parameters: + /// - method: DID Method to use (ex: prism, peer) + /// - keys: An array of Tuples with the public key and the key purpose + /// - services: The list of services + /// - Returns: The created DID + func createDID( + method: DIDMethod, + keys: [(KeyPurpose, PublicKey)], + services: [DIDDocument.Service] + ) throws -> DID /// createPrismDID creates a DID for a prism (a device or server that acts as a DID owner and controller) using a given master public key and list of services. This function may throw an error if the master public key or services are invalid. /// diff --git a/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift b/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift index 0c4f5827..75d2d6a9 100644 --- a/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift +++ b/EdgeAgentSDK/Domain/Sources/BBs/Pollux.swift @@ -12,6 +12,7 @@ public enum CredentialOperationsOptions { case entropy(String) // Entropy for any randomization operation. case signableKey(SignableKey) // A key that can be used for signing. case exportableKey(ExportableKey) // A key that can be exported. + case exportableKeys([ExportableKey]) // A key that can be exported. case zkpPresentationParams(attributes: [String: Bool], predicates: [String]) // Anoncreds zero-knowledge proof presentation parameters case disclosingClaims(claims: [String]) case thid(String) diff --git a/EdgeAgentSDK/Domain/Sources/Models/Errors.swift b/EdgeAgentSDK/Domain/Sources/Models/Errors.swift index 5ebfb531..e3fe93a6 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/Errors.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/Errors.swift @@ -377,6 +377,9 @@ public enum CastorError: KnownPrismError { /// An error case representing inability to retrieve the public key from a document. case cannotRetrievePublicKeyFromDocument + /// An error case representing that a master key was not provided or that it had more than one + case requiresOneAndJustOneMasterKey + /// The error code returned by the server. public var code: Int { switch self { @@ -400,6 +403,8 @@ public enum CastorError: KnownPrismError { return 29 case .cannotRetrievePublicKeyFromDocument: return 30 + case .requiresOneAndJustOneMasterKey: + return 31 } } @@ -432,6 +437,8 @@ public enum CastorError: KnownPrismError { return "No resolvers in castor are able to resolve the method \(method), please provide a resolver" case .cannotRetrievePublicKeyFromDocument: return "The public keys in the DIDDocument are not in multibase or the multibase is invalid" + case .requiresOneAndJustOneMasterKey: + return "The array contains none or more than one master key" } } } diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift index e792a442..6d285d17 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+Credentials.swift @@ -180,12 +180,20 @@ public extension DIDCommAgent { .first() .await() - guard let storedPrivateKey = didInfo?.privateKeys.first else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + let downloader = DownloadDataWithResolver(castor: castor) + guard + let attachment = offer.attachments.first, + let offerFormat = attachment.format + else { + throw PolluxError.unsupportedIssuedMessage + } + + guard let storedPrivateKey = didInfo?.privateKeys else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } - let privateKey = try await apollo.restorePrivateKey(storedPrivateKey) + let privateKeys = try await storedPrivateKey.asyncMap { try await apollo.restorePrivateKey($0) } + let exporting = privateKeys.compactMap(\.exporting) guard - let exporting = privateKey.exporting, let linkSecret = try await pluto.getLinkSecret().first().await() else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } @@ -194,14 +202,6 @@ public extension DIDCommAgent { let linkSecretString = String(data: restored.raw, encoding: .utf8) else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } - let downloader = DownloadDataWithResolver(castor: castor) - guard - let attachment = offer.attachments.first, - let offerFormat = attachment.format - else { - throw PolluxError.unsupportedIssuedMessage - } - let jsonData: Data switch attachment.data { case let attchedData as AttachmentBase64: @@ -218,7 +218,7 @@ public extension DIDCommAgent { type: offerFormat, offerPayload: jsonData, options: [ - .exportableKey(exporting), + .exportableKeys(exporting), .subjectDID(did), .linkSecret(id: did.string, secret: linkSecretString), .credentialDefinitionDownloader(downloader: downloader), diff --git a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift index fb0a8a0e..80b80184 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/DIDCommAgent/DIDCommAgent+DIDs.swift @@ -12,11 +12,13 @@ public extension DIDCommAgent { /// - services: an array of services associated to the DID /// - Returns: The new created DID func createNewPrismDID( + keys: [(KeyPurpose, PrivateKey)] = [], keyPathIndex: Int? = nil, alias: String? = nil, services: [DIDDocument.Service] = [] ) async throws -> DID { try await edgeAgent.createNewPrismDID( + keys: keys, keyPathIndex: keyPathIndex, alias: alias, services: services diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift index d73fe609..e8573902 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Credentials.swift @@ -136,12 +136,12 @@ public extension EdgeAgent { .first() .await() - guard let storedPrivateKey = didInfo?.privateKeys.first else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } + guard let storedPrivateKey = didInfo?.privateKeys else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } - let privateKey = try await apollo.restorePrivateKey(storedPrivateKey) + let privateKeys = try await storedPrivateKey.asyncMap { try await apollo.restorePrivateKey($0) } + let exporting = privateKeys.compactMap(\.exporting) guard - let exporting = privateKey.exporting, let linkSecret = try await pluto.getLinkSecret().first().await() else { throw EdgeAgentError.cannotFindDIDKeyPairIndex } @@ -155,7 +155,7 @@ public extension EdgeAgent { type: type, offerPayload: offerPayload, options: [ - .exportableKey(exporting), + .exportableKeys(exporting), .subjectDID(did), .linkSecret(id: did.string, secret: linkSecretString), .credentialDefinitionDownloader(downloader: downloader), diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+DIDHigherFucntions.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+DIDHigherFucntions.swift index ede7a3bf..79d5118c 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+DIDHigherFucntions.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+DIDHigherFucntions.swift @@ -64,17 +64,33 @@ Could not find key in storage please use Castor instead and provide the private keyPathIndex: Int? = nil, alias: String? = nil, services: [DIDDocument.Service] = [] + ) async throws -> DID { + return try await createNewPrismDID( + keys: masterPrivateKey.map { [(KeyPurpose.master, $0)] } ?? [], + keyPathIndex: keyPathIndex, + alias: alias, + services: services + ) + } + + /// This method create a new Prism DID, that can be used to identify the agent and interact with other agents. + /// - Parameters: + /// - keyPathIndex: key path index used to identify the DID + /// - alias: An alias that can be used to identify the DID + /// - services: an array of services associated to the DID + /// - Returns: The new created DID + func createNewPrismDID( + keys: [(KeyPurpose, PrivateKey)] = [], + keyPathIndex: Int? = nil, + alias: String? = nil, + services: [DIDDocument.Service] = [] ) async throws -> DID { let seed = self.seed let apollo = self.apollo let castor = self.castor + var usingKeys = keys - var usingPrivateKey: PrivateKey - - if let masterPrivateKey { - usingPrivateKey = masterPrivateKey - } - else { + if keys.first(where: { $0.0 == .master })?.1 == nil { let lastKeyPairIndex = try await pluto .getPrismLastKeyPairIndex() .first() @@ -83,7 +99,7 @@ Could not find key in storage please use Castor instead and provide the private // If the user provided a key path index use it, if not use the last + 1 let index = keyPathIndex ?? (lastKeyPairIndex + 1) // Create the key pair - usingPrivateKey = try apollo.createPrivateKey(parameters: [ + let usingPrivateKey = try apollo.createPrivateKey(parameters: [ KeyProperties.type.rawValue: "EC", KeyProperties.seed.rawValue: seed.value.base64Encoded(), KeyProperties.curve.rawValue: KnownKeyCurves.secp256k1.rawValue, @@ -92,20 +108,47 @@ Could not find key in storage please use Castor instead and provide the private keyIndex: index ).derivationPath.keyPathString() ]) + usingKeys.append((.master, usingPrivateKey)) + } + let groupedKeys = Dictionary(grouping: usingKeys, by: { $0.0 }) + let finalKeys = groupedKeys.flatMap { (key, value) in + value.enumerated().map { + var privateKey = $0.element.1 + var publicKey = $0.element.1.publicKey() + let identifier = "#\(key.rawValue)\($0.offset)" + privateKey.identifier = identifier + publicKey.identifier = identifier + return (key, privateKey, publicKey) + } } - var publicKey = usingPrivateKey.publicKey() + let newDID = try castor.createDID( + method: "prism", + keys: finalKeys.map { + ($0, $2) + }, + services: services) + + let finalKeysAfterDid = groupedKeys.flatMap { (key, value) in + value.enumerated().map { + var privateKey = $0.element.1 + var publicKey = $0.element.1.publicKey() + let identifier = DIDUrl(did: newDID, fragment: "\(key.rawValue)\($0.offset)").string + privateKey.identifier = identifier + publicKey.identifier = identifier + return (key, privateKey, publicKey) + } + } - let newDID = try castor.createPrismDID(masterPublicKey: publicKey, services: services) - let kid = DIDUrl(did: newDID, fragment: "#authentication0").string - usingPrivateKey.identifier = kid - publicKey.identifier = kid logger.debug(message: "Created new Prism DID", metadata: [ .maskedMetadataByLevel(key: "DID", value: newDID.string, level: .debug), .maskedMetadataByLevel(key: "keyPathIndex", value: "\(index)", level: .debug) ]) - try await registerPrismDID(did: newDID, privateKey: usingPrivateKey, alias: alias) + try await registerPrismDID( + did: newDID, + keys: finalKeysAfterDid.map(\.1), + alias: alias) return newDID } @@ -137,6 +180,34 @@ Could not find key in storage please use Castor instead and provide the private .await() } + /// This method registers a Prism DID, that can be used to identify the agent and interact with other agents. + /// - Parameters: + /// - did: the DID which will be registered. + /// - keyPathIndex: key path index used to identify the DID + /// - alias: An alias that can be used to identify the DID + /// - Returns: The new created DID + func registerPrismDID( + did: DID, + keys: [PrivateKey], + alias: String? = nil + ) async throws { + logger.debug(message: "Register of DID in storage", metadata: [ + .maskedMetadataByLevel(key: "DID", value: did.string, level: .debug) + ]) + + let storablePrivateKeys = try keys + .map { + guard let storablePrivateKey = $0 as? (PrivateKey & StorableKey) else { + throw KeyError.keyRequiresConformation(conformations: ["PrivateKey", "StorableKey"]) + } + return storablePrivateKey + } + try await pluto + .storeDID(did: did, privateKeys: storablePrivateKeys, alias: alias) + .first() + .await() + } + /// This function gets the DID information (alias) for a given DID /// /// - Parameter did: The DID for which the information is requested diff --git a/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent+DIDs.swift b/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent+DIDs.swift index c8f693ac..20589d97 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent+DIDs.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/OIDCAgent/OIDCAgent+DIDs.swift @@ -17,6 +17,7 @@ public extension OIDCAgent { services: [DIDDocument.Service] = [] ) async throws -> DID { try await edgeAgent.createNewPrismDID( + keys: [], keyPathIndex: keyPathIndex, alias: alias, services: services diff --git a/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift b/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift index bc74b39b..c8ba1054 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/BackupWalletTests.swift @@ -45,8 +45,8 @@ final class BackupWalletTests: XCTestCase { func testBackup() async throws { let (backupAgent, backupPluto) = try createAgent() - _ = try await backupAgent.createNewPrismDID() - _ = try await backupAgent.createNewPrismDID() + _ = try await backupAgent.createNewPrismDID(masterPrivateKey: nil) + _ = try await backupAgent.createNewPrismDID(masterPrivateKey: nil) backupPluto.didPairs = [ .init( diff --git a/EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift b/EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift index 838a3b44..baa58695 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/CheckTest.swift @@ -4,6 +4,8 @@ import Castor import Builders import CryptoKit import Core +import Pluto +import Pollux @testable import EdgeAgent import XCTest diff --git a/EdgeAgentSDK/EdgeAgent/Tests/Helper/Castor+Testing.swift b/EdgeAgentSDK/EdgeAgent/Tests/Helper/Castor+Testing.swift index 23d47105..ac87f3d8 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/Helper/Castor+Testing.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/Helper/Castor+Testing.swift @@ -16,6 +16,11 @@ struct CastorStub: Castor { return parseDIDResponse } + func createDID(method: DIDMethod, keys: [(KeyPurpose, any PublicKey)], services: [DIDDocument.Service]) throws -> DID { + guard throwCreateDIDError == nil else { throw throwCreateDIDError! } + return createDIDResponse + } + func createPrismDID(masterPublicKey: PublicKey, services: [DIDDocument.Service]) throws -> DID { guard throwCreateDIDError == nil else { throw throwCreateDIDError! } return createDIDResponse diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/CreateSDJWTCrendentialRequest.swift b/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/CreateSDJWTCrendentialRequest.swift new file mode 100644 index 00000000..4165b879 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/CreateSDJWTCrendentialRequest.swift @@ -0,0 +1,59 @@ +import Combine +import Domain +import Foundation +import JSONWebAlgorithms +import JSONWebKey +import JSONWebToken +import JSONWebSignature + +private struct Schema: Codable { + let name: String + let version: String + let attrNames: [String] + let issuerId: String +} + +struct CreateSDJWTCredentialRequest { + static func create(didStr: String, keys: [ExportableKey], offerData: Data) async throws -> String { + let jsonObject = try JSONSerialization.jsonObject(with: offerData) + guard + let domain = findValue(forKey: "domain", in: jsonObject), + let challenge = findValue(forKey: "challenge", in: jsonObject), + let key = keys.filter({ $0.jwk.crv?.lowercased() == "ed25519" }).first + else { throw PolluxError.offerDoesntProvideEnoughInformation } + + let keyJWK = key.jwk + let claims = ClaimsRequestSignatureJWT( + iss: didStr, + sub: nil, + aud: [domain], + exp: nil, + nbf: nil, + iat: nil, + jti: nil, + nonce: challenge, + vp: .init(context: .init([ + "https://www.w3.org/2018/presentations/v1" + ]), type: .init([ + "VerifiablePresentation" + ])) + ) + + guard let kty = JWK.KeyType(rawValue: keyJWK.kty) else { throw PolluxError.invalidPrismDID } + let jwt = try JWT.signed( + payload: claims, + protectedHeader: DefaultJWSHeaderImpl( + algorithm: .EdDSA, + keyID: keyJWK.kid + ), + key: JSONWebKey.JWK( + keyType: kty, + keyID: keyJWK.kid, + x: keyJWK.x.flatMap { Data(fromBase64URL: $0) }, + d: keyJWK.d.flatMap { Data(fromBase64URL: $0) } + ) + ) + + return jwt.jwtString + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift index 57434b41..3c76973f 100644 --- a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift +++ b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift @@ -111,18 +111,19 @@ extension PolluxImpl { else { throw PolluxError.invalidPrismDID } - + guard let exportableKeyOption = options.first(where: { - if case .exportableKey = $0 { return true } + if case .exportableKeys = $0 { return true } return false }), - case let CredentialOperationsOptions.exportableKey(exportableKey) = exportableKeyOption + case let CredentialOperationsOptions.exportableKeys(exportableKeys) = exportableKeyOption, + let exportableFirstKey = exportableKeys.filter({ $0.jwk.crv?.lowercased() == "secp256k1" }).first else { throw PolluxError.requiresExportableKeyForOperation(operation: "Create Credential Request") } - - return try await CreateJWTCredentialRequest.create(didStr: did.string, key: exportableKey, offerData: offerData) + + return try await CreateJWTCredentialRequest.create(didStr: did.string, key: exportableFirstKey, offerData: offerData) } private func processSDJWTCredentialRequest(offerData: Data, options: [CredentialOperationsOptions]) async throws -> String { @@ -138,15 +139,15 @@ extension PolluxImpl { guard let exportableKeyOption = options.first(where: { - if case .exportableKey = $0 { return true } + if case .exportableKeys = $0 { return true } return false }), - case let CredentialOperationsOptions.exportableKey(exportableKey) = exportableKeyOption + case let CredentialOperationsOptions.exportableKeys(exportableKeys) = exportableKeyOption else { throw PolluxError.requiresExportableKeyForOperation(operation: "Create Credential Request") } - return try await CreateJWTCredentialRequest.create(didStr: did.string, key: exportableKey, offerData: offerData) + return try await CreateSDJWTCredentialRequest.create(didStr: did.string, keys: exportableKeys, offerData: offerData) } private func processAnoncredsCredentialRequest( diff --git a/EdgeAgentSDK/Pollux/Tests/Mocks/MockCastor.swift b/EdgeAgentSDK/Pollux/Tests/Mocks/MockCastor.swift index 8e36257a..48b7b8f2 100644 --- a/EdgeAgentSDK/Pollux/Tests/Mocks/MockCastor.swift +++ b/EdgeAgentSDK/Pollux/Tests/Mocks/MockCastor.swift @@ -16,6 +16,11 @@ struct MockCastor: Castor { return parseDIDResponse } + func createDID(method: DIDMethod, keys: [(KeyPurpose, any PublicKey)], services: [DIDDocument.Service]) throws -> DID { + guard throwCreateDIDError == nil else { throw throwCreateDIDError! } + return createDIDResponse + } + func createPrismDID(masterPublicKey: PublicKey, services: [DIDDocument.Service]) throws -> DID { guard throwCreateDIDError == nil else { throw throwCreateDIDError! } return createDIDResponse diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj index ae9f1207..9a7870f8 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -11,6 +11,16 @@ EE0E1FB229473B10003CD7D5 /* SteradianBold.otf in Resources */ = {isa = PBXBuildFile; fileRef = EE0E1FAE29473B10003CD7D5 /* SteradianBold.otf */; }; EE0E1FB329473B10003CD7D5 /* SteradianMedium.otf in Resources */ = {isa = PBXBuildFile; fileRef = EE0E1FAF29473B10003CD7D5 /* SteradianMedium.otf */; }; EE0E1FB429473B10003CD7D5 /* SteradianRegular.otf in Resources */ = {isa = PBXBuildFile; fileRef = EE0E1FB029473B10003CD7D5 /* SteradianRegular.otf */; }; + EE26FD972D720C1A00292E6C /* Apollo in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FD962D720C1A00292E6C /* Apollo */; }; + EE26FD992D720C1A00292E6C /* Authenticate in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FD982D720C1A00292E6C /* Authenticate */; }; + EE26FD9B2D720C1A00292E6C /* Builders in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FD9A2D720C1A00292E6C /* Builders */; }; + EE26FD9D2D720C1A00292E6C /* Castor in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FD9C2D720C1A00292E6C /* Castor */; }; + EE26FD9F2D720C1A00292E6C /* Domain in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FD9E2D720C1A00292E6C /* Domain */; }; + EE26FDA12D720C1A00292E6C /* EdgeAgent in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FDA02D720C1A00292E6C /* EdgeAgent */; }; + EE26FDA32D720C1A00292E6C /* EdgeAgentSDK in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FDA22D720C1A00292E6C /* EdgeAgentSDK */; }; + EE26FDA52D720C1A00292E6C /* Mercury in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FDA42D720C1A00292E6C /* Mercury */; }; + EE26FDA72D720C1A00292E6C /* Pluto in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FDA62D720C1A00292E6C /* Pluto */; }; + EE26FDA92D720C1A00292E6C /* Pollux in Frameworks */ = {isa = PBXBuildFile; productRef = EE26FDA82D720C1A00292E6C /* Pollux */; }; EE2D40972ACC470100CF9446 /* CredentialDetailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE2D40962ACC470100CF9446 /* CredentialDetailViewModel.swift */; }; EE3813502938D5B100A3A710 /* Apollo in Frameworks */ = {isa = PBXBuildFile; productRef = EE38134F2938D5B100A3A710 /* Apollo */; }; EE3813522938D5B100A3A710 /* Authenticate in Frameworks */ = {isa = PBXBuildFile; productRef = EE3813512938D5B100A3A710 /* Authenticate */; }; @@ -282,11 +292,21 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + EE26FD9D2D720C1A00292E6C /* Castor in Frameworks */, EE38135A2938D5B100A3A710 /* Mercury in Frameworks */, + EE26FDA32D720C1A00292E6C /* EdgeAgentSDK in Frameworks */, EE3813582938D5B100A3A710 /* Domain in Frameworks */, EE38135E2938D5B100A3A710 /* Pollux in Frameworks */, EEC5A8392BEA5E3A00AED928 /* EdgeAgent in Frameworks */, EE38135C2938D5B100A3A710 /* Pluto in Frameworks */, + EE26FDA12D720C1A00292E6C /* EdgeAgent in Frameworks */, + EE26FD992D720C1A00292E6C /* Authenticate in Frameworks */, + EE26FDA72D720C1A00292E6C /* Pluto in Frameworks */, + EE26FDA52D720C1A00292E6C /* Mercury in Frameworks */, + EE26FD9B2D720C1A00292E6C /* Builders in Frameworks */, + EE26FD9F2D720C1A00292E6C /* Domain in Frameworks */, + EE26FDA92D720C1A00292E6C /* Pollux in Frameworks */, + EE26FD972D720C1A00292E6C /* Apollo in Frameworks */, EE3813542938D5B100A3A710 /* Builders in Frameworks */, EE3813502938D5B100A3A710 /* Apollo in Frameworks */, EE3813562938D5B100A3A710 /* Castor in Frameworks */, @@ -842,6 +862,16 @@ EE38135B2938D5B100A3A710 /* Pluto */, EE38135D2938D5B100A3A710 /* Pollux */, EEC5A8382BEA5E3A00AED928 /* EdgeAgent */, + EE26FD962D720C1A00292E6C /* Apollo */, + EE26FD982D720C1A00292E6C /* Authenticate */, + EE26FD9A2D720C1A00292E6C /* Builders */, + EE26FD9C2D720C1A00292E6C /* Castor */, + EE26FD9E2D720C1A00292E6C /* Domain */, + EE26FDA02D720C1A00292E6C /* EdgeAgent */, + EE26FDA22D720C1A00292E6C /* EdgeAgentSDK */, + EE26FDA42D720C1A00292E6C /* Mercury */, + EE26FDA62D720C1A00292E6C /* Pluto */, + EE26FDA82D720C1A00292E6C /* Pollux */, ); productName = AtalaPrismWalletDemo; productReference = EEE61FB62937CA280053AE52 /* AtalaPrismWalletDemo.app */; @@ -872,7 +902,7 @@ ); mainGroup = EEE61FAD2937CA280053AE52; packageReferences = ( - EE86C3C82CF5EC750072BEB7 /* XCRemoteSwiftPackageReference "identus-edge-agent-sdk-swift" */, + EE26FD952D720C1A00292E6C /* XCLocalSwiftPackageReference "../../../atala-prism-wallet-sdk-swift" */, ); productRefGroup = EEE61FB72937CA280053AE52 /* Products */; projectDirPath = ""; @@ -1234,18 +1264,54 @@ }; /* End XCConfigurationList section */ -/* Begin XCRemoteSwiftPackageReference section */ - EE86C3C82CF5EC750072BEB7 /* 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; - }; +/* Begin XCLocalSwiftPackageReference section */ + EE26FD952D720C1A00292E6C /* XCLocalSwiftPackageReference "../../../atala-prism-wallet-sdk-swift" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../../../atala-prism-wallet-sdk-swift"; }; -/* End XCRemoteSwiftPackageReference section */ +/* End XCLocalSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + EE26FD962D720C1A00292E6C /* Apollo */ = { + isa = XCSwiftPackageProductDependency; + productName = Apollo; + }; + EE26FD982D720C1A00292E6C /* Authenticate */ = { + isa = XCSwiftPackageProductDependency; + productName = Authenticate; + }; + EE26FD9A2D720C1A00292E6C /* Builders */ = { + isa = XCSwiftPackageProductDependency; + productName = Builders; + }; + EE26FD9C2D720C1A00292E6C /* Castor */ = { + isa = XCSwiftPackageProductDependency; + productName = Castor; + }; + EE26FD9E2D720C1A00292E6C /* Domain */ = { + isa = XCSwiftPackageProductDependency; + productName = Domain; + }; + EE26FDA02D720C1A00292E6C /* EdgeAgent */ = { + isa = XCSwiftPackageProductDependency; + productName = EdgeAgent; + }; + EE26FDA22D720C1A00292E6C /* EdgeAgentSDK */ = { + isa = XCSwiftPackageProductDependency; + productName = EdgeAgentSDK; + }; + EE26FDA42D720C1A00292E6C /* Mercury */ = { + isa = XCSwiftPackageProductDependency; + productName = Mercury; + }; + EE26FDA62D720C1A00292E6C /* Pluto */ = { + isa = XCSwiftPackageProductDependency; + productName = Pluto; + }; + EE26FDA82D720C1A00292E6C /* Pollux */ = { + isa = XCSwiftPackageProductDependency; + productName = Pollux; + }; EE38134F2938D5B100A3A710 /* Apollo */ = { isa = XCSwiftPackageProductDependency; productName = Apollo; diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift index 72ff60af..f715a7cf 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Credentials/CredentialsList/CredentialListViewModel.swift @@ -140,7 +140,13 @@ final class CredentialListViewModelImpl: CredentialListViewModel { } switch message.piuri { case ProtocolTypes.didcommOfferCredential3_0.rawValue: - let newPrismDID = try await self.agent.createNewPrismDID() + var authenticationPrivateKey = try apollo.createPrivateKey(parameters: [ + KeyProperties.type.rawValue: "EC", + KeyProperties.curve.rawValue: KnownKeyCurves.ed25519.rawValue + ]) + let newPrismDID = try await self.agent.createNewPrismDID( + keys: [(.authentication, authenticationPrivateKey)] + ) guard let requestCredential = try await self.agent.prepareRequestCredentialWithIssuer( did: newPrismDID, offer: try OfferCredential3_0(fromMessage: message) diff --git a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageView.swift b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageView.swift index 8a2ebf42..ccb476c1 100644 --- a/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageView.swift +++ b/Sample/AtalaPrismWalletDemo/AtalaPrismWalletDemo/Modules/WalletDemo2/Mediator/MediatorPage/MediatorPageView.swift @@ -12,7 +12,7 @@ protocol MediatorPageViewModel: ObservableObject { struct MediatorPageView: View { @StateObject var viewModel: ViewModel - @State var didInput = "did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vc2l0LXByaXNtLW1lZGlhdG9yLmF0YWxhcHJpc20uaW8iLCJhIjpbImRpZGNvbW0vdjIiXX19.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL3NpdC1wcmlzbS1tZWRpYXRvci5hdGFsYXByaXNtLmlvL3dzIiwiYSI6WyJkaWRjb21tL3YyIl19fQ" + @State var didInput = "did:peer:2.Ez6LSghwSE437wnDE1pt3X6hVDUQzSjsHzinpX3XFvMjRAm7y.Vz6Mkhh1e5CEYYq6JBUcTZ6Cp2ranCWRrv7Yax3Le4N59R6dd.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHBzOi8vbWVkaWF0b3IuYXRhbGFwcmlzbS5pbyIsImEiOlsiZGlkY29tbS92MiJdfX0.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6IndzczovL21lZGlhdG9yLmF0YWxhcHJpc20uaW8vd3MiLCJhIjpbImRpZGNvbW0vdjIiXX19" var body: some View { VStack {