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 Dec 20, 2023
1 parent ae14bb9 commit 80377b1
Show file tree
Hide file tree
Showing 18 changed files with 550 additions and 46 deletions.
7 changes: 5 additions & 2 deletions AtalaPrismSDK/Builders/Sources/PolluxBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
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,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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,58 @@ 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)!)
let credDefId = offer.getCredDefId()

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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Loading

0 comments on commit 80377b1

Please sign in to comment.