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 sdjwt verifier flow #161

Merged
merged 2 commits into from
Aug 26, 2024
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
14 changes: 14 additions & 0 deletions EdgeAgentSDK/Domain/Sources/Models/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,12 @@ public enum PolluxError: KnownPrismError {
internalErrors: [Error]
)

/// An error case indicating that a credential cannot be verified..
case cannotVerifyCredential(
credential: String? = nil,
internalErrors: [Error]
)

/// An error case indicating that a specified input path was not found.
case inputPathNotFound(path: String)

Expand Down Expand Up @@ -874,6 +880,8 @@ public enum PolluxError: KnownPrismError {
return 78
case .credentialIsSuspended:
return 79
case .cannotVerifyCredential:
return 80
}
}

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

case .credentialIsSuspended(let jwtString):
return "Credential (\(jwtString)) is suspended"
case .cannotVerifyCredential(let credential, let fieldErrors):
let errors = fieldErrors.map { " - \(errorMessage($0))" }.joined(separator: "\n")
return
"""
Cannot verify credential: \(credential.map { "with name: \($0)"} ?? ""), with errors: \n \(errors)
"""
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Proof.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public extension EdgeAgent {
request: request.makeMessage(),
options: [
.exportableKey(exporting),
.subjectDID(subjectDID)
.subjectDID(subjectDID),
.disclosingClaims(claims: credential.claims.map(\.key))
]
)
default:
Expand Down
64 changes: 64 additions & 0 deletions EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Builders
import Core
import Domain
import eudi_lib_sdjwt_swift
import Logging
import JSONWebSignature
import JSONWebToken
Expand Down Expand Up @@ -99,6 +100,44 @@ final class PresentationExchangeFlowTests: XCTestCase {
}
}

func testSDJWTPresentationRequest() async throws {
let prismDID = try await edgeAgent.createNewPrismDID()
let subjectDID = try await edgeAgent.createNewPrismDID()

let sdjwt = try await makeCredentialSDJWT(issuerDID: prismDID, subjectDID: subjectDID)
let credential = try SDJWTCredential(sdjwtString: sdjwt)

logger.info("Creating presentation request")
let message = try await edgeAgent.initiatePresentationRequest(
type: .jwt,
fromDID: DID(method: "test", methodId: "alice"),
toDID: DID(method: "test", methodId: "bob"),
claimFilters: [
.init(
paths: ["$.vc.credentialSubject.test"],
type: "string",
required: true,
pattern: "aliceTest"
)
]
)

try await edgeAgent.pluto.storeMessage(message: message.makeMessage(), direction: .sent).first().await()

let presentation = try await edgeAgent.createPresentationForRequestProof(
request: message,
credential: credential
)

let verification = try await edgeAgent.pollux.verifyPresentation(
message: presentation.makeMessage(),
options: []
)

logger.info(verification ? "Verification was successful" : "Verification failed")
XCTAssertTrue(verification)
}

private func makeCredentialJWT(issuerDID: DID, subjectDID: DID) async throws -> String {
let payload = MockCredentialClaim(
iss: issuerDID.string,
Expand All @@ -121,6 +160,31 @@ final class PresentationExchangeFlowTests: XCTestCase {
}
return try JWT.signed(payload: payload, protectedHeader: jwsHeader, key: jwkD.toJoseJWK()).jwtString
}

private func makeCredentialSDJWT(issuerDID: DID, subjectDID: DID) async throws -> String {
guard
let key = try await edgeAgent.pluto.getDIDPrivateKeys(did: issuerDID).first().await()?.first,
let jwkD = try await edgeAgent.apollo.restorePrivateKey(key).exporting?.jwk
else {
XCTFail()
fatalError()
}

let sdjwt = try SDJWTIssuer.issue(
issuersPrivateKey: try jwkD.toJoseJWK(),
header: DefaultJWSHeaderImpl(algorithm: .ES256K)
) {
ConstantClaims.iss(domain: issuerDID.string)
ConstantClaims.sub(subject: subjectDID.string)
ObjectClaim("vc") {
ObjectClaim("credentialSubject") {
FlatDisclosedClaim("test", "aliceTest")
}
}
}

return CompactSerialiser(signedSDJWT: sdjwt).serialised
}
}

private struct MockCredentialClaim: JWTRegisteredFieldsClaims, Codable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ extension JWTCredential: ProvableCredential {
let requestData = try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: jsonData)
let payload: Data = try JWT.getPayload(jwtString: jwtString)
do {
try VerifyPresentationSubmission.verifyPresentationSubmissionClaims(
try VerifyPresentationSubmissionJWT.verifyPresentationSubmissionClaims(
request: requestData.presentationDefinition, credentials: [payload]
)
return true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,11 @@ struct JWTPresentation {
PresentationSubmission.Descriptor(
id: $0.id,
path: "$.verifiable_credential[0]",
format: "jwt_vp",
format: "jwt",
pathNested: .init(
id: $0.id,
path: "$.vp.verifiableCredential[0]",
format: "jwt_vc"
format: "jwt"
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public struct PresentationDefinition: Codable {
public var ldpVp: LDPFormat?
/// Generic LDP format.
public var ldp: LDPFormat?
/// Generic SDJWT format..
public var sdJwt: JWTFormat?
}

/// Unique identifier for the presentation definition.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Foundation

public protocol PresentationExchangeClaimVerifier {
func verifyClaim(inputDescriptor: InputDescriptor) throws
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Foundation

public protocol SubmissionDescriptorFormatParser {
var format: String { get }
func parse(path: String, presentationData: Data) async throws -> String
func parsePayload(path: String, presentationData: Data) async throws -> Data
func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier
}
6 changes: 6 additions & 0 deletions EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,9 @@ extension SDJWTCredential: Credential {
return "sd-jwt"
}
}

extension SDJWTCredential {
func getAlg() throws -> String? {
return sdjwt.jwt.protectedHeader.algorithm?.rawValue
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Core
import Domain
import eudi_lib_sdjwt_swift
import Foundation
Expand Down Expand Up @@ -50,6 +51,13 @@ struct SDJWTPresentation {
}

switch attachment.format {
case "dif/presentation-exchange/[email protected]":
return try presentation(
credential: credential,
request: requestData,
disclosingClaims: disclosingClaims,
key: exportableKey
)
default:
return try vcPresentation(
credential: credential,
Expand All @@ -60,6 +68,58 @@ struct SDJWTPresentation {
}
}

private func presentation(
credential: SDJWTCredential,
request: Data,
disclosingClaims: [String],
key: ExportableKey
) throws -> String {
let presentationRequest = try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: request)

guard
let jwtFormat = presentationRequest.presentationDefinition.format?.sdJwt,
try jwtFormat.supportedTypes.contains(where: { try $0 == credential.getAlg() })
else {
throw PolluxError.credentialIsNotOfPresentationDefinitionRequiredAlgorithm
}

let credentialSubject = try credential.sdjwt.recreateClaims().recreatedClaims.rawData()

try presentationRequest.presentationDefinition.inputDescriptors.forEach {
try $0.constraints.fields.forEach {
guard credentialSubject.query(values: $0.path) != nil else {
throw PolluxError.credentialDoesntProvideOneOrMoreInputDescriptors(path: $0.path)
}
}
}
let presentationDefinitions = presentationRequest.presentationDefinition.inputDescriptors.map {
PresentationSubmission.Descriptor(
id: $0.id,
path: "$.verifiable_credential[0]",
format: "sd_jwt"
)
}

let presentationSubmission = PresentationSubmission(
definitionId: presentationRequest.presentationDefinition.id,
descriptorMap: presentationDefinitions
)

let payload = try vcPresentation(
credential: credential,
request: request,
disclosingClaims: disclosingClaims,
key: key
)

let container = PresentationContainer(
presentationSubmission: presentationSubmission,
verifiableCredential: [AnyCodable(stringLiteral: payload)]
)

return try JSONEncoder.didComm().encode(container).tryToString()
}

private func vcPresentation(
credential: SDJWTCredential,
request: Data,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Domain
import Foundation
import JSONSchema
import JSONWebToken

struct JWTPresentationExchangeParser: SubmissionDescriptorFormatParser {
let format = "jwt"
let verifier: VerifyJWT

func parse(path: String, presentationData: Data) async throws -> String {
guard
let jwt = presentationData.query(string: path)
else {
throw PolluxError.credentialPathInvalid(path: path)
}

guard try await verifier.verifyJWT(jwtString: jwt) else {
throw PolluxError.cannotVerifyCredential(credential: jwt, internalErrors: [])
}

return jwt
}

func parsePayload(path: String, presentationData: Data) async throws -> Data {
let jwt = try await parse(path: path, presentationData: presentationData)
return try JWT.getPayload(jwtString: jwt)
}

func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier {
let jwt = try await parse(path: descriptor.path, presentationData: presentationData)
return JWTVerifierPresentationExchange(
castor: verifier.castor,
jwtString: jwt,
submissionDescriptor: descriptor
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Domain
import Foundation
import JSONSchema
import JSONWebToken

struct JWTVCPresentationExchangeParser: SubmissionDescriptorFormatParser {
let format = "jwt_vc"
let verifier: VerifyJWT

func parse(path: String, presentationData: Data) async throws -> String {
guard
let jwt = presentationData.query(string: path)
else {
throw PolluxError.credentialPathInvalid(path: path)
}

guard try await verifier.verifyJWT(jwtString: jwt) else {
throw PolluxError.cannotVerifyCredential(credential: jwt, internalErrors: [])
}

return jwt
}

func parsePayload(path: String, presentationData: Data) async throws -> Data {
let jwt = try await parse(path: path, presentationData: presentationData)
return try JWT.getPayload(jwtString: jwt)
}

func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier {
let jwt = try await parse(path: descriptor.path, presentationData: presentationData)
return JWTVerifierPresentationExchange(
castor: verifier.castor,
jwtString: jwt,
submissionDescriptor: descriptor
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Domain
import Foundation
import JSONSchema
import JSONWebToken

struct JWTVPPresentationExchangeParser: SubmissionDescriptorFormatParser {
let format = "jwt_vp"
let verifier: VerifyJWT

func parse(path: String, presentationData: Data) async throws -> String {
guard
let jwt = presentationData.query(string: path)
else {
throw PolluxError.credentialPathInvalid(path: path)
}

guard try await verifier.verifyJWT(jwtString: jwt) else {
throw PolluxError.cannotVerifyCredential(credential: jwt, internalErrors: [])
}

return jwt
}

func parsePayload(path: String, presentationData: Data) async throws -> Data {
let jwt = try await parse(path: path, presentationData: presentationData)
return try JWT.getPayload(jwtString: jwt)
}

func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier {
let jwt = try await parse(path: descriptor.path, presentationData: presentationData)
return JWTVerifierPresentationExchange(
castor: verifier.castor,
jwtString: jwt,
submissionDescriptor: descriptor
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import Domain
import Foundation
import JSONWebToken

struct JWTVerifierPresentationExchange: PresentationExchangeClaimVerifier {
let castor: Castor
let jwtString: String
let submissionDescriptor: PresentationSubmission.Descriptor

func verifyClaim(inputDescriptor: InputDescriptor) throws {
let payload = try JWT.getPayload(jwtString: jwtString)
try VerifyJsonClaim.verify(inputDescriptor: inputDescriptor, jsonData: payload)
}
}
Loading
Loading