diff --git a/EdgeAgentSDK/Domain/Sources/Models/Errors.swift b/EdgeAgentSDK/Domain/Sources/Models/Errors.swift index c14e4b69..5ebfb531 100644 --- a/EdgeAgentSDK/Domain/Sources/Models/Errors.swift +++ b/EdgeAgentSDK/Domain/Sources/Models/Errors.swift @@ -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) @@ -874,6 +880,8 @@ public enum PolluxError: KnownPrismError { return 78 case .credentialIsSuspended: return 79 + case .cannotVerifyCredential: + return 80 } } @@ -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) +""" } } } diff --git a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Proof.swift b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Proof.swift index b76346f4..4eb27591 100644 --- a/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Proof.swift +++ b/EdgeAgentSDK/EdgeAgent/Sources/EdgeAgent+Proof.swift @@ -71,7 +71,8 @@ public extension EdgeAgent { request: request.makeMessage(), options: [ .exportableKey(exporting), - .subjectDID(subjectDID) + .subjectDID(subjectDID), + .disclosingClaims(claims: credential.claims.map(\.key)) ] ) default: diff --git a/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift b/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift index c784050f..546961cb 100644 --- a/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift +++ b/EdgeAgentSDK/EdgeAgent/Tests/PresentationExchangeTests.swift @@ -1,6 +1,7 @@ import Builders import Core import Domain +import eudi_lib_sdjwt_swift import Logging import JSONWebSignature import JSONWebToken @@ -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, @@ -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 { diff --git a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift index 54206621..934125e5 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTCredential+ProofableCredential.swift @@ -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 diff --git a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift index 9b2db182..47f48225 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/JWT/JWTPresentation.swift @@ -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" ) ) } diff --git a/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/PresentationDefinition.swift b/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/PresentationDefinition.swift index 841c4fd5..91b836a3 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/PresentationDefinition.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/PresentationDefinition.swift @@ -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. diff --git a/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/PresentationExchangeClaimVerifier.swift b/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/PresentationExchangeClaimVerifier.swift new file mode 100644 index 00000000..5b671a36 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/PresentationExchangeClaimVerifier.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol PresentationExchangeClaimVerifier { + func verifyClaim(inputDescriptor: InputDescriptor) throws +} diff --git a/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/SubmissionDescriptorFormatParser.swift b/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/SubmissionDescriptorFormatParser.swift new file mode 100644 index 00000000..629c83b2 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Models/PresentationExchage/SubmissionDescriptorFormatParser.swift @@ -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 +} diff --git a/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT.swift b/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT.swift index 0a83ac46..89d19275 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWT.swift @@ -54,3 +54,9 @@ extension SDJWTCredential: Credential { return "sd-jwt" } } + +extension SDJWTCredential { + func getAlg() throws -> String? { + return sdjwt.jwt.protectedHeader.algorithm?.rawValue + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift b/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift index bdbacfd8..8f40590b 100644 --- a/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift +++ b/EdgeAgentSDK/Pollux/Sources/Models/SDJWT/SDJWTPresentation.swift @@ -1,3 +1,4 @@ +import Core import Domain import eudi_lib_sdjwt_swift import Foundation @@ -50,6 +51,13 @@ struct SDJWTPresentation { } switch attachment.format { + case "dif/presentation-exchange/definitions@v1.0": + return try presentation( + credential: credential, + request: requestData, + disclosingClaims: disclosingClaims, + key: exportableKey + ) default: return try vcPresentation( credential: credential, @@ -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, diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTPresentationExchangeParser.swift b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTPresentationExchangeParser.swift new file mode 100644 index 00000000..0e3e7cc2 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTPresentationExchangeParser.swift @@ -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 + ) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTVCPresentationExchangeParser.swift b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTVCPresentationExchangeParser.swift new file mode 100644 index 00000000..5838af71 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTVCPresentationExchangeParser.swift @@ -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 + ) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTVPPresentationExchangeParser.swift b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTVPPresentationExchangeParser.swift new file mode 100644 index 00000000..864a2783 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/JWTVPPresentationExchangeParser.swift @@ -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 + ) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/JWT/PresentationExchangeJWTClaimVerifier.swift b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/PresentationExchangeJWTClaimVerifier.swift new file mode 100644 index 00000000..aeff4366 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/PresentationExchangeJWTClaimVerifier.swift @@ -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) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyJWT.swift b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyJWT.swift new file mode 100644 index 00000000..bb77f63b --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyJWT.swift @@ -0,0 +1,43 @@ +import Domain +import Foundation +import JSONWebAlgorithms +import JSONWebToken + +struct VerifyJWT { + let castor: Castor + + func verifyJWT(jwtString: String) async throws -> Bool { + try await verifyJWTCredentialRevocation(jwtString: jwtString) + let payload: DefaultJWTClaimsImpl = try JWT.getPayload(jwtString: jwtString) + guard let issuer = payload.iss else { + throw PolluxError.requiresThatIssuerExistsAndIsAPrismDID + } + + let issuerDID = try DID(string: issuer) + let issuerKeys = try await castor.getDIDPublicKeys(did: issuerDID) + + ES256KVerifier.bouncyCastleFailSafe = true + + let validations = issuerKeys + .compactMap(\.exporting) + .compactMap { + try? JWT.verify(jwtString: jwtString, senderKey: $0.jwk.toJoseJWK()) + } + ES256KVerifier.bouncyCastleFailSafe = false + return !validations.isEmpty + } + + private func verifyJWTCredentialRevocation(jwtString: String) async throws { + guard let credential = try? JWTCredential(data: jwtString.tryToData()) else { + return + } + let isRevoked = try await credential.isRevoked + let isSuspended = try await credential.isSuspended + guard !isRevoked else { + throw PolluxError.credentialIsRevoked(jwtString: jwtString) + } + guard !isSuspended else { + throw PolluxError.credentialIsSuspended(jwtString: jwtString) + } + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyPresentationSubmission.swift b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyPresentationSubmissionJWT.swift similarity index 98% rename from EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyPresentationSubmission.swift rename to EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyPresentationSubmissionJWT.swift index d551489d..f6b2f6f6 100644 --- a/EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyPresentationSubmission.swift +++ b/EdgeAgentSDK/Pollux/Sources/Operation/JWT/VerifyPresentationSubmissionJWT.swift @@ -2,7 +2,7 @@ import Domain import Foundation import JSONSchema -struct VerifyPresentationSubmission { +struct VerifyPresentationSubmissionJWT { static func verifyPresentationSubmissionClaims( request: PresentationDefinition, credentials: [Data] diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/SubmissionDescriptorParser.swift b/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/SubmissionDescriptorParser.swift new file mode 100644 index 00000000..1131d69e --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/SubmissionDescriptorParser.swift @@ -0,0 +1,27 @@ +import Domain +import Foundation + +struct SubmissionDescriptorParser { + let parsers: [SubmissionDescriptorFormatParser] + func parse( + descriptor: PresentationSubmission.Descriptor, + presentationData: Data + ) async throws -> PresentationExchangeClaimVerifier { + try await processPath(descriptor: descriptor, presentationData: presentationData) + } + + private func processPath(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier { + guard + let parser = parsers.first(where: { $0.format == descriptor.format }) + else { + throw PolluxError.unsupportedSubmittedFormat(string: descriptor.format, validFormats: parsers.map(\.format)) + } + + guard let nestedDescriptor = descriptor.pathNested else { + return try await parser.parseClaimVerifier(descriptor: descriptor, presentationData: presentationData) + } + + let nestedPayload: Data = try await parser.parsePayload(path: descriptor.path, presentationData: presentationData) + return try await processPath(descriptor: nestedDescriptor, presentationData: nestedPayload) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/VerifyJsonClaim.swift b/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/VerifyJsonClaim.swift new file mode 100644 index 00000000..c59f0ebb --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/VerifyJsonClaim.swift @@ -0,0 +1,71 @@ +import Domain +import Foundation +import JSONSchema + +struct VerifyJsonClaim { + static func verify(inputDescriptor: InputDescriptor, jsonData: Data) throws { + try validateCredentialPresentationClaims(inputDescriptor: inputDescriptor, jsonData: jsonData) + } + + private static func validateCredentialPresentationClaims(inputDescriptor: InputDescriptor, jsonData: Data) throws { + struct FieldValidation { + let valid: Bool + let error: Error + } + + let requiredFields = inputDescriptor + .constraints + .fields + .filter { $0.optional == nil || $0.optional == false } + + let fieldValidations = try requiredFields.map { field in + var validatedField = false + var errors = [Error]() + + try field.path.forEach { + guard !validatedField else { return } + let filterJson = try field.filter.map { try JSONEncoder().encode($0) } + + do { + try queryAndValidatePath($0, filter: filterJson?.tryToString(), jsonData: jsonData) + validatedField = true + } catch { + errors.append(error) + } + } + + return FieldValidation( + valid: validatedField, + error: PolluxError.cannotVerifyInputField( + name: field.name, + paths: field.path, + internalErrors: errors) + ) + } + + guard fieldValidations.allSatisfy(\.valid) else { + let errors = fieldValidations.filter { !$0.valid }.map(\.error) + throw PolluxError.cannotVerifyInput(name: inputDescriptor.name, purpose: inputDescriptor.purpose, fieldErrors: errors) + } + } + + private static func queryAndValidatePath(_ path: String, filter: String?, jsonData: Data) throws { + let query = jsonData.query(values: path)?.first + + guard query != nil else { throw PolluxError.inputPathNotFound(path: path) } + guard let filter else { return } + + let jsonFilter = try JSONSerialization.jsonObject(with: filter.tryToData()) as? [String: Any] + switch try JSONSchema.validate(["value": query], schema: [ + "type": "object", + "properties": [ + "value": jsonFilter + ] + ]) { + case .valid: + return + case .invalid(let errors): + throw PolluxError.inputFilterErrors(descriptions: errors.map(\.description)) + } + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/VerifyPresentationSubmission.swift b/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/VerifyPresentationSubmission.swift new file mode 100644 index 00000000..74389e4f --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/PresentationExchange/VerifyPresentationSubmission.swift @@ -0,0 +1,71 @@ +import Core +import Domain +import Foundation +import JSONSchema +import JSONWebAlgorithms +import JSONWebToken +import JSONWebSignature + +struct VerifyPresentationSubmission { + let castor: Castor + let parsers: [SubmissionDescriptorFormatParser] + + func verifyPresentationSubmission( + json: Data, + presentationRequest: PresentationExchangeRequest + ) async throws -> Bool { + let presentationContainer = try JSONDecoder.didComm().decode(PresentationContainer.self, from: json) + guard let submission = presentationContainer.presentationSubmission else { + throw PolluxError.presentationSubmissionNotAvailable + } + let verifiers = try await submission.descriptorMap.asyncMap { descriptor in + return try await SubmissionDescriptorParser(parsers: parsers) + .parse(descriptor: descriptor, presentationData: json) + } + try verifyPresentationSubmissionClaims( + request: presentationRequest.presentationDefinition, + claimVerifiers: verifiers + ) + return true + } + + private func verifyPresentationSubmissionClaims( + request: PresentationDefinition, + claimVerifiers: [PresentationExchangeClaimVerifier] + ) throws { + let requiredInputDescriptors = presentationClaimsRequirements(request: request) + try validateCredentialPresentationClaims(inputDescriptors: requiredInputDescriptors, claimVerifiers: claimVerifiers) + } + + private func validateCredentialPresentationClaims( + inputDescriptors: [InputDescriptor], + claimVerifiers: [PresentationExchangeClaimVerifier] + ) throws { + var inputErrors = [Error]() + inputDescriptors.forEach { input in + var errors = [Error]() + var descriptorValid = false + claimVerifiers.forEach { + guard !descriptorValid else { return } + do { + try $0.verifyClaim(inputDescriptor: input) + descriptorValid = true + } catch { + errors.append(error) + } + } + + if !descriptorValid { + inputErrors.append(contentsOf: errors) + } + } + + guard inputErrors.isEmpty else { + throw PolluxError.cannotVerifyPresentationInputs(errors: inputErrors) + } + } + + private func presentationClaimsRequirements(request: PresentationDefinition) -> [InputDescriptor] { + return request.inputDescriptors.filter { $0.constraints.fields.contains { $0.optional == nil || $0.optional == false } } + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/SDJWTClaimVerifier.swift b/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/SDJWTClaimVerifier.swift new file mode 100644 index 00000000..694901b6 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/SDJWTClaimVerifier.swift @@ -0,0 +1,17 @@ +import Domain +import Foundation +import eudi_lib_sdjwt_swift + +struct SDJWTClaimVerifier: PresentationExchangeClaimVerifier { + let sdjwtString: String + let submissionDescriptor: PresentationSubmission.Descriptor + + func verifyClaim(inputDescriptor: InputDescriptor) throws { + let payload = try CompactParser(serialisedString: sdjwtString) + .getSignedSdJwt() + .recreateClaims() + .recreatedClaims + .rawData() + try VerifyJsonClaim.verify(inputDescriptor: inputDescriptor, jsonData: payload) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/SDJWTPresentationExchangeParser.swift b/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/SDJWTPresentationExchangeParser.swift new file mode 100644 index 00000000..5f53fc4e --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/SDJWTPresentationExchangeParser.swift @@ -0,0 +1,42 @@ +import Domain +import Foundation +import JSONSchema +import JSONWebToken +import eudi_lib_sdjwt_swift + +struct SDJWTPresentationExchangeParser: SubmissionDescriptorFormatParser { + let format = "sd_jwt" + let verifier: VerifySDJWT + + func parse(path: String, presentationData: Data) async throws -> String { + guard + let sdjwt = presentationData.query(string: path) + else { + throw PolluxError.credentialPathInvalid(path: path) + } + + guard try await verifier.verifySDJWT(sdjwtString: sdjwt) else { + throw PolluxError.cannotVerifyCredential(credential: sdjwt, internalErrors: []) + } + + return sdjwt + } + + func parsePayload(path: String, presentationData: Data) async throws -> Data { + let sdjwt = try await parse(path: path, presentationData: presentationData) + let json = try CompactParser(serialisedString: sdjwt) + .getSignedSdJwt() + .recreateClaims() + .recreatedClaims + + return try json.rawData() + } + + func parseClaimVerifier(descriptor: PresentationSubmission.Descriptor, presentationData: Data) async throws -> PresentationExchangeClaimVerifier { + let sdjwt = try await parse(path: descriptor.path, presentationData: presentationData) + return SDJWTClaimVerifier( + sdjwtString: sdjwt, + submissionDescriptor: descriptor + ) + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/VerifySDJWT.swift b/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/VerifySDJWT.swift new file mode 100644 index 00000000..fab77cc0 --- /dev/null +++ b/EdgeAgentSDK/Pollux/Sources/Operation/SDJWT/VerifySDJWT.swift @@ -0,0 +1,58 @@ +import Domain +import Foundation +import eudi_lib_sdjwt_swift +import JSONWebKey +import JSONWebToken + +struct VerifySDJWT { + let castor: Castor + + func verifySDJWT(sdjwtString: String) async throws -> Bool { + let issuer = try getIssuer(sdjwtString: sdjwtString) + let issuerDID = try DID(string: issuer) + let issuerKeys = try await castor + .getDIDPublicKeys(did: issuerDID) + .compactMap { $0.exporting } + .compactMap { try $0.jwk.toJoseJWK() } + + return try verifyAllKeysSDJWT(sdjwtString: sdjwtString, keys: issuerKeys) + } + + private func verifyAllKeysSDJWT(sdjwtString: String, keys: [JSONWebKey.JWK]) throws -> Bool { + var isVerified = false + keys.forEach { key in + do { + let result = try verifyForKeySDJWT(sdjwtString: sdjwtString, key: key) + guard result else { + return + } + isVerified = true + } catch { + print(error) + } + } + return isVerified + } + + private func verifyForKeySDJWT(sdjwtString: String, key: JSONWebKey.JWK) throws -> Bool { + let result = try SDJWTVerifier(parser: CompactParser(serialisedString: sdjwtString)) + .verifyPresentation { jws in + try SignatureVerifier(signedJWT: jws, publicKey: key) + } + switch result { + case .success: + return true + case .failure(let failure): + throw failure + } + } + + private func getIssuer(sdjwtString: String) throws -> String { + guard + let jwt = sdjwtString.components(separatedBy: "~").first, + let issuer = try JWT.getIssuer(jwtString: jwt) + else { throw PolluxError.invalidCredentialError } + + return issuer + } +} diff --git a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift index 5574cd41..bbd3a051 100644 --- a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift +++ b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+CredentialVerification.swift @@ -39,7 +39,15 @@ extension PolluxImpl { switch attachment.format { case "dif/presentation-exchange/submission@v1.0": - return try await verifyPresentationSubmission(json: jsonData, requestId: requestId) + let request = try await getDefinition(id: requestId) + return try await VerifyPresentationSubmission( + castor: castor, + parsers: presentationExchangeParsers + ) + .verifyPresentationSubmission( + json: jsonData, + presentationRequest: request + ) case "anoncreds/proof@v1.0": return try await verifyAnoncreds( presentation: jsonData, @@ -51,50 +59,25 @@ extension PolluxImpl { } } - private func verifyPresentationSubmission(json: Data, requestId: String) async throws -> Bool { - let presentationContainer = try JSONDecoder.didComm().decode(PresentationContainer.self, from: json) - let presentationRequest = try await getDefinition(id: requestId) - guard let submission = presentationContainer.presentationSubmission else { - throw PolluxError.presentationSubmissionNotAvailable - } - let credentials = try getCredentialJWT(submission: submission, presentationData: json) - - try VerifyPresentationSubmission.verifyPresentationSubmissionClaims( - request: presentationRequest.presentationDefinition, - credentials: try credentials.map { - try JWT.getPayload(jwtString: $0) - } - ) - - try await verifyJWTs(credentials: credentials) - return true - } - - private func getCredentialJWT(submission: PresentationSubmission, presentationData: Data) throws -> [String] { - return submission.descriptorMap - .filter({ $0.format == "jwt" || $0.format == "jwt_vc" || $0.format == "jwt_vp" }) - .compactMap { try? processJWTPath(descriptor: $0, presentationData: presentationData) } - } - - private func processJWTPath(descriptor: PresentationSubmission.Descriptor, presentationData: Data) throws -> String { - guard descriptor.format == "jwt" || descriptor.format == "jwt_vc" || descriptor.format == "jwt_vp" else { - throw UnknownError.somethingWentWrongError( - customMessage: "This should not happen since its filtered before", - underlyingErrors: nil - ) - } - + private func getDefinition(id: String) async throws -> PresentationExchangeRequest { guard - let jwts = presentationData.query(string: descriptor.path) + let request = try await pluto.getMessage(id: id).first().await(), + let attachmentData = request.attachments.first?.data else { - throw PolluxError.credentialPathInvalid(path: descriptor.path) + throw PolluxError.couldNotFindPresentationRequest(id: id) } - guard let nestedDescriptor = descriptor.pathNested else { - return jwts + let json: Data + switch attachmentData { + case let jsonData as AttachmentJsonData: + json = jsonData.data + case let base64Data as AttachmentBase64: + json = try base64Data.decoded() + default: + throw PolluxError.invalidAttachmentType(supportedTypes: ["base64", "json"]) } - let nestedPayload: Data = try JWT.getPayload(jwtString: jwts) - return try processJWTPath(descriptor: nestedDescriptor, presentationData: nestedPayload) + + return try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: json) } private func verifyJWTs(credentials: [String]) async throws { @@ -147,27 +130,6 @@ extension PolluxImpl { } } - private func getDefinition(id: String) async throws -> PresentationExchangeRequest { - guard - let request = try await pluto.getMessage(id: id).first().await(), - let attachmentData = request.attachments.first?.data - else { - throw PolluxError.couldNotFindPresentationRequest(id: id) - } - - let json: Data - switch attachmentData { - case let jsonData as AttachmentJsonData: - json = jsonData.data - case let base64Data as AttachmentBase64: - json = try base64Data.decoded() - default: - throw PolluxError.invalidAttachmentType(supportedTypes: ["base64", "json"]) - } - - return try JSONDecoder.didComm().decode(PresentationExchangeRequest.self, from: json) - } - private func verifyAnoncreds( presentation: Data, requestId: String, diff --git a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+Presentation.swift b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+Presentation.swift index ff027af9..eadec2ea 100644 --- a/EdgeAgentSDK/Pollux/Sources/PolluxImpl+Presentation.swift +++ b/EdgeAgentSDK/Pollux/Sources/PolluxImpl+Presentation.swift @@ -39,7 +39,7 @@ extension PolluxImpl { ) } let presentationDefinition = PresentationDefinition( - format: .init(jwt: .init(alg: [.ES256K])), + format: .init(jwt: .init(alg: [.ES256K]), sdJwt: .init(alg: [.ES256K])), inputDescriptors: descriptors ) diff --git a/EdgeAgentSDK/Pollux/Sources/PolluxImpl.swift b/EdgeAgentSDK/Pollux/Sources/PolluxImpl.swift index e69b1890..1c5320cd 100644 --- a/EdgeAgentSDK/Pollux/Sources/PolluxImpl.swift +++ b/EdgeAgentSDK/Pollux/Sources/PolluxImpl.swift @@ -4,8 +4,27 @@ import Domain public struct PolluxImpl { let pluto: Pluto let castor: Castor - public init(castor: Castor, pluto: Pluto) { + let presentationExchangeParsers: [SubmissionDescriptorFormatParser] + public init( + castor: Castor, + pluto: Pluto, + presentationExchangeParsers: [SubmissionDescriptorFormatParser] + ) { self.pluto = pluto self.castor = castor + self.presentationExchangeParsers = presentationExchangeParsers + } + + public init(castor: Castor, pluto: Pluto) { + self.init( + castor: castor, + pluto: pluto, + presentationExchangeParsers: [ + JWTPresentationExchangeParser(verifier: .init(castor: castor)), + JWTVCPresentationExchangeParser(verifier: .init(castor: castor)), + JWTVPPresentationExchangeParser(verifier: .init(castor: castor)), + SDJWTPresentationExchangeParser(verifier: .init(castor: castor)) + ] + ) } }