Skip to content

Commit ff403aa

Browse files
feat(pollux): add sdjwt verifier flow
Now the SDK can as a verifier request presentation of a sdjwt and verify all the claims. Fixes ATL-6865 Signed-off-by: goncalo-frade-iohk <[email protected]>
1 parent 3ec399a commit ff403aa

25 files changed

+663
-68
lines changed

EdgeAgentSDK/Domain/Sources/Models/Errors.swift

+14
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,12 @@ public enum PolluxError: KnownPrismError {
793793
internalErrors: [Error]
794794
)
795795

796+
/// An error case indicating that a credential cannot be verified..
797+
case cannotVerifyCredential(
798+
credential: String? = nil,
799+
internalErrors: [Error]
800+
)
801+
796802
/// An error case indicating that a specified input path was not found.
797803
case inputPathNotFound(path: String)
798804

@@ -874,6 +880,8 @@ public enum PolluxError: KnownPrismError {
874880
return 78
875881
case .credentialIsSuspended:
876882
return 79
883+
case .cannotVerifyCredential:
884+
return 80
877885
}
878886
}
879887

@@ -960,6 +968,12 @@ Cannot verify input descriptor field \(name.map { "with name: \($0)"} ?? ""), wi
960968

961969
case .credentialIsSuspended(let jwtString):
962970
return "Credential (\(jwtString)) is suspended"
971+
case .cannotVerifyCredential(let credential, let fieldErrors):
972+
let errors = fieldErrors.map { " - \(errorMessage($0))" }.joined(separator: "\n")
973+
return
974+
"""
975+
Cannot verify credential: \(credential.map { "with name: \($0)"} ?? ""), with errors: \n \(errors)
976+
"""
963977
}
964978
}
965979
}

EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Proof.swift

+2-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ public extension EdgeAgent {
7171
request: request.makeMessage(),
7272
options: [
7373
.exportableKey(exporting),
74-
.subjectDID(subjectDID)
74+
.subjectDID(subjectDID),
75+
.disclosingClaims(claims: credential.claims.map(\.key))
7576
]
7677
)
7778
default:

EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift

+64
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Builders
22
import Core
33
import Domain
4+
import eudi_lib_sdjwt_swift
45
import Logging
56
import JSONWebSignature
67
import JSONWebToken
@@ -99,6 +100,44 @@ final class PresentationExchangeFlowTests: XCTestCase {
99100
}
100101
}
101102

103+
func testSDJWTPresentationRequest() async throws {
104+
let prismDID = try await edgeAgent.createNewPrismDID()
105+
let subjectDID = try await edgeAgent.createNewPrismDID()
106+
107+
let sdjwt = try await makeCredentialSDJWT(issuerDID: prismDID, subjectDID: subjectDID)
108+
let credential = try SDJWTCredential(sdjwtString: sdjwt)
109+
110+
logger.info("Creating presentation request")
111+
let message = try await edgeAgent.initiatePresentationRequest(
112+
type: .jwt,
113+
fromDID: DID(method: "test", methodId: "alice"),
114+
toDID: DID(method: "test", methodId: "bob"),
115+
claimFilters: [
116+
.init(
117+
paths: ["$.vc.credentialSubject.test"],
118+
type: "string",
119+
required: true,
120+
pattern: "aliceTest"
121+
)
122+
]
123+
)
124+
125+
try await edgeAgent.pluto.storeMessage(message: message.makeMessage(), direction: .sent).first().await()
126+
127+
let presentation = try await edgeAgent.createPresentationForRequestProof(
128+
request: message,
129+
credential: credential
130+
)
131+
132+
let verification = try await edgeAgent.pollux.verifyPresentation(
133+
message: presentation.makeMessage(),
134+
options: []
135+
)
136+
137+
logger.info(verification ? "Verification was successful" : "Verification failed")
138+
XCTAssertTrue(verification)
139+
}
140+
102141
private func makeCredentialJWT(issuerDID: DID, subjectDID: DID) async throws -> String {
103142
let payload = MockCredentialClaim(
104143
iss: issuerDID.string,
@@ -121,6 +160,31 @@ final class PresentationExchangeFlowTests: XCTestCase {
121160
}
122161
return try JWT.signed(payload: payload, protectedHeader: jwsHeader, key: jwkD.toJoseJWK()).jwtString
123162
}
163+
164+
private func makeCredentialSDJWT(issuerDID: DID, subjectDID: DID) async throws -> String {
165+
guard
166+
let key = try await edgeAgent.pluto.getDIDPrivateKeys(did: issuerDID).first().await()?.first,
167+
let jwkD = try await edgeAgent.apollo.restorePrivateKey(key).exporting?.jwk
168+
else {
169+
XCTFail()
170+
fatalError()
171+
}
172+
173+
let sdjwt = try SDJWTIssuer.issue(
174+
issuersPrivateKey: try jwkD.toJoseJWK(),
175+
header: DefaultJWSHeaderImpl(algorithm: .ES256K)
176+
) {
177+
ConstantClaims.iss(domain: issuerDID.string)
178+
ConstantClaims.sub(subject: subjectDID.string)
179+
ObjectClaim("vc") {
180+
ObjectClaim("credentialSubject") {
181+
FlatDisclosedClaim("test", "aliceTest")
182+
}
183+
}
184+
}
185+
186+
return CompactSerialiser(signedSDJWT: sdjwt).serialised
187+
}
124188
}
125189

126190
private struct MockCredentialClaim: JWTRegisteredFieldsClaims, Codable {

EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ extension JWTCredential: ProvableCredential {
3333
let requestData = try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: jsonData)
3434
let payload: Data = try JWT.getPayload(jwtString: jwtString)
3535
do {
36-
try VerifyPresentationSubmission.verifyPresentationSubmissionClaims(
36+
try VerifyPresentationSubmissionJWT.verifyPresentationSubmissionClaims(
3737
request: requestData.presentationDefinition, credentials: [payload]
3838
)
3939
return true

EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@ struct JWTPresentation {
127127
PresentationSubmission.Descriptor(
128128
id: $0.id,
129129
path: "$.verifiable_credential[0]",
130-
format: "jwt_vp",
130+
format: "jwt",
131131
pathNested: .init(
132132
id: $0.id,
133133
path: "$.vp.verifiableCredential[0]",
134-
format: "jwt_vc"
134+
format: "jwt"
135135
)
136136
)
137137
}

EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/PresentationDefinition.swift

+2
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ public struct PresentationDefinition: Codable {
8888
public var ldpVp: LDPFormat?
8989
/// Generic LDP format.
9090
public var ldp: LDPFormat?
91+
/// Generic SDJWT format..
92+
public var sdJwt: JWTFormat?
9193
}
9294

9395
/// Unique identifier for the presentation definition.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Foundation
2+
3+
public protocol PresentationExchangeClaimVerifier {
4+
func verifyClaim(inputDescriptor: InputDescriptor) throws
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import Foundation
2+
3+
public protocol SubmissionDescriptorFormatParser {
4+
var format: String { get }
5+
func parse(path: String, presentationData: Data) async throws -> String
6+
func parsePayload(path: String, presentationData: Data) async throws -> Data
7+
func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier
8+
}

EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT.swift

+6
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,9 @@ extension SDJWTCredential: Credential {
5454
return "sd-jwt"
5555
}
5656
}
57+
58+
extension SDJWTCredential {
59+
func getAlg() throws -> String? {
60+
return sdjwt.jwt.protectedHeader.algorithm?.rawValue
61+
}
62+
}

EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift

+60
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Core
12
import Domain
23
import eudi_lib_sdjwt_swift
34
import Foundation
@@ -50,6 +51,13 @@ struct SDJWTPresentation {
5051
}
5152

5253
switch attachment.format {
54+
case "dif/presentation-exchange/[email protected]":
55+
return try presentation(
56+
credential: credential,
57+
request: requestData,
58+
disclosingClaims: disclosingClaims,
59+
key: exportableKey
60+
)
5361
default:
5462
return try vcPresentation(
5563
credential: credential,
@@ -60,6 +68,58 @@ struct SDJWTPresentation {
6068
}
6169
}
6270

71+
private func presentation(
72+
credential: SDJWTCredential,
73+
request: Data,
74+
disclosingClaims: [String],
75+
key: ExportableKey
76+
) throws -> String {
77+
let presentationRequest = try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: request)
78+
79+
guard
80+
let jwtFormat = presentationRequest.presentationDefinition.format?.sdJwt,
81+
try jwtFormat.supportedTypes.contains(where: { try $0 == credential.getAlg() })
82+
else {
83+
throw PolluxError.credentialIsNotOfPresentationDefinitionRequiredAlgorithm
84+
}
85+
86+
let credentialSubject = try credential.sdjwt.recreateClaims().recreatedClaims.rawData()
87+
88+
try presentationRequest.presentationDefinition.inputDescriptors.forEach {
89+
try $0.constraints.fields.forEach {
90+
guard credentialSubject.query(values: $0.path) != nil else {
91+
throw PolluxError.credentialDoesntProvideOneOrMoreInputDescriptors(path: $0.path)
92+
}
93+
}
94+
}
95+
let presentationDefinitions = presentationRequest.presentationDefinition.inputDescriptors.map {
96+
PresentationSubmission.Descriptor(
97+
id: $0.id,
98+
path: "$.verifiable_credential[0]",
99+
format: "sd_jwt"
100+
)
101+
}
102+
103+
let presentationSubmission = PresentationSubmission(
104+
definitionId: presentationRequest.presentationDefinition.id,
105+
descriptorMap: presentationDefinitions
106+
)
107+
108+
let payload = try vcPresentation(
109+
credential: credential,
110+
request: request,
111+
disclosingClaims: disclosingClaims,
112+
key: key
113+
)
114+
115+
let container = PresentationContainer(
116+
presentationSubmission: presentationSubmission,
117+
verifiableCredential: [AnyCodable(stringLiteral: payload)]
118+
)
119+
120+
return try JSONEncoder.didComm().encode(container).tryToString()
121+
}
122+
63123
private func vcPresentation(
64124
credential: SDJWTCredential,
65125
request: Data,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Domain
2+
import Foundation
3+
import JSONSchema
4+
import JSONWebToken
5+
6+
struct JWTPresentationExchangeParser: SubmissionDescriptorFormatParser {
7+
let format = "jwt"
8+
let verifier: VerifyJWT
9+
10+
func parse(path: String, presentationData: Data) async throws -> String {
11+
guard
12+
let jwt = presentationData.query(string: path)
13+
else {
14+
throw PolluxError.credentialPathInvalid(path: path)
15+
}
16+
17+
guard try await verifier.verifyJWT(jwtString: jwt) else {
18+
throw PolluxError.cannotVerifyCredential(credential: jwt, internalErrors: [])
19+
}
20+
21+
return jwt
22+
}
23+
24+
func parsePayload(path: String, presentationData: Data) async throws -> Data {
25+
let jwt = try await parse(path: path, presentationData: presentationData)
26+
return try JWT.getPayload(jwtString: jwt)
27+
}
28+
29+
func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier {
30+
let jwt = try await parse(path: descriptor.path, presentationData: presentationData)
31+
return JWTVerifierPresentationExchange(
32+
castor: verifier.castor,
33+
jwtString: jwt,
34+
submissionDescriptor: descriptor
35+
)
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Domain
2+
import Foundation
3+
import JSONSchema
4+
import JSONWebToken
5+
6+
struct JWTVCPresentationExchangeParser: SubmissionDescriptorFormatParser {
7+
let format = "jwt_vc"
8+
let verifier: VerifyJWT
9+
10+
func parse(path: String, presentationData: Data) async throws -> String {
11+
guard
12+
let jwt = presentationData.query(string: path)
13+
else {
14+
throw PolluxError.credentialPathInvalid(path: path)
15+
}
16+
17+
guard try await verifier.verifyJWT(jwtString: jwt) else {
18+
throw PolluxError.cannotVerifyCredential(credential: jwt, internalErrors: [])
19+
}
20+
21+
return jwt
22+
}
23+
24+
func parsePayload(path: String, presentationData: Data) async throws -> Data {
25+
let jwt = try await parse(path: path, presentationData: presentationData)
26+
return try JWT.getPayload(jwtString: jwt)
27+
}
28+
29+
func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier {
30+
let jwt = try await parse(path: descriptor.path, presentationData: presentationData)
31+
return JWTVerifierPresentationExchange(
32+
castor: verifier.castor,
33+
jwtString: jwt,
34+
submissionDescriptor: descriptor
35+
)
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import Domain
2+
import Foundation
3+
import JSONSchema
4+
import JSONWebToken
5+
6+
struct JWTVPPresentationExchangeParser: SubmissionDescriptorFormatParser {
7+
let format = "jwt_vp"
8+
let verifier: VerifyJWT
9+
10+
func parse(path: String, presentationData: Data) async throws -> String {
11+
guard
12+
let jwt = presentationData.query(string: path)
13+
else {
14+
throw PolluxError.credentialPathInvalid(path: path)
15+
}
16+
17+
guard try await verifier.verifyJWT(jwtString: jwt) else {
18+
throw PolluxError.cannotVerifyCredential(credential: jwt, internalErrors: [])
19+
}
20+
21+
return jwt
22+
}
23+
24+
func parsePayload(path: String, presentationData: Data) async throws -> Data {
25+
let jwt = try await parse(path: path, presentationData: presentationData)
26+
return try JWT.getPayload(jwtString: jwt)
27+
}
28+
29+
func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier {
30+
let jwt = try await parse(path: descriptor.path, presentationData: presentationData)
31+
return JWTVerifierPresentationExchange(
32+
castor: verifier.castor,
33+
jwtString: jwt,
34+
submissionDescriptor: descriptor
35+
)
36+
}
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import Domain
2+
import Foundation
3+
import JSONWebToken
4+
5+
struct JWTVerifierPresentationExchange: PresentationExchangeClaimVerifier {
6+
let castor: Castor
7+
let jwtString: String
8+
let submissionDescriptor: PresentationSubmission.Descriptor
9+
10+
func verifyClaim(inputDescriptor: InputDescriptor) throws {
11+
let payload = try JWT.getPayload(jwtString: jwtString)
12+
try VerifyJsonClaim.verify(inputDescriptor: inputDescriptor, jsonData: payload)
13+
}
14+
}

0 commit comments

Comments
 (0)