From a09afd11d41ea5b68fe6141b06b73ef4f4fc8482 Mon Sep 17 00:00:00 2001 From: Adam Mika Date: Tue, 5 Mar 2024 14:45:22 -0700 Subject: [PATCH 01/15] Progress so far --- .../Credentials/PresentationExchange.swift | 89 +++++++++++++++++++ Sources/Web5/Crypto/JOSE/JWT.swift | 25 ++++++ 2 files changed, 114 insertions(+) create mode 100644 Sources/Web5/Credentials/PresentationExchange.swift diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift new file mode 100644 index 0000000..ad887c8 --- /dev/null +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -0,0 +1,89 @@ +import AnyCodable +import Foundation + +public struct PresentationDefinitionV2: Codable { + public let inputDescriptors: [InputDescriptorV2] +} + +public struct InputDescriptorV2: Codable, Hashable { + public let fields: [FieldV2] +} + +public struct FieldV2: Codable, Hashable { + public let id: String? + public let path: [String] + public let purpose: String? + public let filterJSON: AnyCodable? + public let predicate: Optionality? + public let name: String? + public let optional: Bool? + + public init( + id: String? = nil, + path: [String], + purpose: String? = nil, + filterJSON: AnyCodable? = nil, + predicate: Optionality? = nil, + name: String? = nil, + optional: Bool? = nil + ) { + self.id = id + self.path = path + self.purpose = purpose + self.filterJSON = filterJSON + self.predicate = predicate + self.name = name + self.optional = optional + } +} + +public enum Optionality: Codable { + case required + case preferred +} + +public enum PresentationExchange { + + public func selectCredentials( + vcJWTs: [String], + presentationDefinition: PresentationDefinitionV2 + ) throws -> [String] { + + fatalError("Not implemented") + } + + private func mapInputDescriptorsToVCs( + vcJWTList: [String], + presentationDefinition: PresentationDefinitionV2 + ) -> [InputDescriptorV2: [String]] { + let map = vcJWTList.map { vcJWT in + + } + + + + + + fatalError("Not implemented") + } + + /** + private fun mapInputDescriptorsToVCs( + vcJwtList: Iterable, + presentationDefinition: PresentationDefinitionV2 + ): Map> { + val vcJwtListWithNodes = vcJwtList.zip(vcJwtList.map { vcJwt -> + val vc = JWTParser.parse(vcJwt) as SignedJWT + + JsonPath.parse(vc.payload.toString()) + ?: throw JsonPathParseException() + }) + return presentationDefinition.inputDescriptors.associateWith { inputDescriptor -> + vcJwtListWithNodes.filter { (_, node) -> + vcSatisfiesInputDescriptor(node, inputDescriptor) + }.map { (vcJwt, _) -> vcJwt } + }.filterValues { it.isNotEmpty() } + } + */ + +} diff --git a/Sources/Web5/Crypto/JOSE/JWT.swift b/Sources/Web5/Crypto/JOSE/JWT.swift index b1d9d3a..5a413c1 100644 --- a/Sources/Web5/Crypto/JOSE/JWT.swift +++ b/Sources/Web5/Crypto/JOSE/JWT.swift @@ -90,4 +90,29 @@ public struct JWT { ) ) } + + public struct ParsedJWT { + let header: JWS.Header + let payload: Data + + public init( + header: JWS.Header, + payload: Data + ) { + self.header = header + self.payload = payload + } + } + + public static func parse(jwtString: String) throws -> ParsedJWT { + let parts = jwtString.components(separatedBy: ".") + + guard parts.count == 3 else { + throw JWTError.invalidJWT + } + } + + public enum JWTError: Error { + case invalidJWT + } } From 916531166bfa144172ee1b9f6ed0d0c6de7468a0 Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 01:26:54 -0800 Subject: [PATCH 02/15] Add presentationExchange.selectCredentials --- Package.swift | 4 + Sources/Web5/Common/FlatMap.swift | 29 +++ Sources/Web5/Common/ISO8601Date.swift | 1 + Sources/Web5/Common/OneOrMany.swift | 7 + .../Credentials/PresentationExchange.swift | 170 ++++++++++++++---- Sources/Web5/Crypto/JOSE/JWT.swift | 46 ++++- .../PresentationExchangeTests.swift | 48 +++++ 7 files changed, 267 insertions(+), 38 deletions(-) create mode 100644 Sources/Web5/Common/FlatMap.swift create mode 100644 Tests/Web5Tests/Credentials/PresentationExchangeTests.swift diff --git a/Package.swift b/Package.swift index 77a4678..0b8c836 100644 --- a/Package.swift +++ b/Package.swift @@ -21,6 +21,8 @@ let package = Package( .package(url: "https://github.com/allegro/swift-junit.git", from: "2.1.0"), .package(url: "https://github.com/flight-school/anycodable.git", from: "0.6.7"), .package(url: "https://github.com/Bouke/DNS.git", from: "1.2.0"), + .package(url: "https://github.com/KittyMac/Sextant.git", .upToNextMinor(from: "0.4.0")), + .package(url: "https://github.com/kylef/JSONSchema.swift.git", from: "0.6.0") ], targets: [ .target( @@ -30,6 +32,8 @@ let package = Package( .product(name: "ExtrasBase64", package: "swift-extras-base64"), .product(name: "AnyCodable", package: "anycodable"), .product(name: "DNS", package: "DNS"), + .product(name: "Sextant", package: "sextant"), + .product(name: "JSONSchema", package: "jsonschema.swift") ] ), .testTarget( diff --git a/Sources/Web5/Common/FlatMap.swift b/Sources/Web5/Common/FlatMap.swift new file mode 100644 index 0000000..21ed483 --- /dev/null +++ b/Sources/Web5/Common/FlatMap.swift @@ -0,0 +1,29 @@ +import AnyCodable +import Foundation + +/// Wrapper used to easily encode a `[String: AnyCodable]` to and decode a `[String: AnyCodable]` from a flat map. +@propertyWrapper +struct FlatMap: Codable { + var wrappedValue: [String: AnyCodable]? + + init(wrappedValue: [String: AnyCodable]?) { + self.wrappedValue = wrappedValue + } + + init(from decoder: Decoder) throws { + let value = try decoder.singleValueContainer() + if let mapValue = try? value.decode([String: AnyCodable].self) { + wrappedValue = mapValue + } else { + throw DecodingError.typeMismatch(Date.self, DecodingError.Context(codingPath: [], debugDescription: "TODO")) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + if let wrappedValue { + try container.encode(wrappedValue) + } + } +} diff --git a/Sources/Web5/Common/ISO8601Date.swift b/Sources/Web5/Common/ISO8601Date.swift index c395bcf..a978d22 100644 --- a/Sources/Web5/Common/ISO8601Date.swift +++ b/Sources/Web5/Common/ISO8601Date.swift @@ -1,3 +1,4 @@ +import AnyCodable import Foundation /// Wrapper used to easily encode a `Date` to and decode a `Date` from an ISO 8601 formatted date string. diff --git a/Sources/Web5/Common/OneOrMany.swift b/Sources/Web5/Common/OneOrMany.swift index 21fac74..2c3f037 100644 --- a/Sources/Web5/Common/OneOrMany.swift +++ b/Sources/Web5/Common/OneOrMany.swift @@ -33,6 +33,13 @@ public enum OneOrMany: Codable, Equatable { return nil } } + /* + { + thing: "thing", + "grab": { + "thing" : "thing" + } + */ public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift index ad887c8..09aa788 100644 --- a/Sources/Web5/Credentials/PresentationExchange.swift +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -1,19 +1,64 @@ import AnyCodable import Foundation +import Sextant +import JSONSchema + +public struct VCDataModel: Codable { + public let context: [String] + public let id: String + public let type: [String] + public let issuer: String + public let issuanceDate: String + public let expirationDate: String? + public let credentialSubject: [String: AnyCodable] + public let credentialStatus: CredentialStatus? + public let credentialSchema: CredentialSchema? + public let evidence: [String: AnyCodable]? + + enum CodingKeys: String, CodingKey { + case context = "@context" + case id + case type + case issuer + case issuanceDate + case expirationDate + case credentialSubject + case credentialStatus + case credentialSchema + case evidence + } +} + +public struct CredentialStatus: Codable { + public let id: String + public let type: String + public let statusPurpose: String + public let statusListIndex: String + public let statusListCredential: String +} + +public struct CredentialSchema: Codable { + public let id: String + public let type: String +} public struct PresentationDefinitionV2: Codable { public let inputDescriptors: [InputDescriptorV2] } public struct InputDescriptorV2: Codable, Hashable { - public let fields: [FieldV2] + public let constraints: ConstraintsV2 +} + +public struct ConstraintsV2: Codable, Hashable { + public let fields: [FieldV2]? } public struct FieldV2: Codable, Hashable { public let id: String? public let path: [String] public let purpose: String? - public let filterJSON: AnyCodable? + public let filter: [String: AnyCodable]? public let predicate: Optionality? public let name: String? public let optional: Bool? @@ -22,7 +67,7 @@ public struct FieldV2: Codable, Hashable { id: String? = nil, path: [String], purpose: String? = nil, - filterJSON: AnyCodable? = nil, + filter: [String: AnyCodable]? = nil, predicate: Optionality? = nil, name: String? = nil, optional: Bool? = nil @@ -30,7 +75,7 @@ public struct FieldV2: Codable, Hashable { self.id = id self.path = path self.purpose = purpose - self.filterJSON = filterJSON + self.filter = filter self.predicate = predicate self.name = name self.optional = optional @@ -44,46 +89,105 @@ public enum Optionality: Codable { public enum PresentationExchange { - public func selectCredentials( + public static func selectCredentials( vcJWTs: [String], presentationDefinition: PresentationDefinitionV2 ) throws -> [String] { - - fatalError("Not implemented") + let inputDescriptorToVcMap = try mapInputDescriptorsToVCs(vcJWTList: vcJWTs, presentationDefinition: presentationDefinition) + return inputDescriptorToVcMap.flatMap { $0.value } + } + + public enum Error: LocalizedError { + case parseFailed } - private func mapInputDescriptorsToVCs( + private static func mapInputDescriptorsToVCs( vcJWTList: [String], presentationDefinition: PresentationDefinitionV2 - ) -> [InputDescriptorV2: [String]] { - let map = vcJWTList.map { vcJWT in - + ) throws -> [InputDescriptorV2: [String]] { + let vcJWTListMap: [VCDataModel] = try vcJWTList.map { vcJWT in + let parsedJWT = try JWT.parse(jwtString: vcJWT) + guard let vcJSON = parsedJWT.payload["vc"]?.value as? [String: Any] else { + throw Error.parseFailed + } + + let vcData = try JSONSerialization.data(withJSONObject: vcJSON) + let vc = try JSONDecoder().decode(VCDataModel.self, from: vcData) + + return vc + } + + let vcJwtListWithNodes = zip(vcJWTList, vcJWTListMap) + + let result = try presentationDefinition.inputDescriptors.reduce(into: [InputDescriptorV2: [String]]()) { result, inputDescriptor in + let vcJwtList = try vcJwtListWithNodes.filter { (_, node) in + try vcSatisfiesInputDescriptor(vc: node, inputDescriptor: inputDescriptor) + }.map { (vcJwt, _) in + vcJwt + } + if !vcJwtList.isEmpty { + result[inputDescriptor] = vcJwtList + } + } + return result + } + + private static func vcSatisfiesInputDescriptor(vc: VCDataModel, inputDescriptor: InputDescriptorV2) throws -> Bool { + // If the Input Descriptor has constraints and fields defined, evaluate them. + guard let fields = inputDescriptor.constraints.fields else { + // If no fields are defined, VC satisfies + return true } + let requiredFields = fields.filter { !($0.optional ?? false) } + for field in requiredFields { + + // Takes field.path and queries the vc to see if there is a corresponding path. + let vcJson = try JSONEncoder().encode(vc) + guard let matchedPathValues = vcJson.query(values: field.path) else { return false } + + + if matchedPathValues.isEmpty { + // If no matching fields are found for a required field, the VC does not satisfy this Input Descriptor. + return false + } + + // If there is a filter, process it + if let filter = field.filter { + let fieldName = field.path[0] + + var filterProperties: [String: [String: String]] = [:] + + for (filterKey, filterValue) in filter { + guard let filterRule = filterValue.value as? String else { + return false + } + + filterProperties[fieldName, default: [:]][filterKey] = filterRule + } + + let satisfiesSchemaMatches = try matchedPathValues.filter { value in + let result = try JSONSchema.validate([fieldName: value], schema: [ + "properties": filterProperties + ]) + return result.valid + } + print(satisfiesSchemaMatches) + + if satisfiesSchemaMatches.isEmpty { + // If the field value does not satisfy the schema, the VC does not satisfy this Input Descriptor. + return false + } + } + } - - - fatalError("Not implemented") + // If the VC passes all the checks, it satisfies the criteria of the Input Descriptor. + return true + } + + private static func validateFilter() { + } - - /** - private fun mapInputDescriptorsToVCs( - vcJwtList: Iterable, - presentationDefinition: PresentationDefinitionV2 - ): Map> { - val vcJwtListWithNodes = vcJwtList.zip(vcJwtList.map { vcJwt -> - val vc = JWTParser.parse(vcJwt) as SignedJWT - - JsonPath.parse(vc.payload.toString()) - ?: throw JsonPathParseException() - }) - return presentationDefinition.inputDescriptors.associateWith { inputDescriptor -> - vcJwtListWithNodes.filter { (_, node) -> - vcSatisfiesInputDescriptor(node, inputDescriptor) - }.map { (vcJwt, _) -> vcJwt } - }.filterValues { it.isNotEmpty() } - } - */ } diff --git a/Sources/Web5/Crypto/JOSE/JWT.swift b/Sources/Web5/Crypto/JOSE/JWT.swift index 5a413c1..eb25d29 100644 --- a/Sources/Web5/Crypto/JOSE/JWT.swift +++ b/Sources/Web5/Crypto/JOSE/JWT.swift @@ -1,4 +1,5 @@ import Foundation +import AnyCodable public struct JWT { @@ -93,11 +94,11 @@ public struct JWT { public struct ParsedJWT { let header: JWS.Header - let payload: Data + let payload: [String: AnyCodable] public init( header: JWS.Header, - payload: Data + payload: [String: AnyCodable] ) { self.header = header self.payload = payload @@ -108,11 +109,46 @@ public struct JWT { let parts = jwtString.components(separatedBy: ".") guard parts.count == 3 else { - throw JWTError.invalidJWT + throw Error.verificationFailed("Malformed JWT. Expected 3 parts. Got \(parts.count)") } + + let base64urlEncodedJwtHeader = String(parts[0]) + let base64urlEncodedJwtPayload = String(parts[1]) + let _ = String(parts[2]) + + let jwtHeader: JWS.Header = try JSONDecoder().decode( + JWS.Header.self, + from: try base64urlEncodedJwtHeader.decodeBase64Url() + ) + + guard jwtHeader.type == "JWT" else { + throw Error.verificationFailed("Expected JWT header to contain typ property set to JWT") + } + + guard let _ = jwtHeader.keyID else { + throw Error.verificationFailed("Expected JWT header to contain kid") + } + + let jwtPayload = try JSONDecoder().decode( + [String: AnyCodable].self, + from: base64urlEncodedJwtPayload.decodeBase64Url() + ) + + return ParsedJWT(header: jwtHeader, payload: jwtPayload) } +} - public enum JWTError: Error { - case invalidJWT +// MARK: - Errors + +extension JWT { + public enum Error: LocalizedError { + case verificationFailed(String) + + public var errorDescription: String? { + switch self { + case let .verificationFailed(reason): + return "Verification Failed: \(reason)" + } + } } } diff --git a/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift b/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift new file mode 100644 index 0000000..68d7b6a --- /dev/null +++ b/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift @@ -0,0 +1,48 @@ +import XCTest + +@testable import Web5 + +class PresentationExchangeTests: XCTestCase { + + func testSelectCredentialsWhenMatchesFound() throws { + // Given + let vcJwt = """ + eyJraWQiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkjelEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2SyJ9.eyJpc3MiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkiLCJzdWIiOiJkaWQ6a2V5OnpRM3Noa3BhdmpLUmV3b0JrNmFyUEpuaEE4N1p6aExERVdnVnZaS05ISzZRcVZKREIiLCJpYXQiOjE3MDEzMDI1OTMsInZjIjp7Imlzc3VhbmNlRGF0ZSI6IjIwMjMtMTEtMzBUMDA6MDM6MTNaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6a2V5OnpRM3Noa3BhdmpLUmV3b0JrNmFyUEpuaEE4N1p6aExERVdnVnZaS05ISzZRcVZKREIiLCJsb2NhbFJlc3BlY3QiOiJoaWdoIiwibGVnaXQiOnRydWV9LCJpZCI6InVybjp1dWlkOjZjOGJiY2Y0LTg3YWYtNDQ5YS05YmZiLTMwYmYyOTk3NjIyNyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTdHJlZXRDcmVkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlzc3VlciI6ImRpZDprZXk6elEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSJ9fQ.qoqF4-FinFsQ2J-NFSO46xCE8kUTZqZCU5fYr6tS0TQ6VP8y-ZnyR6R3oAqLs_Yo_CqQi23yi38uDjLjksiD2w + """ + + let vcJwt2 = """ + eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTZLIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCIsInN1YiI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImlhdCI6MTcwMTMwMjU5MywidmMiOnsiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMS0zMFQwMDowMzoxM1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImxvY2FsUmVzcGVjdCI6ImhpZ2giLCJsZWdpdCI6dHJ1ZX0sImlkIjoidXJuOnV1aWQ6NmM4YmJjZjQtODdhZi00NDlhLTliZmItMzBiZjI5OTc2MjI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlN0cmVldENyZWQiXSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSkZReUlzSW5WelpTSTZJbk5wWnlJc0ltTnlkaUk2SW5ObFkzQXlOVFpyTVNJc0ltdHBaQ0k2SWs1Q1gzRnNWVWx5TURZdFFXbHJWbFpOUm5KbGNUQnNZeTF2YlZGMGVtTTJiSGRvYUdOMlowODJjalFpTENKNElqb2lSR1IwVGpoWE5uaGZNelJ3UWw5dWEzaFNNR1V4ZEcxRWEwNXVjMHBsZFhOQ1FVVlFla3RYWjJaZldTSXNJbmtpT2lKMU0xY3hNelZwYm05a1RIRjBZMFZpT1dwT1IxTXpTbk5OWDNWR1MyMXJTbE5pT0ZSeVl6bHNkVmRKSWl3aVlXeG5Jam9pUlZNeU5UWkxJbjAifX0.8AehkiboIK6SZy6LHC9ugy_OcT2VsjluzH4qzsgjfTtq9fEsGyY-cOW_xekNUa2RE2VzlP6FXk0gDn4xf6_r4g + """ + + let pd = PresentationDefinitionV2( + inputDescriptors: [ + InputDescriptorV2( + constraints: ConstraintsV2( + fields: [ + FieldV2( + path: ["$.credentialSubject.legit"], + filter: ["type":"boolean"] + ), + FieldV2( + path: ["$.credentialSubject.localRespect"], + filter: ["type":"string", "const": "high"] + ), + FieldV2( + path: ["$.issuer"], + optional: false + ) + ] + ) + ) + ] + ) + + // When + + let result = try PresentationExchange.selectCredentials(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd) + + print(result) + // Then + XCTAssertEqual(result, [vcJwt, vcJwt2]) + } +} From ca7e90d16747dbd73ce383e03ce56b9981f2c65d Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 01:28:21 -0800 Subject: [PATCH 03/15] cleanup --- Sources/Web5/Credentials/PresentationExchange.swift | 1 - Tests/Web5Tests/Credentials/PresentationExchangeTests.swift | 5 ----- 2 files changed, 6 deletions(-) diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift index 09aa788..24dfafb 100644 --- a/Sources/Web5/Credentials/PresentationExchange.swift +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -173,7 +173,6 @@ public enum PresentationExchange { ]) return result.valid } - print(satisfiesSchemaMatches) if satisfiesSchemaMatches.isEmpty { // If the field value does not satisfy the schema, the VC does not satisfy this Input Descriptor. diff --git a/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift b/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift index 68d7b6a..15da452 100644 --- a/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift +++ b/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift @@ -5,7 +5,6 @@ import XCTest class PresentationExchangeTests: XCTestCase { func testSelectCredentialsWhenMatchesFound() throws { - // Given let vcJwt = """ eyJraWQiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkjelEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2SyJ9.eyJpc3MiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkiLCJzdWIiOiJkaWQ6a2V5OnpRM3Noa3BhdmpLUmV3b0JrNmFyUEpuaEE4N1p6aExERVdnVnZaS05ISzZRcVZKREIiLCJpYXQiOjE3MDEzMDI1OTMsInZjIjp7Imlzc3VhbmNlRGF0ZSI6IjIwMjMtMTEtMzBUMDA6MDM6MTNaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6a2V5OnpRM3Noa3BhdmpLUmV3b0JrNmFyUEpuaEE4N1p6aExERVdnVnZaS05ISzZRcVZKREIiLCJsb2NhbFJlc3BlY3QiOiJoaWdoIiwibGVnaXQiOnRydWV9LCJpZCI6InVybjp1dWlkOjZjOGJiY2Y0LTg3YWYtNDQ5YS05YmZiLTMwYmYyOTk3NjIyNyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTdHJlZXRDcmVkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlzc3VlciI6ImRpZDprZXk6elEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSJ9fQ.qoqF4-FinFsQ2J-NFSO46xCE8kUTZqZCU5fYr6tS0TQ6VP8y-ZnyR6R3oAqLs_Yo_CqQi23yi38uDjLjksiD2w """ @@ -37,12 +36,8 @@ class PresentationExchangeTests: XCTestCase { ] ) - // When - let result = try PresentationExchange.selectCredentials(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd) - print(result) - // Then XCTAssertEqual(result, [vcJwt, vcJwt2]) } } From 0ae0de313e49258eb4e8a4c849c5061e8e2b4d2a Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 12:50:56 -0800 Subject: [PATCH 04/15] add marks --- Sources/Web5/Credentials/PresentationExchange.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift index 24dfafb..dd70787 100644 --- a/Sources/Web5/Credentials/PresentationExchange.swift +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -89,6 +89,7 @@ public enum Optionality: Codable { public enum PresentationExchange { + // MARK: - Select Credentials public static func selectCredentials( vcJWTs: [String], presentationDefinition: PresentationDefinitionV2 @@ -101,6 +102,7 @@ public enum PresentationExchange { case parseFailed } + // MARK: - Map Input Descriptors to VCs private static func mapInputDescriptorsToVCs( vcJWTList: [String], presentationDefinition: PresentationDefinitionV2 @@ -131,7 +133,8 @@ public enum PresentationExchange { } return result } - + + // MARK: - VC Satisfies Input Descriptor private static func vcSatisfiesInputDescriptor(vc: VCDataModel, inputDescriptor: InputDescriptorV2) throws -> Bool { // If the Input Descriptor has constraints and fields defined, evaluate them. guard let fields = inputDescriptor.constraints.fields else { From 96f36ea05425da62a65a333e9f89966dc9683bf7 Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 13:46:32 -0800 Subject: [PATCH 05/15] Add satisfiesPresentationDefinition --- Sources/Web5/Credentials/PresentationExchange.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift index dd70787..6dc9be2 100644 --- a/Sources/Web5/Credentials/PresentationExchange.swift +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -98,6 +98,17 @@ public enum PresentationExchange { return inputDescriptorToVcMap.flatMap { $0.value } } + // MARK: - Satisfies Presentation Definition + public static func satisfiesPresentationDefinition( + vcJWTs: [String], + presentationDefinition: PresentationDefinitionV2 + ) throws -> Void { + let inputDescriptorToVcMap = try selectCredentials(vcJWTs: vcJWTs, presentationDefinition: presentationDefinition) + guard inputDescriptorToVcMap.count > 0 else { + throw Error.parseFailed + } + } + public enum Error: LocalizedError { case parseFailed } From 06b6d0b6458813013e4e7781580d46f0d21fa0a3 Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 14:34:44 -0800 Subject: [PATCH 06/15] add tests --- .../Credentials/PresentationExchange.swift | 34 +++++--- .../PresentationExchangeTests.swift | 82 ++++++++++++------- 2 files changed, 75 insertions(+), 41 deletions(-) diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift index 6dc9be2..0ad22a4 100644 --- a/Sources/Web5/Credentials/PresentationExchange.swift +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -104,14 +104,11 @@ public enum PresentationExchange { presentationDefinition: PresentationDefinitionV2 ) throws -> Void { let inputDescriptorToVcMap = try selectCredentials(vcJWTs: vcJWTs, presentationDefinition: presentationDefinition) - guard inputDescriptorToVcMap.count > 0 else { - throw Error.parseFailed + guard inputDescriptorToVcMap.count == presentationDefinition.inputDescriptors.count else { + print("this is testing it reaches this line") + throw Error.missingCredentials(presentationDefinition.inputDescriptors.count, inputDescriptorToVcMap.count) } } - - public enum Error: LocalizedError { - case parseFailed - } // MARK: - Map Input Descriptors to VCs private static func mapInputDescriptorsToVCs( @@ -121,7 +118,7 @@ public enum PresentationExchange { let vcJWTListMap: [VCDataModel] = try vcJWTList.map { vcJWT in let parsedJWT = try JWT.parse(jwtString: vcJWT) guard let vcJSON = parsedJWT.payload["vc"]?.value as? [String: Any] else { - throw Error.parseFailed + throw Error.missingCredentialObject } let vcData = try JSONSerialization.data(withJSONObject: vcJSON) @@ -198,9 +195,26 @@ public enum PresentationExchange { // If the VC passes all the checks, it satisfies the criteria of the Input Descriptor. return true } - - private static func validateFilter() { + +} + +// MARK: - Errors +extension PresentationExchange { + public enum Error: LocalizedError { + case missingCredentialObject + case missingCredentials(Int, Int) + public var errorDescription: String? { + switch self { + case .missingCredentialObject: + return "Failed to find Verifiable Credential object in parsed JWT" + case .missingCredentials(let totalNeeded, let actualReceived): + return """ + Missing input descriptors: The presentation definition requires + \(totalNeeded) descriptors, but only + \(actualReceived) were found. Check and provide the missing descriptors. + """ + } + } } - } diff --git a/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift b/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift index 15da452..ebf99f2 100644 --- a/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift +++ b/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift @@ -4,40 +4,60 @@ import XCTest class PresentationExchangeTests: XCTestCase { - func testSelectCredentialsWhenMatchesFound() throws { - let vcJwt = """ - eyJraWQiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkjelEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2SyJ9.eyJpc3MiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkiLCJzdWIiOiJkaWQ6a2V5OnpRM3Noa3BhdmpLUmV3b0JrNmFyUEpuaEE4N1p6aExERVdnVnZaS05ISzZRcVZKREIiLCJpYXQiOjE3MDEzMDI1OTMsInZjIjp7Imlzc3VhbmNlRGF0ZSI6IjIwMjMtMTEtMzBUMDA6MDM6MTNaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6a2V5OnpRM3Noa3BhdmpLUmV3b0JrNmFyUEpuaEE4N1p6aExERVdnVnZaS05ISzZRcVZKREIiLCJsb2NhbFJlc3BlY3QiOiJoaWdoIiwibGVnaXQiOnRydWV9LCJpZCI6InVybjp1dWlkOjZjOGJiY2Y0LTg3YWYtNDQ5YS05YmZiLTMwYmYyOTk3NjIyNyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTdHJlZXRDcmVkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlzc3VlciI6ImRpZDprZXk6elEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSJ9fQ.qoqF4-FinFsQ2J-NFSO46xCE8kUTZqZCU5fYr6tS0TQ6VP8y-ZnyR6R3oAqLs_Yo_CqQi23yi38uDjLjksiD2w - """ - - let vcJwt2 = """ - eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTZLIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCIsInN1YiI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImlhdCI6MTcwMTMwMjU5MywidmMiOnsiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMS0zMFQwMDowMzoxM1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImxvY2FsUmVzcGVjdCI6ImhpZ2giLCJsZWdpdCI6dHJ1ZX0sImlkIjoidXJuOnV1aWQ6NmM4YmJjZjQtODdhZi00NDlhLTliZmItMzBiZjI5OTc2MjI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlN0cmVldENyZWQiXSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSkZReUlzSW5WelpTSTZJbk5wWnlJc0ltTnlkaUk2SW5ObFkzQXlOVFpyTVNJc0ltdHBaQ0k2SWs1Q1gzRnNWVWx5TURZdFFXbHJWbFpOUm5KbGNUQnNZeTF2YlZGMGVtTTJiSGRvYUdOMlowODJjalFpTENKNElqb2lSR1IwVGpoWE5uaGZNelJ3UWw5dWEzaFNNR1V4ZEcxRWEwNXVjMHBsZFhOQ1FVVlFla3RYWjJaZldTSXNJbmtpT2lKMU0xY3hNelZwYm05a1RIRjBZMFZpT1dwT1IxTXpTbk5OWDNWR1MyMXJTbE5pT0ZSeVl6bHNkVmRKSWl3aVlXeG5Jam9pUlZNeU5UWkxJbjAifX0.8AehkiboIK6SZy6LHC9ugy_OcT2VsjluzH4qzsgjfTtq9fEsGyY-cOW_xekNUa2RE2VzlP6FXk0gDn4xf6_r4g - """ - - let pd = PresentationDefinitionV2( - inputDescriptors: [ - InputDescriptorV2( - constraints: ConstraintsV2( - fields: [ - FieldV2( - path: ["$.credentialSubject.legit"], - filter: ["type":"boolean"] - ), - FieldV2( - path: ["$.credentialSubject.localRespect"], - filter: ["type":"string", "const": "high"] - ), - FieldV2( - path: ["$.issuer"], - optional: false - ) - ] - ) + let vcJwt = """ + eyJraWQiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkjelEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSIsInR5cCI6IkpXVCIsImFsZyI6IkVTMjU2SyJ9.eyJpc3MiOiJkaWQ6a2V5OnpRM3NoTkx0MWFNV1BiV1JHYThWb2VFYkpvZko3eEplNEZDUHBES3hxMU5aeWdwaXkiLCJzdWIiOiJkaWQ6a2V5OnpRM3Noa3BhdmpLUmV3b0JrNmFyUEpuaEE4N1p6aExERVdnVnZaS05ISzZRcVZKREIiLCJpYXQiOjE3MDEzMDI1OTMsInZjIjp7Imlzc3VhbmNlRGF0ZSI6IjIwMjMtMTEtMzBUMDA6MDM6MTNaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6a2V5OnpRM3Noa3BhdmpLUmV3b0JrNmFyUEpuaEE4N1p6aExERVdnVnZaS05ISzZRcVZKREIiLCJsb2NhbFJlc3BlY3QiOiJoaWdoIiwibGVnaXQiOnRydWV9LCJpZCI6InVybjp1dWlkOjZjOGJiY2Y0LTg3YWYtNDQ5YS05YmZiLTMwYmYyOTk3NjIyNyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJTdHJlZXRDcmVkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlzc3VlciI6ImRpZDprZXk6elEzc2hOTHQxYU1XUGJXUkdhOFZvZUViSm9mSjd4SmU0RkNQcERLeHExTlp5Z3BpeSJ9fQ.qoqF4-FinFsQ2J-NFSO46xCE8kUTZqZCU5fYr6tS0TQ6VP8y-ZnyR6R3oAqLs_Yo_CqQi23yi38uDjLjksiD2w + """ + + let vcJwt2 = """ + eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTZLIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCIsInN1YiI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImlhdCI6MTcwMTMwMjU5MywidmMiOnsiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMS0zMFQwMDowMzoxM1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImxvY2FsUmVzcGVjdCI6ImhpZ2giLCJsZWdpdCI6dHJ1ZX0sImlkIjoidXJuOnV1aWQ6NmM4YmJjZjQtODdhZi00NDlhLTliZmItMzBiZjI5OTc2MjI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlN0cmVldENyZWQiXSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSkZReUlzSW5WelpTSTZJbk5wWnlJc0ltTnlkaUk2SW5ObFkzQXlOVFpyTVNJc0ltdHBaQ0k2SWs1Q1gzRnNWVWx5TURZdFFXbHJWbFpOUm5KbGNUQnNZeTF2YlZGMGVtTTJiSGRvYUdOMlowODJjalFpTENKNElqb2lSR1IwVGpoWE5uaGZNelJ3UWw5dWEzaFNNR1V4ZEcxRWEwNXVjMHBsZFhOQ1FVVlFla3RYWjJaZldTSXNJbmtpT2lKMU0xY3hNelZwYm05a1RIRjBZMFZpT1dwT1IxTXpTbk5OWDNWR1MyMXJTbE5pT0ZSeVl6bHNkVmRKSWl3aVlXeG5Jam9pUlZNeU5UWkxJbjAifX0.8AehkiboIK6SZy6LHC9ugy_OcT2VsjluzH4qzsgjfTtq9fEsGyY-cOW_xekNUa2RE2VzlP6FXk0gDn4xf6_r4g + """ + + let pd = PresentationDefinitionV2( + inputDescriptors: [ + InputDescriptorV2( + constraints: ConstraintsV2( + fields: [ + FieldV2( + path: ["$.credentialSubject.legit"], + filter: ["type":"boolean"] + ), + FieldV2( + path: ["$.credentialSubject.localRespect"], + filter: ["type":"string", "const": "high"] + ), + FieldV2( + path: ["$.issuer"], + filter: ["type":"string", "const": "did:key:zQ3shNLt1aMWPbWRGa8VoeEbJofJ7xJe4FCPpDKxq1NZygpiy"] + ) + ] + ) + ), + InputDescriptorV2( + constraints: ConstraintsV2( + fields: [ + FieldV2( + path: ["$.credentialSubject.legit"], + filter: ["type":"boolean"] + ), + FieldV2( + path: ["$.credentialSubject.localRespect"], + filter: ["type":"string", "const": "low"] + ), + ] ) - ] - ) + ) + ] + ) + + func testSelectCredentialsWhenMatchesFound() throws { let result = try PresentationExchange.selectCredentials(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd) - XCTAssertEqual(result, [vcJwt, vcJwt2]) + XCTAssertEqual(result, [vcJwt]) + } + + func testSatisfiesPresentationDefinitionWhenDoesNotSatisfy() throws { + + XCTAssertThrowsError(try PresentationExchange.satisfiesPresentationDefinition(vcJWTs: [vcJwt], presentationDefinition: pd)) } } From 1251dd9517579fb8c2af3670b3f3cbcd4eb7587a Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 14:57:47 -0800 Subject: [PATCH 07/15] downgrade sextant version --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 0b8c836..2d59bf9 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( .package(url: "https://github.com/allegro/swift-junit.git", from: "2.1.0"), .package(url: "https://github.com/flight-school/anycodable.git", from: "0.6.7"), .package(url: "https://github.com/Bouke/DNS.git", from: "1.2.0"), - .package(url: "https://github.com/KittyMac/Sextant.git", .upToNextMinor(from: "0.4.0")), + .package(url: "https://github.com/KittyMac/Sextant.git", "0.4.0"..."0.4.30"), .package(url: "https://github.com/kylef/JSONSchema.swift.git", from: "0.6.0") ], targets: [ From 18d8e8925f1c4422c7751564681d6a136e35bba3 Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 15:07:20 -0800 Subject: [PATCH 08/15] bump swift --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 669d3d7..e66439c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - uses: swift-actions/setup-swift@v1 with: - swift-version: "5.9" + swift-version: "5.9.2" - name: Build run: swift build From 01e5e2b30e9cfcdb87093e593e7f699c5f054b1f Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 15:17:00 -0800 Subject: [PATCH 09/15] versions --- .github/workflows/ci.yml | 2 +- Package.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e66439c..669d3d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - uses: swift-actions/setup-swift@v1 with: - swift-version: "5.9.2" + swift-version: "5.9" - name: Build run: swift build diff --git a/Package.swift b/Package.swift index 2d59bf9..62dcc09 100644 --- a/Package.swift +++ b/Package.swift @@ -21,7 +21,7 @@ let package = Package( .package(url: "https://github.com/allegro/swift-junit.git", from: "2.1.0"), .package(url: "https://github.com/flight-school/anycodable.git", from: "0.6.7"), .package(url: "https://github.com/Bouke/DNS.git", from: "1.2.0"), - .package(url: "https://github.com/KittyMac/Sextant.git", "0.4.0"..."0.4.30"), + .package(url: "https://github.com/KittyMac/Sextant.git", exact: "0.4.0"), .package(url: "https://github.com/kylef/JSONSchema.swift.git", from: "0.6.0") ], targets: [ From c72b49359b77a35ee6563d4f35de0e1891a55963 Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 15:54:14 -0800 Subject: [PATCH 10/15] remove print --- Sources/Web5/Credentials/PresentationExchange.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift index 0ad22a4..d80b846 100644 --- a/Sources/Web5/Credentials/PresentationExchange.swift +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -105,7 +105,6 @@ public enum PresentationExchange { ) throws -> Void { let inputDescriptorToVcMap = try selectCredentials(vcJWTs: vcJWTs, presentationDefinition: presentationDefinition) guard inputDescriptorToVcMap.count == presentationDefinition.inputDescriptors.count else { - print("this is testing it reaches this line") throw Error.missingCredentials(presentationDefinition.inputDescriptors.count, inputDescriptorToVcMap.count) } } From 66591a10f87e85ff339a2e30078208578b06311f Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 15:56:51 -0800 Subject: [PATCH 11/15] cleanup import --- Sources/Web5/Common/ISO8601Date.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/Web5/Common/ISO8601Date.swift b/Sources/Web5/Common/ISO8601Date.swift index a978d22..c395bcf 100644 --- a/Sources/Web5/Common/ISO8601Date.swift +++ b/Sources/Web5/Common/ISO8601Date.swift @@ -1,4 +1,3 @@ -import AnyCodable import Foundation /// Wrapper used to easily encode a `Date` to and decode a `Date` from an ISO 8601 formatted date string. From a85eb8a32ab9fbe15ab7504dc6f5ae9f1ce8e440 Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 15:57:28 -0800 Subject: [PATCH 12/15] cleanup --- Sources/Web5/Common/OneOrMany.swift | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Sources/Web5/Common/OneOrMany.swift b/Sources/Web5/Common/OneOrMany.swift index 2c3f037..21fac74 100644 --- a/Sources/Web5/Common/OneOrMany.swift +++ b/Sources/Web5/Common/OneOrMany.swift @@ -33,13 +33,6 @@ public enum OneOrMany: Codable, Equatable { return nil } } - /* - { - thing: "thing", - "grab": { - "thing" : "thing" - } - */ public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() From 55452624bab8b4c18805dfc97557c68f2259e208 Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 16:01:32 -0800 Subject: [PATCH 13/15] other cleanup --- Sources/Web5/Crypto/JOSE/JWT.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Web5/Crypto/JOSE/JWT.swift b/Sources/Web5/Crypto/JOSE/JWT.swift index eb25d29..f50b848 100644 --- a/Sources/Web5/Crypto/JOSE/JWT.swift +++ b/Sources/Web5/Crypto/JOSE/JWT.swift @@ -125,7 +125,7 @@ public struct JWT { throw Error.verificationFailed("Expected JWT header to contain typ property set to JWT") } - guard let _ = jwtHeader.keyID else { + guard jwtHeader.keyID != nil else { throw Error.verificationFailed("Expected JWT header to contain kid") } From c6061cdec279f8f9bbe96dd501ae390833cf7c3c Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Fri, 8 Mar 2024 16:46:20 -0800 Subject: [PATCH 14/15] change error name --- Sources/Web5/Credentials/PresentationExchange.swift | 6 +++--- Sources/Web5/Crypto/JOSE/JWT.swift | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift index d80b846..070f9bb 100644 --- a/Sources/Web5/Credentials/PresentationExchange.swift +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -105,7 +105,7 @@ public enum PresentationExchange { ) throws -> Void { let inputDescriptorToVcMap = try selectCredentials(vcJWTs: vcJWTs, presentationDefinition: presentationDefinition) guard inputDescriptorToVcMap.count == presentationDefinition.inputDescriptors.count else { - throw Error.missingCredentials(presentationDefinition.inputDescriptors.count, inputDescriptorToVcMap.count) + throw Error.missingDescriptors(presentationDefinition.inputDescriptors.count, inputDescriptorToVcMap.count) } } @@ -201,13 +201,13 @@ public enum PresentationExchange { extension PresentationExchange { public enum Error: LocalizedError { case missingCredentialObject - case missingCredentials(Int, Int) + case missingDescriptors(Int, Int) public var errorDescription: String? { switch self { case .missingCredentialObject: return "Failed to find Verifiable Credential object in parsed JWT" - case .missingCredentials(let totalNeeded, let actualReceived): + case .missingDescriptors(let totalNeeded, let actualReceived): return """ Missing input descriptors: The presentation definition requires \(totalNeeded) descriptors, but only diff --git a/Sources/Web5/Crypto/JOSE/JWT.swift b/Sources/Web5/Crypto/JOSE/JWT.swift index f50b848..57f4efa 100644 --- a/Sources/Web5/Crypto/JOSE/JWT.swift +++ b/Sources/Web5/Crypto/JOSE/JWT.swift @@ -114,7 +114,6 @@ public struct JWT { let base64urlEncodedJwtHeader = String(parts[0]) let base64urlEncodedJwtPayload = String(parts[1]) - let _ = String(parts[2]) let jwtHeader: JWS.Header = try JSONDecoder().decode( JWS.Header.self, From bb7090efeb6a055fbb09628c90ea23d24d83c36b Mon Sep 17 00:00:00 2001 From: Kirah Sapong Date: Mon, 18 Mar 2024 15:08:20 -0700 Subject: [PATCH 15/15] add web5-spec vector test for select_credentials --- .../Credentials/PresentationExchange.swift | 8 +- .../Web5TestVectorsPresentationExchange.swift | 56 ++++++++++ .../PresentationExchangeTests.swift | 105 ++++++++++++------ 3 files changed, 130 insertions(+), 39 deletions(-) create mode 100644 Tests/Web5TestVectors/Web5TestVectorsPresentationExchange.swift diff --git a/Sources/Web5/Credentials/PresentationExchange.swift b/Sources/Web5/Credentials/PresentationExchange.swift index 070f9bb..c2cf75b 100644 --- a/Sources/Web5/Credentials/PresentationExchange.swift +++ b/Sources/Web5/Credentials/PresentationExchange.swift @@ -44,6 +44,11 @@ public struct CredentialSchema: Codable { public struct PresentationDefinitionV2: Codable { public let inputDescriptors: [InputDescriptorV2] + + enum CodingKeys: String, CodingKey { + case inputDescriptors = "input_descriptors" + } + } public struct InputDescriptorV2: Codable, Hashable { @@ -95,7 +100,7 @@ public enum PresentationExchange { presentationDefinition: PresentationDefinitionV2 ) throws -> [String] { let inputDescriptorToVcMap = try mapInputDescriptorsToVCs(vcJWTList: vcJWTs, presentationDefinition: presentationDefinition) - return inputDescriptorToVcMap.flatMap { $0.value } + return Array(Set(inputDescriptorToVcMap.flatMap { $0.value })) } // MARK: - Satisfies Presentation Definition @@ -157,7 +162,6 @@ public enum PresentationExchange { let vcJson = try JSONEncoder().encode(vc) guard let matchedPathValues = vcJson.query(values: field.path) else { return false } - if matchedPathValues.isEmpty { // If no matching fields are found for a required field, the VC does not satisfy this Input Descriptor. return false diff --git a/Tests/Web5TestVectors/Web5TestVectorsPresentationExchange.swift b/Tests/Web5TestVectors/Web5TestVectorsPresentationExchange.swift new file mode 100644 index 0000000..abffde0 --- /dev/null +++ b/Tests/Web5TestVectors/Web5TestVectorsPresentationExchange.swift @@ -0,0 +1,56 @@ +import CustomDump +import Mocker +import XCTest + +@testable import Web5 + +final class Web5TestVectorsPresentationExchange: XCTestCase { + + func test_resolve() throws { + struct Input: Codable { + let presentationDefinition: PresentationDefinitionV2 + let credentialJwts: [String] + let mockServer: [String: [String: String]]? + + func mocks() throws -> [Mock] { + guard let mockServer = mockServer else { return [] } + + return try mockServer.map({ key, value in + return Mock( + url: URL(string: key)!, + contentType: .json, + statusCode: 200, + data: [ + .get: try JSONEncoder().encode(value) + ] + ) + }) + } + } + + struct Output: Codable { + let selectedCredentials: [String] + } + + let testVector = try TestVector( + fileName: "select_credentials", + subdirectory: "test-vectors/presentation_exchange" + ) + + testVector.run { vector in + let expectation = XCTestExpectation(description: "async resolve") + Task { + /// Register each of the mock network responses + try vector.input.mocks().forEach { $0.register() } + + /// Resolve each input didURI, make sure it matches output + let result = try PresentationExchange.selectCredentials(vcJWTs: vector.input.credentialJwts, presentationDefinition: vector.input.presentationDefinition) + XCTAssertEqual(result.sorted(), vector.output!.selectedCredentials.sorted()) + expectation.fulfill() + } + + wait(for: [expectation], timeout: 1) + } + } + +} diff --git a/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift b/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift index ebf99f2..3fda4c4 100644 --- a/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift +++ b/Tests/Web5Tests/Credentials/PresentationExchangeTests.swift @@ -12,52 +12,83 @@ class PresentationExchangeTests: XCTestCase { eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTZLIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCIsInN1YiI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImlhdCI6MTcwMTMwMjU5MywidmMiOnsiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMS0zMFQwMDowMzoxM1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImxvY2FsUmVzcGVjdCI6ImhpZ2giLCJsZWdpdCI6dHJ1ZX0sImlkIjoidXJuOnV1aWQ6NmM4YmJjZjQtODdhZi00NDlhLTliZmItMzBiZjI5OTc2MjI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlN0cmVldENyZWQiXSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSkZReUlzSW5WelpTSTZJbk5wWnlJc0ltTnlkaUk2SW5ObFkzQXlOVFpyTVNJc0ltdHBaQ0k2SWs1Q1gzRnNWVWx5TURZdFFXbHJWbFpOUm5KbGNUQnNZeTF2YlZGMGVtTTJiSGRvYUdOMlowODJjalFpTENKNElqb2lSR1IwVGpoWE5uaGZNelJ3UWw5dWEzaFNNR1V4ZEcxRWEwNXVjMHBsZFhOQ1FVVlFla3RYWjJaZldTSXNJbmtpT2lKMU0xY3hNelZwYm05a1RIRjBZMFZpT1dwT1IxTXpTbk5OWDNWR1MyMXJTbE5pT0ZSeVl6bHNkVmRKSWl3aVlXeG5Jam9pUlZNeU5UWkxJbjAifX0.8AehkiboIK6SZy6LHC9ugy_OcT2VsjluzH4qzsgjfTtq9fEsGyY-cOW_xekNUa2RE2VzlP6FXk0gDn4xf6_r4g """ - let pd = PresentationDefinitionV2( - inputDescriptors: [ - InputDescriptorV2( - constraints: ConstraintsV2( - fields: [ - FieldV2( - path: ["$.credentialSubject.legit"], - filter: ["type":"boolean"] - ), - FieldV2( - path: ["$.credentialSubject.localRespect"], - filter: ["type":"string", "const": "high"] - ), - FieldV2( - path: ["$.issuer"], - filter: ["type":"string", "const": "did:key:zQ3shNLt1aMWPbWRGa8VoeEbJofJ7xJe4FCPpDKxq1NZygpiy"] - ) - ] + let inputDescriptor = InputDescriptorV2( + constraints: ConstraintsV2( + fields: [ + FieldV2( + path: ["$.credentialSubject.legit"], + filter: ["type":"boolean"] + ), + FieldV2( + path: ["$.credentialSubject.localRespect"], + filter: ["type":"string", "const": "high"] + ), + FieldV2( + path: ["$.issuer"], + filter: ["type":"string", "const": "did:key:zQ3shNLt1aMWPbWRGa8VoeEbJofJ7xJe4FCPpDKxq1NZygpiy"] ) - ), - InputDescriptorV2( - constraints: ConstraintsV2( - fields: [ - FieldV2( - path: ["$.credentialSubject.legit"], - filter: ["type":"boolean"] - ), - FieldV2( - path: ["$.credentialSubject.localRespect"], - filter: ["type":"string", "const": "low"] - ), - ] - ) - ) - ] + ] + ) + ) + + let inputDescriptor2 = InputDescriptorV2( + constraints: ConstraintsV2( + fields: [ + FieldV2( + path: ["$.credentialSubject.legit"], + filter: ["type":"boolean"] + ), + FieldV2( + path: ["$.credentialSubject.localStreet"], + filter: ["type":"string", "const": "low"] + ), + ] + ) ) - func testSelectCredentialsWhenMatchesFound() throws { + let inputDescriptor3 = InputDescriptorV2( + constraints: ConstraintsV2( + fields: [ + FieldV2( + path: ["$.credentialSubject.legit"], + filter: ["type":"boolean"] + ), + FieldV2( + path: ["$.credentialSubject.localRespect"], + filter: ["type":"string", "const": "high"] + ), + ] + ) + ) + //select_credentials - web5-spec select_credentials -> Web5TestVectors folder + + func test_select_oneOfTwoCorrectCredentials() throws { + let pd = PresentationDefinitionV2( + inputDescriptors: [ + inputDescriptor, inputDescriptor2 + ] + ) let result = try PresentationExchange.selectCredentials(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd) XCTAssertEqual(result, [vcJwt]) } - func testSatisfiesPresentationDefinitionWhenDoesNotSatisfy() throws { - + func test_throw_zeroCorrectCredentials() throws { + let pd = PresentationDefinitionV2( + inputDescriptors: [ + inputDescriptor, inputDescriptor2 + ] + ) XCTAssertThrowsError(try PresentationExchange.satisfiesPresentationDefinition(vcJWTs: [vcJwt], presentationDefinition: pd)) } + + func test_select_twoOfTwoCorrectCredentials() throws { + let pd = PresentationDefinitionV2( + inputDescriptors: [ + inputDescriptor3 + ] + ) + XCTAssertThrowsError(try PresentationExchange.satisfiesPresentationDefinition(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd)) + } }