From 25e6bba90d36fc71c9d4ae6682eea03ce918a355 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 --- 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 | 43 ++++++++++ .../ParseAnoncredsCredentialFromMessage.swift | 9 +- .../Sources/PolluxImpl+ParseCredential.swift | 22 +++-- .../Pollux/Tests/AnoncredsTests.swift | 4 +- .../Pollux/Tests/Mocks/MockIssuer.swift | 37 ++++++++- Package.swift | 2 +- 12 files changed, 224 insertions(+), 11 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 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..a93ae393 --- /dev/null +++ b/AtalaPrismSDK/Pollux/Sources/Models/AnonCreds/AnoncredsPresentation.swift @@ -0,0 +1,43 @@ +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() + print(credential.schemaId) + print(credential.credentialDefinitionId) + return try Prover().createPresentation( + presentationRequest: request, + credentials: [credentialRequest], + selfAttested: nil, + linkSecret: linkSecret, + schemas: [credential.schemaId: schema], + credentialDefinitions: [credential.credentialDefinitionId: credentialDefinition] + ).getJson() + } +} diff --git a/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/ParseAnoncredsCredentialFromMessage.swift b/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/ParseAnoncredsCredentialFromMessage.swift index d27c94f1..f562c0d7 100644 --- a/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/ParseAnoncredsCredentialFromMessage.swift +++ b/AtalaPrismSDK/Pollux/Sources/Operation/Anoncreds/ParseAnoncredsCredentialFromMessage.swift @@ -6,14 +6,19 @@ struct ParseAnoncredsCredentialFromMessage { static func parse( issuerCredentialData: Data, linkSecret: String, - credentialDefinitionDownloader: Downloader + credentialDefinitionDownloader: Downloader, + schemaDownloader: Downloader ) 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) + return AnoncredsCredentialStack( + schema: try JSONDecoder.didComm().decode(AnonCredentialSchema.self, from: schemaData), definition: try JSONDecoder.didComm().decode(AnonCredentialDefinition.self, from: credentialDefinitionData), credential: domainCred ) diff --git a/AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift b/AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift index 57d2bcf2..053c1fbc 100644 --- a/AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift +++ b/AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift @@ -25,7 +25,7 @@ extension PolluxImpl { }), case let CredentialOperationsOptions.linkSecret(_, secret: linkSecret) = linkSecretOption else { - throw PolluxError.invalidPrismDID + throw PolluxError.missingAndIsRequiredForOperation(type: "linkSecret") } guard @@ -33,9 +33,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.invalidPrismDID + 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.missingAndIsRequiredForOperation(type: "schemaDownloader") } switch issuedAttachment.data { @@ -43,13 +53,15 @@ extension PolluxImpl { return try await ParseAnoncredsCredentialFromMessage.parse( issuerCredentialData: json.data, linkSecret: linkSecret, - credentialDefinitionDownloader: downloader + credentialDefinitionDownloader: definitionDownloader, + schemaDownloader: schemaDownloader ) case let base64 as AttachmentBase64: return try await ParseAnoncredsCredentialFromMessage.parse( issuerCredentialData: try base64.decoded(), linkSecret: linkSecret, - credentialDefinitionDownloader: downloader + credentialDefinitionDownloader: definitionDownloader, + schemaDownloader: schemaDownloader ) default: throw PolluxError.unsupportedIssuedMessage diff --git a/AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift b/AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift index 64fa9062..493c3e78 100644 --- a/AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift +++ b/AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift @@ -33,7 +33,8 @@ 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 issuedMessage = try issuer.issueCredential(offer: offer, request: request.0) let credential = try await PolluxImpl().parseCredential( @@ -41,6 +42,7 @@ final class AnoncredsTests: XCTestCase { options: [ .linkSecret(id: "test", secret: linkSecretValue), .credentialDefinitionDownloader(downloader: defDownloader), + .schemaDownloader(downloader: schemaDownloader) ] ) diff --git a/AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift b/AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift index 5dc9112d..d8dd033c 100644 --- a/AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift +++ b/AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift @@ -18,7 +18,7 @@ struct MockIssuer { schemaName: "Test", schemaVersion: "1.0.0", issuerId: issuer, - attrNames: ["Test"] + attrNames: ["test"] ) let credDef = try! Issuer().createCredentialDefinition( @@ -101,4 +101,39 @@ struct MockIssuer { ] ) } + + func createPresentationRequest() throws -> (message: Message, requestStr: String) { + let nonce = try Nonce().getValue() + let presentation = """ +{"nonce":"\(nonce)","name":"pres_req_1","version":"1.0.0","requested_attributes":{"attr1_referent":{"name":"test"}},"requested_predicates":{}} +""" + 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":["test"]} +""" + } + + 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: ["http://localhost:8000/schemas/test": schema], + credentialDefinitions: ["http://localhost:8000/definitions/test": credDef] + ) + } } diff --git a/Package.swift b/Package.swift index 511bc065..5f3a57f5 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.3.4") ], targets: [ .target(