Skip to content

Commit

Permalink
feat(pollux): add credential abstraction
Browse files Browse the repository at this point in the history
ATL-4786

Besides adding the new credential abstraction this adds the following:
- Changes on Pollux to use the new abstraction
- Changes on PrismAgent to use new abstraction
- Changes on Sample App to for new abstraction
- Adds tests for new implementation
- Adds Codable to Messages
  • Loading branch information
goncalo-frade-iohk committed Jun 29, 2023
1 parent 6dc434c commit b13f899
Show file tree
Hide file tree
Showing 65 changed files with 1,196 additions and 894 deletions.
4 changes: 2 additions & 2 deletions AtalaPrismSDK/Domain/Sources/BBs/Pluto.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ public protocol Pluto {
/// - Parameter credential: The credential to store.
/// - Returns: A publisher that completes when the operation finishes.
func storeCredential(
credential: VerifiableCredential
credential: StorableCredential
) -> AnyPublisher<Void, Error>

/// Returns all stored PRISM DIDs, along with their associated key pair indices and aliases (if any).
Expand Down Expand Up @@ -213,5 +213,5 @@ public protocol Pluto {

/// Returns all stored verifiable credentials.
/// - Returns: A publisher that emits an array of stored verifiable credentials.
func getAllCredentials() -> AnyPublisher<[VerifiableCredential], Error>
func getAllCredentials() -> AnyPublisher<[StorableCredential], Error>
}
26 changes: 25 additions & 1 deletion AtalaPrismSDK/Domain/Sources/BBs/Pollux.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import Foundation

public enum CredentialOperationsOptions {
case schema(json: Data)
case link_secret(id: String, secret: String)
case subjectDID(DID)
case entropy(String)
case signableKey(SignableKey)
case exportableKey(ExportableKey)
case custom(key: String, data: Data)
}

/// The Pollux protocol defines the set of credential operations that are used in the Atala PRISM architecture.
public protocol Pollux {
/// Parses a JWT-encoded verifiable credential and returns a `VerifiableCredential` object representing the credential.
/// - Parameter jwtString: The JWT-encoded credential to parse.
/// - Throws: An error if the JWT cannot be parsed or decoded, or if the resulting verifiable credential is invalid.
/// - Returns: A `VerifiableCredential` object representing the parsed credential.
func parseVerifiableCredential(jwtString: String) throws -> VerifiableCredential
func parseCredential(data: Data) throws -> Credential
func restoreCredential(restorationIdentifier: String, credentialData: Data) throws -> Credential
func processCredentialRequest(
offerMessage: Message,
options: [CredentialOperationsOptions]
) throws -> String
}

extension Pollux {
func restoreCredential(storedCredential: StorableCredential) throws -> Credential {
try restoreCredential(
restorationIdentifier: storedCredential.recoveryId,
credentialData: storedCredential.credentialData
)
}
}
60 changes: 60 additions & 0 deletions AtalaPrismSDK/Domain/Sources/Models/Credentials/Credential.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation

public struct Claim {
public enum ClaimType: Comparable {
case string(String)
case bool(Bool)
case date(Date)
case data(Data)
case number(Double)

public static func < (lhs: Claim.ClaimType, rhs: Claim.ClaimType) -> Bool {
switch (lhs, rhs) {
case let (.string(str1), .string(str2)):
return str1 < str2
case let (.date(date1), .date(date2)):
return date1 < date2
case let (.number(number1), .number(number2)):
return number1 < number2
default:
return false
}
}
}

public let key: String
public let value: ClaimType

public init(key: String, value: ClaimType) {
self.key = key
self.value = value
}

public func getValueAsString() -> String {
switch value {
case .string(let string):
return string
case .bool(let bool):
return "\(bool)"
case .date(let date):
return date.formatted()
case .data(let data):
return data.base64EncodedString()
case .number(let double):
return "\(double)"
}
}
}

public protocol Credential {
var id: String { get }
var issuer: String { get }
var subject: String? { get }
var claims: [Claim] { get }
var properties: [String: Any] { get }
}

public extension Credential {
var isCodable: Bool { self is Codable }
var codable: Codable? { self as? Codable }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

public protocol ProofableCredential {
func presentation(request: Message, options: [CredentialOperationsOptions]) throws -> String
}

public extension Credential {
var isProofable: Bool { self is ProofableCredential }
var proof: ProofableCredential? { self as? ProofableCredential }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation

public protocol StorableCredential {
var storingId: String { get }
var recoveryId: String { get }
var credentialData: Data { get }
var queryIssuer: String? { get }
var querySubject: String? { get }
var queryCredentialCreated: Date? { get }
var queryCredentialUpdated: Date? { get }
var queryCredentialSchema: String? { get }
var queryValidUntil: Date? { get }
var queryRevoked: Bool? { get }
var queryAvailableClaims: [String] { get }
}

public extension Credential {
var isStorable: Bool { self is StorableCredential }
var storable: StorableCredential? { self as? StorableCredential }
}
14 changes: 14 additions & 0 deletions AtalaPrismSDK/Domain/Sources/Models/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,12 @@ public enum PolluxError: KnownPrismError {

/// An error case representing an invalid JWT credential. This error is thrown when attempting to create a JWT presentation without a valid JWTCredential.
case invalidJWTCredential

/// An error case when the offer doesnt present enough information like Domain or Challenge
case offerDoesntProvideEnoughInformation

/// An error case there is missing an `ExportableKey`
case requiresExportableKeyForOperation(operation: String)

/// The error code returned by the server.
public var code: Int {
Expand All @@ -656,6 +662,10 @@ public enum PolluxError: KnownPrismError {
return 53
case .invalidJWTCredential:
return 54
case .offerDoesntProvideEnoughInformation:
return 55
case .requiresExportableKeyForOperation:
return 56
}
}

Expand All @@ -676,6 +686,10 @@ public enum PolluxError: KnownPrismError {
return "To create a JWT presentation a Prism DID is required"
case .invalidJWTCredential:
return "To create a JWT presentation please provide a valid JWTCredential"
case .offerDoesntProvideEnoughInformation:
return "Offer provided doesnt have challenge or domain in the attachments, or there is no Json Attachment"
case .requiresExportableKeyForOperation(let operation):
return "Operation \(operation) requires an ExportableKey"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
@testable import Domain
import Foundation

import Domain
import Foundation

extension Message: Codable {
Expand All @@ -25,7 +21,7 @@ extension Message: Codable {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(piuri, forKey: .piuri)
try container.encode(body.base64UrlEncodedString(), forKey: .body)
try container.encode(body, forKey: .body)
try container.encode(extraHeaders, forKey: .extraHeaders)
try container.encode(createdTime, forKey: .createdTime)
try container.encode(expiresTimePlus, forKey: .expiresTimePlus)
Expand All @@ -42,7 +38,7 @@ extension Message: Codable {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(String.self, forKey: .id)
let piuri = try container.decode(String.self, forKey: .piuri)
let body = try container.decode(String.self, forKey: .body)
let body = try container.decode(Data.self, forKey: .body)
let extraHeaders = try container.decode([String: String].self, forKey: .extraHeaders)
let createdTime = try container.decode(Date.self, forKey: .createdTime)
let expiresTimePlus = try container.decode(Date.self, forKey: .expiresTimePlus)
Expand All @@ -60,7 +56,7 @@ extension Message: Codable {
from: try from.map { try DID(string: $0) },
to: try to.map { try DID(string: $0) },
fromPrior: fromPrior,
body: Data(fromBase64URL: body)!,
body: body,
extraHeaders: extraHeaders,
createdTime: createdTime,
expiresTimePlus: expiresTimePlus,
Expand All @@ -71,10 +67,3 @@ extension Message: Codable {
)
}
}


extension Message: Equatable {
public static func == (lhs: Domain.Message, rhs: Domain.Message) -> Bool {
lhs.id == rhs.id && lhs.piuri == rhs.piuri
}
}
68 changes: 68 additions & 0 deletions AtalaPrismSDK/Domain/Sources/Models/MessageAttachment.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,71 @@ public struct AttachmentDescriptor {
self.description = description
}
}

extension AttachmentDescriptor: Codable {

enum CodingKeys: String, CodingKey {
case id
case mediaType
case data
case filename
case format
case lastmodTime
case byteCount
case description
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(mediaType, forKey: .mediaType)
try container.encode(data, forKey: .data)
try container.encode(filename, forKey: .filename)
try container.encode(format, forKey: .format)
try container.encode(lastmodTime, forKey: .lastmodTime)
try container.encode(byteCount, forKey: .byteCount)
try container.encode(description, forKey: .description)
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(String.self, forKey: .id)
let mediaType = try? container.decode(String.self, forKey: .mediaType)
let filename = try? container.decode([String].self, forKey: .filename)
let format = try? container.decode(String.self, forKey: .format)
let lastmodTime = try? container.decode(Date.self, forKey: .lastmodTime)
let byteCount = try? container.decode(Int.self, forKey: .byteCount)
let description = try? container.decode(String.self, forKey: .description)
let data: AttachmentData?
if let attchData = try? container.decode(AttachmentBase64.self, forKey: .data) {
data = attchData
} else if let attchData = try? container.decode(AttachmentJws.self, forKey: .data) {
data = attchData
} else if let attchData = try? container.decode(AttachmentHeader.self, forKey: .data) {
data = attchData
} else if let attchData = try? container.decode(AttachmentJwsData.self, forKey: .data) {
data = attchData
} else if let attchData = try? container.decode(AttachmentJsonData.self, forKey: .data) {
data = attchData
} else if let attchData = try? container.decode(AttachmentLinkData.self, forKey: .data) {
data = attchData
} else { data = nil }

guard let data else { throw CommonError.invalidCoding(
message: """
Could not parse AttachmentData to any of the following: AttachmentBase64, AttachmentJws, AttachmentHeader, AttachmentJwsData, AttachmentJsonData, AttachmentLinkData
"""
)}

self.init(
id: id,
mediaType: mediaType,
data: data,
filename: filename,
format: format,
lastmodTime: lastmodTime,
byteCount: byteCount,
description: description
)
}
}
Loading

0 comments on commit b13f899

Please sign in to comment.