Skip to content

Commit 8e83a43

Browse files
authored
Feat/add presentation submission (#48)
* add Create Presentation Submission from Credentials * add vector test * add unit tests and update prev tests * bump spec
1 parent 5208605 commit 8e83a43

File tree

4 files changed

+207
-12
lines changed

4 files changed

+207
-12
lines changed

Sources/Web5/Credentials/PresentationExchange.swift

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,30 @@ public enum Optionality: Codable {
111111
case preferred
112112
}
113113

114+
public struct PresentationSubmission: Codable, Equatable {
115+
public let id: String
116+
public let definitionID: String
117+
public let descriptorMap: [InputDescriptorMapping]
118+
119+
enum CodingKeys: String, CodingKey {
120+
case id
121+
case definitionID = "definition_id"
122+
case descriptorMap = "descriptor_map"
123+
}
124+
}
125+
126+
public struct InputDescriptorMapping: Codable, Hashable {
127+
public let id: String
128+
public let format: String
129+
public let path: String
130+
131+
enum CodingKeys: String, CodingKey {
132+
case id
133+
case format
134+
case path
135+
}
136+
}
137+
114138
public enum PresentationExchange {
115139

116140
// MARK: - Select Credentials
@@ -128,11 +152,59 @@ public enum PresentationExchange {
128152
presentationDefinition: PresentationDefinitionV2
129153
) throws -> Void {
130154
let inputDescriptorToVcMap = try mapInputDescriptorsToVCs(vcJWTList: vcJWTs, presentationDefinition: presentationDefinition)
131-
let inputDescriptorToVcFlatMap = inputDescriptorToVcMap.flatMap { $0.value }
132-
guard inputDescriptorToVcFlatMap.count == presentationDefinition.inputDescriptors.count else {
155+
156+
guard inputDescriptorToVcMap.count == presentationDefinition.inputDescriptors.count else {
133157
throw Error.missingDescriptors(presentationDefinition.inputDescriptors.count, inputDescriptorToVcMap.count)
134158
}
135159
}
160+
161+
// MARK: - Create Presentation From Credentials
162+
public static func createPresentationFromCredentials(
163+
vcJWTs: [String],
164+
presentationDefinition: PresentationDefinitionV2
165+
) throws -> PresentationSubmission {
166+
// Make sure VCs satisfy the PD. Note: VCs should be result from `selectCredentials`
167+
do {
168+
try satisfiesPresentationDefinition(
169+
vcJWTs: vcJWTs,
170+
presentationDefinition: presentationDefinition
171+
)
172+
} catch {
173+
throw Error.reason("""
174+
Credentials do not satisfy the provided PresentationDefinition.
175+
Use `PresentationExchange.selectCredentials` and pass in the result to this method's `vcJWTs` argument.
176+
"""
177+
)
178+
}
179+
180+
var descriptorMapList: [InputDescriptorMapping] = []
181+
182+
// Get our inputDescriptor to VC jwt map
183+
let inputDescriptorToVcMap = try mapInputDescriptorsToVCs(vcJWTList: vcJWTs, presentationDefinition: presentationDefinition)
184+
185+
// Iterate through our inputDescriptors
186+
for (inputDescriptor, vcMatches) in inputDescriptorToVcMap {
187+
// Take the first match and get index
188+
if let matchingIndex = vcJWTs.firstIndex(of: vcMatches[0]) {
189+
descriptorMapList.append(
190+
InputDescriptorMapping(
191+
id: inputDescriptor.id,
192+
format: "jwt_vc",
193+
path: "$.verifiableCredential[\(matchingIndex)]"
194+
)
195+
)
196+
} else {
197+
print("No matching JWT found")
198+
}
199+
200+
}
201+
202+
return PresentationSubmission(
203+
id: UUID().uuidString,
204+
definitionID: presentationDefinition.id,
205+
descriptorMap: descriptorMapList
206+
)
207+
}
136208

137209
// MARK: - Map Input Descriptors to VCs
138210
private static func mapInputDescriptorsToVCs(
@@ -141,7 +213,7 @@ public enum PresentationExchange {
141213
) throws -> [InputDescriptorV2: [String]] {
142214
let vcJWTListMap: [VCDataModel] = try vcJWTList.map { vcJWT in
143215
let parsedJWT = try JWT.parse(jwtString: vcJWT)
144-
guard let vcJSON = parsedJWT.payload.miscellaneous?["vc"]?.value as? [String: Any] else {
216+
guard let vcJSON = parsedJWT.payload.miscellaneous?["vc"]?.value as? [String: Any] else {
145217
throw Error.missingCredentialObject
146218
}
147219

@@ -226,6 +298,7 @@ extension PresentationExchange {
226298
public enum Error: LocalizedError {
227299
case missingCredentialObject
228300
case missingDescriptors(Int, Int)
301+
case reason(String)
229302

230303
public var errorDescription: String? {
231304
switch self {
@@ -237,6 +310,8 @@ extension PresentationExchange {
237310
\(totalNeeded) descriptors, but only
238311
\(actualReceived) were found. Check and provide the missing descriptors.
239312
"""
313+
case .reason(let reason):
314+
return "Error: \(reason)"
240315
}
241316
}
242317
}

Tests/Web5TestVectors/Web5TestVectorsPresentationExchange.swift

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import XCTest
66

77
final class Web5TestVectorsPresentationExchange: XCTestCase {
88

9-
func test_resolve() throws {
9+
func test_selectCredentials() throws {
1010
struct Input: Codable {
1111
let presentationDefinition: PresentationDefinitionV2
1212
let credentialJwts: [String]
@@ -52,5 +52,56 @@ final class Web5TestVectorsPresentationExchange: XCTestCase {
5252
wait(for: [expectation], timeout: 1)
5353
}
5454
}
55+
56+
func test_createPresentationFromDefinition() throws {
57+
struct Input: Codable {
58+
let presentationDefinition: PresentationDefinitionV2
59+
let credentialJwts: [String]
60+
let mockServer: [String: [String: String]]?
61+
62+
func mocks() throws -> [Mock] {
63+
guard let mockServer = mockServer else { return [] }
64+
65+
return try mockServer.map({ key, value in
66+
return Mock(
67+
url: URL(string: key)!,
68+
contentType: .json,
69+
statusCode: 200,
70+
data: [
71+
.get: try JSONEncoder().encode(value)
72+
]
73+
)
74+
})
75+
}
76+
}
77+
78+
struct Output: Codable {
79+
let presentationSubmission: PresentationSubmission
80+
}
81+
82+
let testVector = try TestVector<Input, Output>(
83+
fileName: "create_presentation_from_credentials",
84+
subdirectory: "test-vectors/presentation_exchange"
85+
)
86+
87+
testVector.run { vector in
88+
let expectation = XCTestExpectation(description: "async resolve")
89+
Task {
90+
/// Register each of the mock network responses
91+
try vector.input.mocks().forEach { $0.register() }
92+
93+
/// Select valid credentials from each of the inputs
94+
let credentials = try PresentationExchange.selectCredentials(vcJWTs: vector.input.credentialJwts, presentationDefinition: vector.input.presentationDefinition)
95+
96+
/// Create a presentation submission from the selected credentials and make sure it matches the output
97+
let result = try PresentationExchange.createPresentationFromCredentials(vcJWTs: credentials, presentationDefinition: vector.input.presentationDefinition)
98+
XCTAssertEqual(result.definitionID, vector.output!.presentationSubmission.definitionID)
99+
XCTAssertEqual(result.descriptorMap, vector.output!.presentationSubmission.descriptorMap)
100+
expectation.fulfill()
101+
}
102+
103+
wait(for: [expectation], timeout: 1)
104+
}
105+
}
55106

56107
}

Tests/Web5TestVectors/web5-spec

Tests/Web5Tests/Credentials/PresentationExchangeTests.swift

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class PresentationExchangeTests: XCTestCase {
1212
eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTZLIn0.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKRlF5SXNJblZ6WlNJNkluTnBaeUlzSW1OeWRpSTZJbk5sWTNBeU5UWnJNU0lzSW10cFpDSTZJazVDWDNGc1ZVbHlNRFl0UVdsclZsWk5SbkpsY1RCc1l5MXZiVkYwZW1NMmJIZG9hR04yWjA4MmNqUWlMQ0o0SWpvaVJHUjBUamhYTm5oZk16UndRbDl1YTNoU01HVXhkRzFFYTA1dWMwcGxkWE5DUVVWUWVrdFhaMlpmV1NJc0lua2lPaUoxTTFjeE16VnBibTlrVEhGMFkwVmlPV3BPUjFNelNuTk5YM1ZHUzIxclNsTmlPRlJ5WXpsc2RWZEpJaXdpWVd4bklqb2lSVk15TlRaTEluMCIsInN1YiI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImlhdCI6MTcwMTMwMjU5MywidmMiOnsiaXNzdWFuY2VEYXRlIjoiMjAyMy0xMS0zMFQwMDowMzoxM1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6elEzc2hrcGF2aktSZXdvQms2YXJQSm5oQTg3WnpoTERFV2dWdlpLTkhLNlFxVkpEQiIsImxvY2FsUmVzcGVjdCI6ImhpZ2giLCJsZWdpdCI6dHJ1ZX0sImlkIjoidXJuOnV1aWQ6NmM4YmJjZjQtODdhZi00NDlhLTliZmItMzBiZjI5OTc2MjI3IiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlN0cmVldENyZWQiXSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiXSwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSkZReUlzSW5WelpTSTZJbk5wWnlJc0ltTnlkaUk2SW5ObFkzQXlOVFpyTVNJc0ltdHBaQ0k2SWs1Q1gzRnNWVWx5TURZdFFXbHJWbFpOUm5KbGNUQnNZeTF2YlZGMGVtTTJiSGRvYUdOMlowODJjalFpTENKNElqb2lSR1IwVGpoWE5uaGZNelJ3UWw5dWEzaFNNR1V4ZEcxRWEwNXVjMHBsZFhOQ1FVVlFla3RYWjJaZldTSXNJbmtpT2lKMU0xY3hNelZwYm05a1RIRjBZMFZpT1dwT1IxTXpTbk5OWDNWR1MyMXJTbE5pT0ZSeVl6bHNkVmRKSWl3aVlXeG5Jam9pUlZNeU5UWkxJbjAifX0.8AehkiboIK6SZy6LHC9ugy_OcT2VsjluzH4qzsgjfTtq9fEsGyY-cOW_xekNUa2RE2VzlP6FXk0gDn4xf6_r4g
1313
"""
1414

15+
// vcJwt satisfies this
1516
let inputDescriptor = InputDescriptorV2(
1617
id: "1234567890_a",
1718
name: nil,
@@ -36,6 +37,7 @@ class PresentationExchangeTests: XCTestCase {
3637
)
3738
)
3839

40+
// no creds satisfy this
3941
let inputDescriptor2 = InputDescriptorV2(
4042
id: "1234567890_b",
4143
name: nil,
@@ -56,6 +58,7 @@ class PresentationExchangeTests: XCTestCase {
5658
)
5759
)
5860

61+
// vcJwt and vcJwt2 both satisfy this
5962
let inputDescriptor3 = InputDescriptorV2(
6063
id: "1234567890_c",
6164
name: nil,
@@ -76,7 +79,7 @@ class PresentationExchangeTests: XCTestCase {
7679
)
7780
)
7881

79-
func test_select_oneOfTwoCorrectCredentials() throws {
82+
func test_selectOneCorrectCredential() throws {
8083
let pd = PresentationDefinitionV2(
8184
id: "1234567890_d",
8285
name: nil,
@@ -89,34 +92,100 @@ class PresentationExchangeTests: XCTestCase {
8992
)
9093
let result = try PresentationExchange.selectCredentials(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd)
9194

92-
XCTAssertEqual(result, [vcJwt])
95+
XCTAssertEqual(result.sorted(), [vcJwt])
9396
}
9497

95-
func test_throw_zeroCorrectCredentials() throws {
98+
func test_selectTwoCorrectCredentials() throws {
9699
let pd = PresentationDefinitionV2(
97100
id: "1234567890_e",
98101
name: nil,
99102
purpose: nil,
100103
format: nil,
101104
submissionRequirements: nil,
105+
inputDescriptors: [
106+
inputDescriptor, inputDescriptor3
107+
]
108+
)
109+
let result = try PresentationExchange.selectCredentials(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd)
110+
111+
XCTAssertEqual(result.sorted(), [vcJwt, vcJwt2])
112+
}
113+
114+
func test_selectNoCorrectCredentials() throws {
115+
let pd = PresentationDefinitionV2(
116+
id: "1234567890_f",
117+
name: nil,
118+
purpose: nil,
119+
format: nil,
120+
submissionRequirements: nil,
121+
inputDescriptors: [
122+
inputDescriptor2
123+
]
124+
)
125+
let result = try PresentationExchange.selectCredentials(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd)
126+
127+
XCTAssertEqual(result.sorted(), [])
128+
}
129+
130+
func test_throwOnInsufficientCorrectCredentials() throws {
131+
let pd = PresentationDefinitionV2(
132+
id: "1234567890_g",
133+
name: nil,
134+
purpose: nil,
135+
format: nil,
136+
submissionRequirements: nil,
102137
inputDescriptors: [
103138
inputDescriptor, inputDescriptor2
104139
]
105140
)
106141
XCTAssertThrowsError(try PresentationExchange.satisfiesPresentationDefinition(vcJWTs: [vcJwt], presentationDefinition: pd))
107142
}
108143

109-
func test_select_twoOfTwoCorrectCredentials() throws {
144+
func test_noThrowOnSufficientCorrectCredentials() throws {
110145
let pd = PresentationDefinitionV2(
111-
id: "1234567890_f",
146+
id: "1234567890_h",
147+
name: nil,
148+
purpose: nil,
149+
format: nil,
150+
submissionRequirements: nil,
151+
inputDescriptors: [
152+
inputDescriptor, inputDescriptor3
153+
]
154+
)
155+
XCTAssertNoThrow(try PresentationExchange.satisfiesPresentationDefinition(vcJWTs: [vcJwt], presentationDefinition: pd))
156+
}
157+
158+
func test_createPresentationFromCredentials() throws {
159+
let pd = PresentationDefinitionV2(
160+
id: "1234567890_e",
112161
name: nil,
113162
purpose: nil,
114163
format: nil,
115164
submissionRequirements: nil,
116165
inputDescriptors: [
117-
inputDescriptor3
166+
inputDescriptor, inputDescriptor3
118167
]
119168
)
120-
XCTAssertThrowsError(try PresentationExchange.satisfiesPresentationDefinition(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd))
169+
let credentials = try PresentationExchange.selectCredentials(vcJWTs: [vcJwt, vcJwt2], presentationDefinition: pd)
170+
let submission = try PresentationExchange.createPresentationFromCredentials(vcJWTs: credentials, presentationDefinition: pd)
171+
XCTAssertNotNil(submission.id)
172+
XCTAssertEqual(submission.definitionID, pd.id)
173+
XCTAssertEqual(submission.descriptorMap.count, 2)
174+
XCTAssertEqual(submission.descriptorMap[0].path, "$.verifiableCredential[0]")
175+
}
176+
177+
func test_throwsOnCreatePresentationFromInvalidCredentials() throws {
178+
let pd = PresentationDefinitionV2(
179+
id: "1234567890_e",
180+
name: nil,
181+
purpose: nil,
182+
format: nil,
183+
submissionRequirements: nil,
184+
inputDescriptors: [
185+
inputDescriptor, inputDescriptor3
186+
]
187+
)
188+
189+
XCTAssertThrowsError(try PresentationExchange.createPresentationFromCredentials(vcJWTs: [vcJwt2], presentationDefinition: pd))
121190
}
122191
}

0 commit comments

Comments
 (0)