From 75fcd471cdfc183c8636890c74b65fa38e90c4cf Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 10:59:30 +0300 Subject: [PATCH] [fix] sdjwt vc verifier, init sdjwt from json --- Sources/Issuer/SDJWT.swift | 8 + Sources/Parser/CompactParser.swift | 8 +- Sources/Types.swift | 16 ++ Sources/Utilities/JwsJsonSupportOption.swift | 100 +++++++-- Sources/Verifier/SDJWTVCVerifier.swift | 224 +++++++++++++++++++ Sources/Verifier/SDJWTVerifier.swift | 15 -- Sources/Verifier/SdJwtVcVerifier.swift | 22 -- 7 files changed, 336 insertions(+), 57 deletions(-) create mode 100644 Sources/Verifier/SDJWTVCVerifier.swift delete mode 100644 Sources/Verifier/SdJwtVcVerifier.swift diff --git a/Sources/Issuer/SDJWT.swift b/Sources/Issuer/SDJWT.swift index 9441ec6..5b454c2 100644 --- a/Sources/Issuer/SDJWT.swift +++ b/Sources/Issuer/SDJWT.swift @@ -96,6 +96,13 @@ public struct SignedSDJWT { self.kbJwt = try? JWS(jwsString: serializedKbJwt ?? "") } + init?(json: JSON) throws { + let triple = try JwsJsonSupport.parseJWSJson(unverifiedSdJwt: json) + self.jwt = triple.jwt + self.disclosures = triple.disclosures + self.kbJwt = triple.kbJwt + } + private init?(sdJwt: SDJWT, issuersPrivateKey: KeyType) { // Create a Signed SDJWT with no key binding guard let signedJwt = try? SignedSDJWT.createSignedJWT(key: issuersPrivateKey, jwt: sdJwt.jwt) else { @@ -191,6 +198,7 @@ public extension SignedSDJWT { return try self.toSDJWT().recreateClaims() } + func asJwsJsonObject( option: JwsJsonSupportOption = .flattened, kbJwt: JWTString?, diff --git a/Sources/Parser/CompactParser.swift b/Sources/Parser/CompactParser.swift index f83cb97..0e9cfdf 100644 --- a/Sources/Parser/CompactParser.swift +++ b/Sources/Parser/CompactParser.swift @@ -74,9 +74,11 @@ public class CompactParser: ParserProtocol { } // Ensure that all components are properly assigned - guard let unwrappedHeader = header, - let unwrappedPayload = payload, - let unwrappedSignature = signature else { + guard + let unwrappedHeader = header, + let unwrappedPayload = payload, + let unwrappedSignature = signature + else { throw SDJWTVerifierError.parsingError } diff --git a/Sources/Types.swift b/Sources/Types.swift index f275423..53a2418 100644 --- a/Sources/Types.swift +++ b/Sources/Types.swift @@ -32,6 +32,22 @@ public enum SDJWTError: Error { case macAsAlgorithm } +public enum SDJWTVerifierError: Error { + case parsingError + case invalidJwt + case invalidIssuer + case keyBindingFailed(description: String) + case invalidDisclosure(disclosures: [Disclosure]) + case missingOrUnknownHashingAlgorithm + case nonUniqueDisclosures + case nonUniqueDisclosureDigests + case missingDigests(disclosures: [Disclosure]) + case noAlgorithmProvided + case failedToCreateVerifier + case expiredJwt + case notValidYetJwt +} + /// Static Keys Used by the JWT enum Keys: String { case sd = "_sd" diff --git a/Sources/Utilities/JwsJsonSupportOption.swift b/Sources/Utilities/JwsJsonSupportOption.swift index 087fb24..5e9c0f3 100644 --- a/Sources/Utilities/JwsJsonSupportOption.swift +++ b/Sources/Utilities/JwsJsonSupportOption.swift @@ -15,18 +15,18 @@ */ import Foundation import SwiftyJSON +import JSONWebSignature + +fileprivate let JWS_JSON_HEADER = "header" +fileprivate let JWS_JSON_DISCLOSURES = "disclosures" +fileprivate let JWS_JSON_KB_JWT = "kb_jwt" +fileprivate let JWS_JSON_PROTECTED = "protected" +fileprivate let JWS_JSON_SIGNATURE = "signature" +fileprivate let JWS_JSON_SIGNATURES = "signatures" +fileprivate let JWS_JSON_PAYLOAD = "payload" public enum JwsJsonSupportOption { - case general, flattened - - private static let JWS_JSON_HEADER = "header" - private static let JWS_JSON_DISCLOSURES = "disclosures" - private static let JWS_JSON_KB_JWT = "kb_jwt" - private static let JWS_JSON_PROTECTED = "protected" - private static let JWS_JSON_SIGNATURE = "signature" - private static let JWS_JSON_SIGNATURES = "signatures" - private static let JWS_JSON_PAYLOAD = "payload" } internal extension JwsJsonSupportOption { @@ -40,16 +40,16 @@ internal extension JwsJsonSupportOption { ) -> JSON { let headersAndSignature = JSONObject { [ - Self.JWS_JSON_HEADER: JSONObject { + JWS_JSON_HEADER: JSONObject { [ - Self.JWS_JSON_DISCLOSURES: JSONArray { + JWS_JSON_DISCLOSURES: JSONArray { disclosures.map { JSON($0) } }, - Self.JWS_JSON_KB_JWT: kbJwt == nil ? nil : JSON(kbJwt!) + JWS_JSON_KB_JWT: kbJwt == nil ? nil : JSON(kbJwt!) ] }, - Self.JWS_JSON_PROTECTED: JSON(protected), - Self.JWS_JSON_SIGNATURE: JSON(signature) + JWS_JSON_PROTECTED: JSON(protected), + JWS_JSON_SIGNATURE: JSON(signature) ] } @@ -57,8 +57,8 @@ internal extension JwsJsonSupportOption { case .general: return JSONObject { [ - Self.JWS_JSON_PAYLOAD: JSON(payload), - Self.JWS_JSON_SIGNATURES: JSONArray { + JWS_JSON_PAYLOAD: JSON(payload), + JWS_JSON_SIGNATURES: JSONArray { [headersAndSignature] } ] @@ -66,7 +66,7 @@ internal extension JwsJsonSupportOption { case .flattened: return JSONObject { [ - Self.JWS_JSON_PAYLOAD: JSON(payload), + JWS_JSON_PAYLOAD: JSON(payload), ] headersAndSignature } @@ -74,4 +74,70 @@ internal extension JwsJsonSupportOption { } } +internal class JwsJsonSupport { + + static func parseJWSJson(unverifiedSdJwt: JSON) throws -> (jwt: JWS, disclosures: [String], kbJwt: JWS?) { + + let signatureContainer: JSON = unverifiedSdJwt[JWS_JSON_SIGNATURES] + .array? + .first ?? unverifiedSdJwt + + let unverifiedJwt = try createUnverifiedJwt( + signatureContainer: signatureContainer, + unverifiedSdJwt: unverifiedSdJwt + ) + + let unprotectedHeader = extractUnprotectedHeader(from: signatureContainer) + + return try extractUnverifiedValues( + unprotectedHeader: unprotectedHeader, + unverifiedJwt: unverifiedJwt + ) + } + + static private func createUnverifiedJwt(signatureContainer: JSON, unverifiedSdJwt: JSON) throws -> String { + guard let protected = signatureContainer[JWS_JSON_PROTECTED].string else { + throw SDJWTVerifierError.invalidJwt + } + + guard let signature = signatureContainer[JWS_JSON_SIGNATURE].string else { + throw SDJWTVerifierError.invalidJwt + } + + guard let payload = unverifiedSdJwt[JWS_JSON_PAYLOAD].string else { + throw SDJWTVerifierError.invalidJwt + } + + return "\(protected).\(payload).\(signature)" + } + + static private func extractUnprotectedHeader(from signatureContainer: JSON) -> JSON? { + if let jsonObject = signatureContainer[JWS_JSON_HEADER].dictionary { + return JSON(jsonObject) + } + return nil + } + + static func extractUnverifiedValues(unprotectedHeader: JSON?, unverifiedJwt: String) throws -> (JWS, [String], JWS?) { + + let unverifiedDisclosures: [String] = unprotectedHeader?[JWS_JSON_DISCLOSURES] + .array? + .compactMap { element in + return element.string + } ?? [] + + let jws: JWS? = if let unverifiedKBJwt = unprotectedHeader?[JWS_JSON_KB_JWT].string { + try JWS(jwsString: unverifiedKBJwt) + } else { + nil + } + + return ( + try JWS(jwsString: unverifiedJwt), + unverifiedDisclosures, + jws + ) + } +} + diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift new file mode 100644 index 0000000..2237411 --- /dev/null +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2023 European Commission + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Foundation +import X509 +import JSONWebKey +import SwiftyJSON +import JSONWebSignature +import JSONWebToken + +private let HTTPS_URI_SCHEME = "https" +private let DID_URI_SCHEME = "did" +private let SD_JWT_VC_TYPE = "vc+sd-jwt" + +public protocol X509CertificateTrust { + func isTrusted(chain: [Certificate]) async -> Bool +} + +struct X509CertificateTrustNone: X509CertificateTrust { + func isTrusted(chain: [Certificate]) async -> Bool { + return false + } +} + +public struct X509CertificateTrustFactory { + public static let none: X509CertificateTrust = X509CertificateTrustNone() +} + +/** + * A protocol to look up public keys from DIDs/DID URLs. + */ +public protocol LookupPublicKeysFromDIDDocument { + func lookup(did: String, didUrl: String?) async -> [JWK]? +} + +protocol SdJwtVcVerifierType { + func verifyIssuance( + unverifiedSdJwt: String + ) async throws -> Result + func verifyIssuance( + unverifiedSdJwt: JSON + ) async throws -> Result +} + +public class SDJWTVCVerifier: SdJwtVcVerifierType { + + private let trust: X509CertificateTrust + private let lookup: LookupPublicKeysFromDIDDocument? + private let fetcher: any SdJwtVcIssuerMetaDataFetching + + public init( + fetcher: SdJwtVcIssuerMetaDataFetching = SdJwtVcIssuerMetaDataFetcher( + urlSession: .shared + ), + trust: X509CertificateTrust = X509CertificateTrustFactory.none, + lookup: LookupPublicKeysFromDIDDocument? = nil + ) { + self.fetcher = fetcher + self.trust = trust + self.lookup = lookup + } + + func verifyIssuance( + unverifiedSdJwt: String + ) async throws -> Result { + let parser = CompactParser(serialisedString: unverifiedSdJwt) + let jws = try parser.getSignedSdJwt().jwt + let jwk = try await issuerJwsKeySelector( + jws: jws, + trust: trust, + lookup: lookup + ) + + switch jwk { + case .success(let jwk): + return try SDJWTVerifier( + parser: CompactParser( + serialisedString: unverifiedSdJwt + ) + ).verifyIssuance { jws in + try SignatureVerifier( + signedJWT: jws, + publicKey: jwk + ) + } + case .failure(let error): + throw error + } + } + + func verifyIssuance( + unverifiedSdJwt: JSON + ) async throws -> Result { + + guard + let sdJwt = try SignedSDJWT( + json: unverifiedSdJwt + ) + else { + throw SDJWTVerifierError.invalidJwt + } + + let jws = sdJwt.jwt + let jwk = try await issuerJwsKeySelector( + jws: jws, + trust: trust, + lookup: lookup + ) + + switch jwk { + case .success(let jwk): + return try SDJWTVerifier( + sdJwt: sdJwt + ).verifyIssuance { jws in + try SignatureVerifier( + signedJWT: jws, + publicKey: jwk + ) + } + case .failure(let error): + throw error + } + } +} + +private extension SDJWTVCVerifier { + func issuerJwsKeySelector( + jws: JWS, + trust: X509CertificateTrust, + lookup: LookupPublicKeysFromDIDDocument? + ) async throws -> Result { + + guard jws.protectedHeader.algorithm != nil else { + throw SDJWTVerifierError.noAlgorithmProvided + } + + guard let source = try keySource(jws: jws) else { + return .failure(SDJWTVerifierError.invalidJwt) + } + + switch source { + case .metadata(let iss, let kid): + guard let jwk = try await fetcher.fetchIssuerMetaData( + issuer: iss + )?.jwks.first(where: { $0.keyID == kid }) else { + return .failure(SDJWTVerifierError.invalidJwt) + } + return .success(jwk) + + case .x509CertChain(_, let chain): + if await trust.isTrusted(chain: chain) { + guard let jwk = try chain + .first? + .publicKey + .serializeAsPEM() + .pemString + .pemToSecKey()? + .jwk else { + return .failure(SDJWTVerifierError.invalidJwt) + } + return .success(jwk) + } + return .failure(SDJWTVerifierError.invalidJwt) + case .didUrl(let iss, let kid): + guard let key = await lookup?.lookup( + did: iss, + didUrl: kid + )?.first(where: { $0.keyID == kid }) else { + return .failure(SDJWTVerifierError.invalidJwt) + } + return .success(key) + } + } + + func keySource(jws: JWS) throws -> SdJwtVcIssuerPublicKeySource? { + let kid = jws.protectedHeader.keyID + let certChain = try [Certificate(pemEncoded: jws.protectedHeader.x509CertificateChain!)] + let payload = jws.payload + let json = try JSON(data: payload) + + guard let iss = json["iss"].string else { + throw SDJWTVerifierError.invalidIssuer + } + + let issUrl = URL(string: iss) + let issScheme = issUrl?.scheme + + if issScheme == HTTPS_URI_SCHEME && certChain.isEmpty { + guard let issUrl = issUrl else { + return nil + } + return .metadata( + iss: issUrl, + kid: kid + ) + } else if issScheme == HTTPS_URI_SCHEME { + guard let issUrl = issUrl else { + return nil + } + return .x509CertChain( + iss: issUrl, + chain: certChain + ) + } else if issScheme == DID_URI_SCHEME && certChain.isEmpty { + return .didUrl( + iss: iss, + kid: kid + ) + } + return nil + } +} diff --git a/Sources/Verifier/SDJWTVerifier.swift b/Sources/Verifier/SDJWTVerifier.swift index 26b4219..6cde786 100644 --- a/Sources/Verifier/SDJWTVerifier.swift +++ b/Sources/Verifier/SDJWTVerifier.swift @@ -24,21 +24,6 @@ public protocol VerifierProtocol { func verify() throws -> ReturnType } -public enum SDJWTVerifierError: Error { - case parsingError - case invalidJwt - case keyBindingFailed(description: String) - case invalidDisclosure(disclosures: [Disclosure]) - case missingOrUnknownHashingAlgorithm - case nonUniqueDisclosures - case nonUniqueDisclosureDigests - case missingDigests(disclosures: [Disclosure]) - case noAlgorithmProvided - case failedToCreateVerifier - case expiredJwt - case notValidYetJwt -} - /// `SDJWTVerifier` is a class for verifying SD JSON Web Tokens (SDJWT) in a Swift application. /// This class provides comprehensive methods to validate both cases of Issuance to a holder and presentation to a verifier /// diff --git a/Sources/Verifier/SdJwtVcVerifier.swift b/Sources/Verifier/SdJwtVcVerifier.swift deleted file mode 100644 index 0801929..0000000 --- a/Sources/Verifier/SdJwtVcVerifier.swift +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (c) 2023 European Commission - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -protocol SdJwtVcVerifierType { - -} - -class SdJwtVcVerifier { - -}