From 1d51dc482d81f75f16e94d676b56fb3c55b33f8d Mon Sep 17 00:00:00 2001 From: goncalo-frade-iohk Date: Mon, 27 Nov 2023 13:07:05 +0000 Subject: [PATCH] feat(pollux): add anoncreds prooving implementation --- .../Builders/Sources/PolluxBuilder.swift | 7 +- AtalaPrismSDK/Domain/Sources/BBs/Pollux.swift | 1 + .../Domain/Sources/Models/Errors.swift | 16 +- .../AnonCreds/AnonCredentialDefinition.swift | 7 + .../AnonCreds/AnonCredentialSchema.swift | 8 + ...dsCredentialStack+ProvableCredential.swift | 83 +++++++++ .../AnonCreds/AnoncredsCredentialStack.swift | 3 + .../AnonCreds/AnoncredsPresentation.swift | 41 +++++ .../CreateAnoncredCredentialRequest.swift | 37 +++- .../ParseAnoncredsCredentialFromMessage.swift | 41 ++++- .../PolluxImpl+CredentialRequest.swift | 25 ++- .../Sources/PolluxImpl+ParseCredential.swift | 29 +++- AtalaPrismSDK/Pollux/Sources/PolluxImpl.swift | 5 +- .../Pollux/Tests/AnoncredsTests.swift | 62 ++++++- .../Pollux/Tests/Mocks/MockIssuer.swift | 67 ++++++-- .../Pollux/Tests/Mocks/MockPluto.swift | 160 ++++++++++++++++++ .../PrismAgent/Sources/PrismAgent.swift | 2 +- Package.swift | 2 +- 18 files changed, 550 insertions(+), 46 deletions(-) create mode 100644 AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnonCredentialSchema.swift create mode 100644 AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ProvableCredential.swift create mode 100644 AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsPresentation.swift create mode 100644 AtalaPrismSDK/Pollux/Tests/Mocks/MockPluto.swift diff --git a/AtalaPrismSDK/Builders/Sources/PolluxBuilder.swift b/AtalaPrismSDK/Builders/Sources/PolluxBuilder.swift index 961d7124..7e5db701 100644 --- a/AtalaPrismSDK/Builders/Sources/PolluxBuilder.swift +++ b/AtalaPrismSDK/Builders/Sources/PolluxBuilder.swift @@ -2,10 +2,13 @@ import Domain import Pollux public struct PolluxBuilder { + private let pluto: Pluto - public init() {} + public init(pluto: Pluto) { + self.pluto = pluto + } public func build() -> Pollux { - PolluxImpl() + PolluxImpl(pluto: pluto) } } diff --git a/AtalaPrismSDK/Domain/Sources/BBs/Pollux.swift b/AtalaPrismSDK/Domain/Sources/BBs/Pollux.swift index bd030cf6..f34e00aa 100644 --- a/AtalaPrismSDK/Domain/Sources/BBs/Pollux.swift +++ b/AtalaPrismSDK/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 zkpPresentationParams(attributes: [String: Bool], predicates: [String]) // Anoncreds zero-knowledge proof presentation parameters case custom(key: String, data: Data) // Any custom data. } diff --git a/AtalaPrismSDK/Domain/Sources/Models/Errors.swift b/AtalaPrismSDK/Domain/Sources/Models/Errors.swift index da857d5b..bfdd3985 100644 --- a/AtalaPrismSDK/Domain/Sources/Models/Errors.swift +++ b/AtalaPrismSDK/Domain/Sources/Models/Errors.swift @@ -730,13 +730,19 @@ public enum PolluxError: KnownPrismError { /// An error case when the offer doesnt present enough information like Domain or Challenge case offerDoesntProvideEnoughInformation - + /// An error case when the issued credential message doesnt present enough information or unsupported attachment case unsupportedIssuedMessage /// An error case there is missing an `ExportableKey` case requiresExportableKeyForOperation(operation: String) + /// An error case when the message doesnt present enough information + case messageDoesntProvideEnoughInformation + + /// An requirement is missing for `CredentialOperationsOptions` + case missingAndIsRequiredForOperation(type: String) + /// The error code returned by the server. public var code: Int { switch self { @@ -754,6 +760,10 @@ public enum PolluxError: KnownPrismError { return 56 case .unsupportedIssuedMessage: return 57 + case .messageDoesntProvideEnoughInformation: + return 58 + case .missingAndIsRequiredForOperation: + return 59 } } @@ -780,6 +790,10 @@ public enum PolluxError: KnownPrismError { return "Operation \(operation) requires an ExportableKey" case .unsupportedIssuedMessage: return "Issue message provided doesnt have a valid attachment" + case .messageDoesntProvideEnoughInformation: + return "Message provided doesnt have enough information (attachment, type)" + case .missingAndIsRequiredForOperation(let type): + return "Operation requires the following parameter \(type)" } } } diff --git a/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnonCredentialDefinition.swift b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnonCredentialDefinition.swift index 8e89cb18..e599c807 100644 --- a/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnonCredentialDefinition.swift +++ b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnonCredentialDefinition.swift @@ -1,3 +1,4 @@ +import AnoncredsSwift import Foundation struct AnonCredentialDefinition: Codable { @@ -18,4 +19,10 @@ struct AnonCredentialDefinition: Codable { let type: String let tag: String let value: Value + + func getAnoncred() throws -> AnoncredsSwift.CredentialDefinition { + let json = try JSONEncoder().encode(self) + let jsonString = try json.toString() + return try .init(jsonString: jsonString) + } } diff --git a/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnonCredentialSchema.swift b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnonCredentialSchema.swift new file mode 100644 index 00000000..f4c38057 --- /dev/null +++ b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnonCredentialSchema.swift @@ -0,0 +1,8 @@ +import Foundation + +struct AnonCredentialSchema: Codable { + let name: String + let version: String + let attrNames: [String] + let issuerId: String +} diff --git a/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ProvableCredential.swift b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ProvableCredential.swift new file mode 100644 index 00000000..bcd8501c --- /dev/null +++ b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack+ProvableCredential.swift @@ -0,0 +1,83 @@ +import Domain +import Foundation + +extension AnoncredsCredentialStack: ProvableCredential { + func presentation( + request: Message, + options: [CredentialOperationsOptions] + ) throws -> String { + let requestStr: String + guard let attachment = request.attachments.first else { + throw PolluxError.messageDoesntProvideEnoughInformation + } + switch attachment.data { + case let attachmentData as AttachmentJsonData: + requestStr = try attachmentData.data.toString() + case let attachmentData as AttachmentBase64: + guard let data = Data(fromBase64URL: attachmentData.base64) else { + throw PolluxError.messageDoesntProvideEnoughInformation + } + requestStr = try data.toString() + default: + throw PolluxError.messageDoesntProvideEnoughInformation + } + + guard + let linkSecretOption = options.first(where: { + if case .linkSecret = $0 { return true } + return false + }), + case let CredentialOperationsOptions.linkSecret(_, secret: linkSecret) = linkSecretOption + else { + throw PolluxError.missingAndIsRequiredForOperation(type: "LinkSecret") + } + + if + let zkpParameters = options.first(where: { + if case .zkpPresentationParams = $0 { return true } + return false + }), + case let CredentialOperationsOptions.zkpPresentationParams(attributes, predicates) = zkpParameters + { + return try AnoncredsPresentation().createPresentation( + stack: self, + request: requestStr, + linkSecret: linkSecret, + attributes: attributes, + predicates: predicates + ) + } else { + return try AnoncredsPresentation().createPresentation( + stack: self, + request: requestStr, + linkSecret: linkSecret, + attributes: try computeAttributes(requestJson: requestStr), + predicates: try computePredicates(requestJson: requestStr) + ) + } + } +} + +private func computeAttributes(requestJson: String) throws -> [String: Bool] { + guard + let json = try JSONSerialization.jsonObject(with: try requestJson.tryData(using: .utf8)) as? [String: Any] + else { + throw PolluxError.messageDoesntProvideEnoughInformation + } + let requestedAttributes = json["requested_attributes"] as? [String: Any] + return requestedAttributes?.reduce([:]) { partialResult, row in + var dic = partialResult + dic[row.key] = true + return dic + } ?? [:] +} + +private func computePredicates(requestJson: String) throws -> [String] { + guard + let json = try JSONSerialization.jsonObject(with: try requestJson.tryData(using: .utf8)) as? [String: Any] + else { + throw PolluxError.messageDoesntProvideEnoughInformation + } + let requestedPredicates = json["requested_predicates"] as? [String: Any] + return requestedPredicates?.map { $0.key } ?? [] +} diff --git a/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack.swift b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack.swift index 089cd075..774fb14c 100644 --- a/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack.swift +++ b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsCredentialStack.swift @@ -3,6 +3,7 @@ import Domain import Foundation struct AnoncredsCredentialStack: Codable { + let schema: AnonCredentialSchema let definition: AnonCredentialDefinition let credential: AnonCredential } @@ -39,6 +40,8 @@ extension AnoncredsCredentialStack: Domain.Credential { ] as [String : Any] (try? JSONEncoder.didComm().encode(definition)).map { properties["credentialDefinition"] = $0 } + (try? JSONEncoder.didComm().encode(schema)) + .map { properties["schema"] = $0 } (try? JSONEncoder.didComm().encode(credential.signature)).map { properties["signature"] = $0 } (try? JSONEncoder.didComm().encode(credential.signatureCorrectnessProof)).map { properties["signatureCorrectnessProof"] = $0 } (try? JSONEncoder.didComm().encode(credential.witness)).map { properties["witness"] = $0 } diff --git a/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsPresentation.swift b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsPresentation.swift new file mode 100644 index 00000000..5d0686db --- /dev/null +++ b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsPresentation.swift @@ -0,0 +1,41 @@ +import AnoncredsSwift +import Domain +import Foundation + +struct AnoncredsPresentation { + func createPresentation( + stack: AnoncredsCredentialStack, + request: String, + linkSecret: String, + attributes: [String: Bool], + predicates: [String] + ) throws -> String { + let linkSecret = try LinkSecret.newFromValue(valueString: linkSecret) + let request = try PresentationRequest(jsonString: request) + let credentialRequest = CredentialRequests( + credential: try stack.credential.getAnoncred(), + requestedAttribute: attributes.map { + .init(referent: $0.key, revealed: $0.value) + }, + requestedPredicate: predicates.map { .init(referent: $0) } + ) + + let credential = stack.credential + let schema = Schema.init( + name: stack.schema.name, + version: stack.schema.version, + attrNames: AttributeNames(stack.schema.attrNames), + issuerId: stack.schema.issuerId + ) + + let credentialDefinition = try stack.definition.getAnoncred() + return try Prover().createPresentation( + presentationRequest: request, + credentials: [credentialRequest], + selfAttested: [:], + linkSecret: linkSecret, + schemas: [credential.schemaId: schema], + credentialDefinitions: [credential.credentialDefinitionId: credentialDefinition] + ).getJson() + } +} diff --git a/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift b/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift index ce423ca8..8f5412f2 100644 --- a/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift +++ b/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/CreateAnoncredCredentialRequest.swift @@ -11,13 +11,30 @@ private struct Schema: Codable { let issuerId: String } +struct StorableCredentialRequestMetadata: StorableCredential { + let metadataJson: Data + let storingId: String + var recoveryId: String { "anoncreds+metadata"} + var credentialData: Data { metadataJson } + var queryIssuer: String? { nil } + var querySubject: String? { nil } + var queryCredentialCreated: Date? { nil } + var queryCredentialUpdated: Date? { nil } + var queryCredentialSchema: String? { nil } + var queryValidUntil: Date? { nil } + var queryRevoked: Bool? { nil } + var queryAvailableClaims: [String] { [] } +} + struct CreateAnoncredCredentialRequest { static func create( did: String, linkSecret: String, linkSecretId: String, offerData: Data, - credentialDefinitionDownloader: Downloader + credentialDefinitionDownloader: Downloader, + thid: String, + pluto: Pluto ) async throws -> String { let linkSecretObj = try LinkSecret.newFromValue(valueString: linkSecret) let offer = try CredentialOffer(jsonString: String(data: offerData, encoding: .utf8)!) @@ -25,17 +42,27 @@ struct CreateAnoncredCredentialRequest { let credentialDefinitionData = try await credentialDefinitionDownloader.downloadFromEndpoint(urlOrDID: credDefId) let credentialDefinitionJson = try credentialDefinitionData.toString() - let credentialDefinition = try CredentialDefinition(jsonString: credentialDefinitionJson) - let def = try Prover().createCredentialRequest( + let requestData = try Prover().createCredentialRequest( entropy: did, proverDid: nil, credDef: credentialDefinition, linkSecret: linkSecretObj, linkSecretId: linkSecretId, credentialOffer: offer - ).request.getJson() - return def + ) + + guard + let metadata = try requestData.metadata.getJson().data(using: .utf8) + else { + throw CommonError.invalidCoding(message: "Could not decode to data") + } + + let storableMetadata = StorableCredentialRequestMetadata(metadataJson: metadata, storingId: thid) + + try await pluto.storeCredential(credential: storableMetadata).first().await() + + return try requestData.request.getJson() } } diff --git a/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/ParseAnoncredsCredentialFromMessage.swift b/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/ParseAnoncredsCredentialFromMessage.swift index d27c94f1..4551267f 100644 --- a/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/ParseAnoncredsCredentialFromMessage.swift +++ b/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/ParseAnoncredsCredentialFromMessage.swift @@ -6,16 +6,49 @@ struct ParseAnoncredsCredentialFromMessage { static func parse( issuerCredentialData: Data, linkSecret: String, - credentialDefinitionDownloader: Downloader + credentialDefinitionDownloader: Downloader, + schemaDownloader: Downloader, + thid: String, + pluto: Pluto ) async throws -> AnoncredsCredentialStack { let domainCred = try JSONDecoder().decode(AnonCredential.self, from: issuerCredentialData) - let credentialDefinitionData = try await credentialDefinitionDownloader .downloadFromEndpoint(urlOrDID: domainCred.credentialDefinitionId) - + let schemaData = try await schemaDownloader + .downloadFromEndpoint(urlOrDID: domainCred.schemaId) + + guard let metadata = try await pluto.getAllCredentials() + .first() + .await() + .first(where: { $0.storingId == thid }) + else { + throw PolluxError.messageDoesntProvideEnoughInformation + } + + let linkSecretObj = try LinkSecret.newFromValue(valueString: linkSecret) + let credentialDefinitionJson = try credentialDefinitionData.toString() + let credentialDefinition = try CredentialDefinition(jsonString: credentialDefinitionJson) + + let credentialMetadataJson = try metadata.credentialData.toString() + let credentialMetadataObj = try CredentialRequestMetadata(jsonString: credentialMetadataJson) + + let credentialObj = try Credential(jsonString: issuerCredentialData.toString()) + + let processedCredential = try Prover().processCredential( + credential: credentialObj, + credRequestMetadata: credentialMetadataObj, + linkSecret: linkSecretObj, + credDef: credentialDefinition, + revRegDef: nil + ) + + let processedCredentialJson = try processedCredential.getJson().tryData(using: .utf8) + let finalCredential = try JSONDecoder().decode(AnonCredential.self, from: processedCredentialJson) + return AnoncredsCredentialStack( + schema: try JSONDecoder.didComm().decode(AnonCredentialSchema.self, from: schemaData), definition: try JSONDecoder.didComm().decode(AnonCredentialDefinition.self, from: credentialDefinitionData), - credential: domainCred + credential: finalCredential ) } } diff --git a/AtalaPrismSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift b/AtalaPrismSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift index 54e23118..a13c3489 100644 --- a/AtalaPrismSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift +++ b/AtalaPrismSDK/Pollux/Sources/PolluxImpl+CredentialRequest.swift @@ -28,12 +28,26 @@ extension PolluxImpl { case "anoncreds/credential-offer@v1.0": switch offerAttachment.data { case let attachmentData as AttachmentJsonData: - return try await processAnoncredsCredentialRequest(offerData: attachmentData.data, options: options) + guard let thid = offerMessage.thid else { + throw PolluxError.messageDoesntProvideEnoughInformation + } + return try await processAnoncredsCredentialRequest( + offerData: attachmentData.data, + thid: thid, + options: options + ) case let attachmentData as AttachmentBase64: - guard let data = Data(fromBase64URL: attachmentData.base64) else { + guard + let thid = offerMessage.thid, + let data = Data(fromBase64URL: attachmentData.base64) + else { throw PolluxError.offerDoesntProvideEnoughInformation } - return try await processAnoncredsCredentialRequest(offerData: data, options: options) + return try await processAnoncredsCredentialRequest( + offerData: data, + thid: thid, + options: options + ) default: throw PolluxError.offerDoesntProvideEnoughInformation } @@ -70,6 +84,7 @@ extension PolluxImpl { private func processAnoncredsCredentialRequest( offerData: Data, + thid: String, options: [CredentialOperationsOptions] ) async throws -> String { guard @@ -107,7 +122,9 @@ extension PolluxImpl { linkSecret: linkSecret, linkSecretId: linkSecretId, offerData: offerData, - credentialDefinitionDownloader: downloader + credentialDefinitionDownloader: downloader, + thid: thid, + pluto: self.pluto ) } } diff --git a/AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift b/AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift index 57d2bcf2..0e97a4b4 100644 --- a/AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift +++ b/AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift @@ -18,6 +18,9 @@ extension PolluxImpl { throw PolluxError.unsupportedIssuedMessage } case "anoncreds", "prism/anoncreds", "anoncreds/credential@v1.0": + guard let thid = issuedCredential.thid else { + throw PolluxError.messageDoesntProvideEnoughInformation + } guard let linkSecretOption = options.first(where: { if case .linkSecret = $0 { return true } @@ -25,7 +28,7 @@ extension PolluxImpl { }), case let CredentialOperationsOptions.linkSecret(_, secret: linkSecret) = linkSecretOption else { - throw PolluxError.invalidPrismDID + throw PolluxError.missingAndIsRequiredForOperation(type: "linkSecret") } guard @@ -33,9 +36,19 @@ extension PolluxImpl { if case .credentialDefinitionDownloader = $0 { return true } return false }), - case let CredentialOperationsOptions.credentialDefinitionDownloader(downloader) = credDefinitionDownloaderOption + case let CredentialOperationsOptions.credentialDefinitionDownloader(definitionDownloader) = credDefinitionDownloaderOption + else { + throw PolluxError.missingAndIsRequiredForOperation(type: "credentialDefinitionDownloader") + } + + guard + let schemaDownloaderOption = options.first(where: { + if case .schemaDownloader = $0 { return true } + return false + }), + case let CredentialOperationsOptions.schemaDownloader(schemaDownloader) = schemaDownloaderOption else { - throw PolluxError.invalidPrismDID + throw PolluxError.missingAndIsRequiredForOperation(type: "schemaDownloader") } switch issuedAttachment.data { @@ -43,13 +56,19 @@ extension PolluxImpl { return try await ParseAnoncredsCredentialFromMessage.parse( issuerCredentialData: json.data, linkSecret: linkSecret, - credentialDefinitionDownloader: downloader + credentialDefinitionDownloader: definitionDownloader, + schemaDownloader: schemaDownloader, + thid: thid, + pluto: self.pluto ) case let base64 as AttachmentBase64: return try await ParseAnoncredsCredentialFromMessage.parse( issuerCredentialData: try base64.decoded(), linkSecret: linkSecret, - credentialDefinitionDownloader: downloader + credentialDefinitionDownloader: definitionDownloader, + schemaDownloader: schemaDownloader, + thid: thid, + pluto: self.pluto ) default: throw PolluxError.unsupportedIssuedMessage diff --git a/AtalaPrismSDK/Pollux/Sources/PolluxImpl.swift b/AtalaPrismSDK/Pollux/Sources/PolluxImpl.swift index 88b877cf..be68fbf5 100644 --- a/AtalaPrismSDK/Pollux/Sources/PolluxImpl.swift +++ b/AtalaPrismSDK/Pollux/Sources/PolluxImpl.swift @@ -2,5 +2,8 @@ import Combine import Domain public struct PolluxImpl { - public init() {} + let pluto: Pluto + public init(pluto: Pluto) { + self.pluto = pluto + } } diff --git a/AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift b/AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift index 64fa9062..f4f1fa21 100644 --- a/AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift +++ b/AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift @@ -3,11 +3,12 @@ import AnoncredsSwift import XCTest final class AnoncredsTests: XCTestCase { + let pluto = MockPluto() let issuer = MockIssuer() var linkSecret: LinkSecret! override func setUp() async throws { - linkSecret = try LinkSecret.newFromValue(valueString: "28380340054639370074509985417762391330214600660319893567746760706478614060614") + linkSecret = try LinkSecret.newFromValue(valueString: "65965334953670062552662719679603258895632947953618378932199361160021795698890") } func testCreateMessageRequest() async throws { @@ -17,7 +18,7 @@ final class AnoncredsTests: XCTestCase { let defDownloader = MockDownloader(returnData: try credDef.getJson().data(using: .utf8)!) // No error means it passed - _ = try await PolluxImpl().processCredentialRequest( + _ = try await PolluxImpl(pluto: pluto).processCredentialRequest( offerMessage: offer, options: [ .linkSecret(id: "test", secret: linkSecretValue), @@ -33,18 +34,67 @@ final class AnoncredsTests: XCTestCase { let credDef = issuer.credDef let defDownloader = MockDownloader(returnData: try credDef.getJson().data(using: .utf8)!) - let prover = try MockProver(linkSecret: linkSecret, credDef: credDef) + let schemaDownloader = MockDownloader(returnData: issuer.getSchemaJson().data(using: .utf8)!) + let prover = MockProver(linkSecret: linkSecret, credDef: credDef) let request = try prover.createRequest(offer: offer) + let credentialMetadata = try StorableCredentialRequestMetadata( + metadataJson: request.1.getJson().tryData(using: .utf8), + storingId: "1" + ) + try await pluto.storeCredential(credential: credentialMetadata).first().await() let issuedMessage = try issuer.issueCredential(offer: offer, request: request.0) - let credential = try await PolluxImpl().parseCredential( + let credential = try await PolluxImpl(pluto: pluto).parseCredential( issuedCredential: issuedMessage, options: [ .linkSecret(id: "test", secret: linkSecretValue), .credentialDefinitionDownloader(downloader: defDownloader), + .schemaDownloader(downloader: schemaDownloader) ] ) - XCTAssertEqual(credential.claims.first?.key, "test") - XCTAssertEqual(credential.claims.first?.getValueAsString(), "test") + XCTAssertTrue(credential.claims.contains(where: { $0.key == "name" })) + XCTAssertTrue(credential.claims.contains(where: { $0.key == "sex" })) + XCTAssertTrue(credential.claims.contains(where: { $0.key == "age" })) + XCTAssertEqual(credential.claims.first(where: { $0.key == "name" })?.getValueAsString(), "Miguel") } + + + func testProvingCredential() async throws { + let offer = try issuer.createOffer() + let linkSecretValue = try linkSecret.getValue() + + let credDef = issuer.credDef + let defDownloader = MockDownloader(returnData: try credDef.getJson().data(using: .utf8)!) + let schemaDownloader = MockDownloader(returnData: issuer.getSchemaJson().data(using: .utf8)!) + let prover = MockProver(linkSecret: linkSecret, credDef: credDef) + let request = try prover.createRequest(offer: offer) + let credentialMetadata = try StorableCredentialRequestMetadata( + metadataJson: request.1.getJson().tryData(using: .utf8), + storingId: "1" + ) + try await pluto.storeCredential(credential: credentialMetadata).first().await() + let issuedMessage = try issuer.issueCredential(offer: offer, request: request.0) + let credential = try await PolluxImpl(pluto: pluto).parseCredential( + issuedCredential: issuedMessage, + options: [ + .linkSecret(id: "test", secret: linkSecretValue), + .credentialDefinitionDownloader(downloader: defDownloader), + .schemaDownloader(downloader: schemaDownloader) + ] + ) + XCTAssertTrue(credential.isProofable) + + let presentationRequest = try issuer.createPresentationRequest() + + let presentation = try credential.proof!.presentation( + request: presentationRequest.message, + options: [ + .linkSecret(id: "", secret: issuer.linkSecret.getValue()), + ] + ) + + let value = try issuer.verifyPresentation(presentation: presentation, request: presentationRequest.requestStr) + XCTAssertTrue(value) + } + } diff --git a/AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift b/AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift index 5dc9112d..61f4d603 100644 --- a/AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift +++ b/AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift @@ -4,7 +4,7 @@ import Foundation struct MockIssuer { - let issuer = "did:web:asadadada" + let issuer = "mock:issuer_id/path&q=bar" let linkSecret: LinkSecret let schema: Schema let credDef: CredentialDefinition @@ -12,20 +12,20 @@ struct MockIssuer { let credDefCorrProof: CredentialKeyCorrectnessProof init() { - self.linkSecret = try! LinkSecret.newFromValue(valueString: "36590588636589688587165354116254517405509622321561684934488049104990967858487") + self.linkSecret = try! LinkSecret.newFromValue(valueString: "65965334953670062552662719679603258895632947953618378932199361160021795698890") self.schema = try! Issuer().createSchema( - schemaName: "Test", - schemaVersion: "1.0.0", + schemaName: "mock:uri2", + schemaVersion: "0.1.0", issuerId: issuer, - attrNames: ["Test"] + attrNames: ["name", "sex", "age"] ) let credDef = try! Issuer().createCredentialDefinition( - schemaId: "http://localhost:8000/schemas/test", + schemaId: schema.name, schema: schema, issuerId: issuer, - tag: "test", + tag: "tag", signatureType: .cl, config: .init(supportRevocation: false) ) @@ -33,17 +33,12 @@ struct MockIssuer { self.credDef = credDef.credentialDefinition self.credDefPriv = credDef.credentialDefinitionPrivate self.credDefCorrProof = credDef.credentialKeyCorrectnessProof - - print("IssuerSecret: \(try! linkSecret.getValue())") - - print("credDef") - print(try! credDef.credentialDefinition.getJson()) } func createOffer() throws -> CredentialOffer { try Issuer().createCredentialOffer( - schemaId: "http://localhost:8000/schemas/test", - credDefId: "http://localhost:8000/definitions/test", + schemaId: "mock:uri2", + credDefId: "mock:uri3", correctnessProof: credDefCorrProof ) } @@ -65,7 +60,8 @@ struct MockIssuer { byteCount: nil, description: nil ) - ] + ], + thid: "1" ) } @@ -78,7 +74,11 @@ struct MockIssuer { credDefPrivate: credDefPriv, credOffer: offer, credRequest: request, - credValues: [.init(raw: "test", encoded: "test")], + credValues: [ + .init(raw: "name", encoded: "Miguel"), + .init(raw: "sex", encoded: "M"), + .init(raw: "age", encoded: "31") + ], revRegId: nil, revStatusList: nil, revocationConfig: nil @@ -98,7 +98,42 @@ struct MockIssuer { byteCount: nil, description: nil ) + ], + thid: "1" + ) + } + + func createPresentationRequest() throws -> (message: Message, requestStr: String) { + let presentation = """ +{"nonce":"1103253414365527824079144","name":"proof_req_1","version":"0.1","requested_attributes":{"sex":{"name":"sex", "restrictions":{"attr::sex::value":"M","cred_def_id":"mock:uri3"}}},"requested_predicates":{"age":{"name":"age", "p_type":">=", "p_value":18}}} +""" + return (Message( + piuri: "", + body: Data(), + attachments: [ + .init( + data: AttachmentBase64(base64: try presentation.tryData(using: .utf8).base64EncodedString()) + ) ] + ), presentation) + } + + func getSchemaJson() -> String { +""" +{"name":"\(schema.name)","issuerId":"\(schema.issuerId)","version":"\(schema.version)","attrNames":["name", "sex", "age"]} +""" + } + + func verifyPresentation(presentation: String, request: String) throws -> Bool { + let presentation = try Presentation(jsonString: presentation) + let request = try PresentationRequest(jsonString: request) + let credDef = self.credDef + let schema = self.schema + return try Verifier().verifyPresentation( + presentation: presentation, + presentationRequest: request, + schemas: ["mock:uri2": schema], + credentialDefinitions: ["mock:uri3": credDef] ) } } diff --git a/AtalaPrismSDK/Pollux/Tests/Mocks/MockPluto.swift b/AtalaPrismSDK/Pollux/Tests/Mocks/MockPluto.swift new file mode 100644 index 00000000..2063b113 --- /dev/null +++ b/AtalaPrismSDK/Pollux/Tests/Mocks/MockPluto.swift @@ -0,0 +1,160 @@ +import Combine +import Domain +import Foundation + +class MockPluto: Pluto { + var credentials = [StorableCredential]() + + func storePrismDID(did: Domain.DID, keyPairIndex: Int, alias: String?) -> AnyPublisher { + Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func storePeerDID(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?) -> AnyPublisher { + Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func storeDID(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?) -> AnyPublisher { + Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func storeDIDPair(pair: Domain.DIDPair) -> AnyPublisher { + Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func storeMessage(message: Domain.Message, direction: Domain.Message.Direction) -> AnyPublisher { + Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func storeMessages(messages: [(Domain.Message, Domain.Message.Direction)]) -> AnyPublisher { + Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func storeMediator(peer: Domain.DID, routingDID: Domain.DID, mediatorDID: Domain.DID) -> AnyPublisher { + Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func storeCredential(credential: Domain.StorableCredential) -> AnyPublisher { + credentials.append(credential) + return Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func storeLinkSecret(secret: StorableKey) -> AnyPublisher { + return Just(()).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllPrismDIDs() -> AnyPublisher<[(did: Domain.DID, keyPairIndex: Int, alias: String?)], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPrismDIDInfo(did: Domain.DID) -> AnyPublisher<(did: Domain.DID, keyPairIndex: Int, alias: String?)?, Error> { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPrismDIDInfo(alias: String) -> AnyPublisher<[(did: Domain.DID, keyPairIndex: Int)], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPrismDIDKeyPairIndex(did: Domain.DID) -> AnyPublisher { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPrismLastKeyPairIndex() -> AnyPublisher { + Just(0).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllPeerDIDs() -> AnyPublisher<[(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?)], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPeerDIDInfo(did: Domain.DID) -> AnyPublisher<(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?)?, Error> { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPeerDIDInfo(alias: String) -> AnyPublisher<[(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?)], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPeerDIDPrivateKeys(did: Domain.DID) -> AnyPublisher<[Domain.StorableKey]?, Error> { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllDIDs() -> AnyPublisher<[(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?)], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getDIDInfo(did: Domain.DID) -> AnyPublisher<(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?)?, Error> { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getDIDInfo(alias: String) -> AnyPublisher<[(did: Domain.DID, privateKeys: [Domain.StorableKey], alias: String?)], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getDIDPrivateKeys(did: Domain.DID) -> AnyPublisher<[Domain.StorableKey]?, Error> { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllDidPairs() -> AnyPublisher<[Domain.DIDPair], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPair(otherDID: Domain.DID) -> AnyPublisher { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPair(name: String) -> AnyPublisher { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getPair(holderDID: Domain.DID) -> AnyPublisher { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMessages() -> AnyPublisher<[Domain.Message], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMessages(did: Domain.DID) -> AnyPublisher<[Domain.Message], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMessagesSent() -> AnyPublisher<[Domain.Message], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMessagesReceived() -> AnyPublisher<[Domain.Message], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMessagesSentTo(did: Domain.DID) -> AnyPublisher<[Domain.Message], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMessagesReceivedFrom(did: Domain.DID) -> AnyPublisher<[Domain.Message], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMessagesOfType(type: String, relatedWithDID: Domain.DID?) -> AnyPublisher<[Domain.Message], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMessages(from: Domain.DID, to: Domain.DID) -> AnyPublisher<[Domain.Message], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getMessage(id: String) -> AnyPublisher { + Just(nil).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllMediators() -> AnyPublisher<[(did: Domain.DID, routingDID: Domain.DID, mediatorDID: Domain.DID)], Error> { + Just([]).tryMap { $0 }.eraseToAnyPublisher() + } + + func getAllCredentials() -> AnyPublisher<[Domain.StorableCredential], Error> { + self.credentials.publisher.collect().tryMap { $0 }.eraseToAnyPublisher() + } + + func getLinkSecret() -> AnyPublisher<[StorableKey], Error> { + return Just([]).tryMap { $0 }.eraseToAnyPublisher() + } +} diff --git a/AtalaPrismSDK/PrismAgent/Sources/PrismAgent.swift b/AtalaPrismSDK/PrismAgent/Sources/PrismAgent.swift index d2f801b6..bb1a6eb6 100644 --- a/AtalaPrismSDK/PrismAgent/Sources/PrismAgent.swift +++ b/AtalaPrismSDK/PrismAgent/Sources/PrismAgent.swift @@ -99,7 +99,7 @@ public class PrismAgent { let apollo = ApolloBuilder().build() let castor = CastorBuilder(apollo: apollo).build() let pluto = PlutoBuilder().build() - let pollux = PolluxBuilder().build() + let pollux = PolluxBuilder(pluto: pluto).build() let secretsStream = createSecretsStream( keyRestoration: apollo, diff --git a/Package.swift b/Package.swift index 511bc065..90888a37 100644 --- a/Package.swift +++ b/Package.swift @@ -67,7 +67,7 @@ let package = Package( .package(url: "git@github.com:swift-libp2p/swift-multibase.git", from: "0.0.1"), .package(url: "git@github.com:GigaBitcoin/secp256k1.swift.git", exact: "0.10.0"), .package(url: "git@github.com:goncalo-frade-iohk/Swift-JWT.git", from: "4.1.3"), - .package(url: "https://github.com/input-output-hk/anoncreds-rs.git", exact: "0.3.3") + .package(url: "https://github.com/input-output-hk/anoncreds-rs.git", exact: "0.4.1") ], targets: [ .target(