Skip to content

Commit

Permalink
feat(pollux): add anoncreds prooving implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
goncalo-frade-iohk committed Nov 30, 2023
1 parent f4a4a80 commit 25e6bba
Show file tree
Hide file tree
Showing 12 changed files with 224 additions and 11 deletions.
1 change: 1 addition & 0 deletions AtalaPrismSDK/Domain/Sources/BBs/Pollux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}

Expand Down
16 changes: 15 additions & 1 deletion AtalaPrismSDK/Domain/Sources/Models/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -754,6 +760,10 @@ public enum PolluxError: KnownPrismError {
return 56
case .unsupportedIssuedMessage:
return 57
case .messageDoesntProvideEnoughInformation:
return 58
case .missingAndIsRequiredForOperation:
return 59
}
}

Expand All @@ -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)"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import AnoncredsSwift
import Foundation

struct AnonCredentialDefinition: Codable {
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

struct AnonCredentialSchema: Codable {
let name: String
let version: String
let attrNames: [String]
let issuerId: String
}
Original file line number Diff line number Diff line change
@@ -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 } ?? []
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Domain
import Foundation

struct AnoncredsCredentialStack: Codable {
let schema: AnonCredentialSchema
let definition: AnonCredentialDefinition
let credential: AnonCredential
}
Expand Down Expand Up @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
22 changes: 17 additions & 5 deletions AtalaPrismSDK/Pollux/Sources/PolluxImpl+ParseCredential.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,31 +25,43 @@ extension PolluxImpl {
}),
case let CredentialOperationsOptions.linkSecret(_, secret: linkSecret) = linkSecretOption
else {
throw PolluxError.invalidPrismDID
throw PolluxError.missingAndIsRequiredForOperation(type: "linkSecret")
}

guard
let credDefinitionDownloaderOption = options.first(where: {
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 {
case let json as AttachmentJsonData:
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
Expand Down
4 changes: 3 additions & 1 deletion AtalaPrismSDK/Pollux/Tests/AnoncredsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,16 @@ 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(
issuedCredential: issuedMessage,
options: [
.linkSecret(id: "test", secret: linkSecretValue),
.credentialDefinitionDownloader(downloader: defDownloader),
.schemaDownloader(downloader: schemaDownloader)
]
)

Expand Down
37 changes: 36 additions & 1 deletion AtalaPrismSDK/Pollux/Tests/Mocks/MockIssuer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct MockIssuer {
schemaName: "Test",
schemaVersion: "1.0.0",
issuerId: issuer,
attrNames: ["Test"]
attrNames: ["test"]
)

let credDef = try! Issuer().createCredentialDefinition(
Expand Down Expand Up @@ -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]
)
}
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ let package = Package(
.package(url: "[email protected]:swift-libp2p/swift-multibase.git", from: "0.0.1"),
.package(url: "[email protected]:GigaBitcoin/secp256k1.swift.git", exact: "0.10.0"),
.package(url: "[email protected]: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(
Expand Down

0 comments on commit 25e6bba

Please sign in to comment.