Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(pollux): add anoncreds prooving implementation #106

Merged
merged 1 commit into from
Dec 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small comment or suggestion but is this if condition or else really needed?
computeAttributes and computePredicates will not process much if the arrays or object with attributes or predicates are empty right?

Could be wrong :)

Copy link
Contributor Author

@goncalo-frade-iohk goncalo-frade-iohk Nov 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah so the way I envision this logic was.

If the user provides ZKP Parameters then we don't do anything, if they wish to provide ZKP parameters with no attributes or predicates, its their option, so we don't compute them.

If the user doesn't provide ZKP parameters then we compute a "default" version based on the presentation request.

We can change this logic but it was what I envisioned

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
Loading