From fd0581e51fc1f57c6b392963871ccc63a7172929 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 30 Sep 2024 10:44:54 +0300 Subject: [PATCH 01/25] [fix] sdjwtvc verifier protovol, added x509 package --- Package.resolved | 27 ++++++++++++++++++++++++ Package.swift | 7 ++++++- Sources/Verifier/SDJWTVerifier.swift | 29 +++++++++++++++++--------- Sources/Verifier/SdJwtVcVerifier.swift | 22 +++++++++++++++++++ 4 files changed, 74 insertions(+), 11 deletions(-) create mode 100644 Sources/Verifier/SdJwtVcVerifier.swift diff --git a/Package.resolved b/Package.resolved index 7ddd407..155af77 100644 --- a/Package.resolved +++ b/Package.resolved @@ -27,6 +27,33 @@ "version" : "0.15.0" } }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "df5d2fcd22e3f480e3ef85bf23e277a4a0ef524d", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-certificates", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-certificates.git", + "state" : { + "revision" : "2f797305c1b5b982acaa6005d8a9f970cc4e97ff", + "version" : "1.5.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "81bee98e706aee68d39ed5996db069ef2b313d62", + "version" : "3.7.1" + } + }, { "identity" : "swiftyjson", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 4955728..59265df 100644 --- a/Package.swift +++ b/Package.swift @@ -25,6 +25,10 @@ let package = Package( .package( url: "https://github.com/beatt83/jose-swift.git", from: "3.1.0" + ), + .package( + url: "https://github.com/apple/swift-certificates.git", + from: "1.0.0" ) ], targets: [ @@ -32,7 +36,8 @@ let package = Package( name: "eudi-lib-sdjwt-swift", dependencies: [ "jose-swift", - .product(name: "SwiftyJSON", package: "swiftyjson") + .product(name: "SwiftyJSON", package: "swiftyjson"), + .product(name: "X509", package: "swift-certificates"), ], path: "Sources", plugins: [ diff --git a/Sources/Verifier/SDJWTVerifier.swift b/Sources/Verifier/SDJWTVerifier.swift index 557c9dc..26b4219 100644 --- a/Sources/Verifier/SDJWTVerifier.swift +++ b/Sources/Verifier/SDJWTVerifier.swift @@ -79,10 +79,15 @@ public class SDJWTVerifier { /// - claimVerifier: An optional closure to verify claims. /// - Returns: A `Result` containing the verified `SignedSDJWT` or an error. /// - public func verifyIssuance(issuersSignatureVerifier: (JWS) throws -> SignatureVerifier, - claimVerifier: ((_ nbf: Int?, _ exp: Int?) throws -> ClaimsVerifier)? = nil) rethrows -> Result { + public func verifyIssuance( + issuersSignatureVerifier: (JWS) throws -> SignatureVerifier, + claimVerifier: ((_ nbf: Int?, _ exp: Int?) throws -> ClaimsVerifier)? = nil + ) rethrows -> Result { Result { - try self.verify(issuersSignatureVerifier: issuersSignatureVerifier, claimVerifier: claimVerifier).get() + try self.verify( + issuersSignatureVerifier: issuersSignatureVerifier, + claimVerifier: claimVerifier + ).get() } } @@ -95,9 +100,11 @@ public class SDJWTVerifier { /// - keyBindingVerifier: An optional closure to verify key binding. /// - Returns: A `Result` containing the verified `SignedSDJWT` or an error. /// - public func verifyPresentation(issuersSignatureVerifier: (JWS) throws -> SignatureVerifier, - claimVerifier: ((_ nbf: Int?, _ exp: Int?) throws -> ClaimsVerifier)? = nil, - keyBindingVerifier: ((JWS, JWK) throws -> KeyBindingVerifier)? = nil) -> Result { + public func verifyPresentation( + issuersSignatureVerifier: (JWS) throws -> SignatureVerifier, + claimVerifier: ((_ nbf: Int?, _ exp: Int?) throws -> ClaimsVerifier)? = nil, + keyBindingVerifier: ((JWS, JWK) throws -> KeyBindingVerifier)? = nil + ) -> Result { Result { let commonVerifyResult = self.verify(issuersSignatureVerifier: issuersSignatureVerifier, claimVerifier: claimVerifier) let sdjwt = try commonVerifyResult.get() @@ -121,10 +128,12 @@ public class SDJWTVerifier { } } - public func verifyEnvelope(envelope: JWS, - issuersSignatureVerifier: (JWS) throws -> SignatureVerifier, - holdersSignatureVerifier: () throws -> SignatureVerifier, - claimVerifier: (_ audClaim: String, _ iat: Int) -> ClaimsVerifier) -> Result { + public func verifyEnvelope( + envelope: JWS, + issuersSignatureVerifier: (JWS) throws -> SignatureVerifier, + holdersSignatureVerifier: () throws -> SignatureVerifier, + claimVerifier: (_ audClaim: String, _ iat: Int) -> ClaimsVerifier + ) -> Result { Result { try issuersSignatureVerifier(sdJwt.jwt).verify() try holdersSignatureVerifier().verify() diff --git a/Sources/Verifier/SdJwtVcVerifier.swift b/Sources/Verifier/SdJwtVcVerifier.swift new file mode 100644 index 0000000..0801929 --- /dev/null +++ b/Sources/Verifier/SdJwtVcVerifier.swift @@ -0,0 +1,22 @@ +/* + * 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 { + +} From 701d9e5075830694b8bf237846d1e35c59c38505 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 30 Sep 2024 14:16:37 +0300 Subject: [PATCH 02/25] [fix] added SdJwtVcIssuerMetaData --- Sources/Model/SdJwtVcIssuerMetaData.swift | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Sources/Model/SdJwtVcIssuerMetaData.swift diff --git a/Sources/Model/SdJwtVcIssuerMetaData.swift b/Sources/Model/SdJwtVcIssuerMetaData.swift new file mode 100644 index 0000000..d4f2f55 --- /dev/null +++ b/Sources/Model/SdJwtVcIssuerMetaData.swift @@ -0,0 +1,27 @@ +/* + * 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 JSONWebKey + +public struct SdJwtVcIssuerMetaData { + public let issuer: URL + public let jwks: [JWK] + + public init(issuer: URL, jwks: [JWK]) { + self.issuer = issuer + self.jwks = jwks + } +} From 2ab87c627b13bb9237cd08af2c0077ec4f571463 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Mon, 30 Sep 2024 16:58:09 +0300 Subject: [PATCH 03/25] [fix] metadata fetcher --- .../SdJwtVcIssuerMetaDataFetcher.swift | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 Sources/Fetchers/SdJwtVcIssuerMetaDataFetcher.swift diff --git a/Sources/Fetchers/SdJwtVcIssuerMetaDataFetcher.swift b/Sources/Fetchers/SdJwtVcIssuerMetaDataFetcher.swift new file mode 100644 index 0000000..447dc29 --- /dev/null +++ b/Sources/Fetchers/SdJwtVcIssuerMetaDataFetcher.swift @@ -0,0 +1,88 @@ +/* + * 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 SwiftyJSON +import JSONWebKey + +public protocol SdJwtVcIssuerMetaDataFetching { + var urlSession: URLSession { get } + func fetchIssuerMetaData(issuer: URL) async throws -> SdJwtVcIssuerMetaData? +} + +public class SdJwtVcIssuerMetaDataFetcher: SdJwtVcIssuerMetaDataFetching { + + public let urlSession: URLSession + + public init(urlSession: URLSession) { + self.urlSession = urlSession + } + + public func fetchIssuerMetaData(issuer: URL) async throws -> SdJwtVcIssuerMetaData? { + let issuerMetadataUrl = issuerMetadataUrl(for: issuer) + let metadata: SdJwtVcIssuerMetadataTO = try await fetch(from: issuerMetadataUrl) + + guard issuer == URL(string: metadata.issuer) else { + throw SDJWTVerifierError.invalidJwt + } + + try xorValues(metadata.jwksUri, metadata.jwks) + + if let jwks = metadata.jwks { + return .init( + issuer: issuer, + jwks: jwks.keys + ) + } else if metadata.jwksUri != nil { + let jwks: JWKSet = try await fetch(from: issuerMetadataUrl) + return .init( + issuer: issuer, + jwks: jwks.keys + ) + } + + return nil + } +} + +private extension SdJwtVcIssuerMetaDataFetcher { + private func issuerMetadataUrl(for issuer: URL) -> URL { + var components = URLComponents(url: issuer, resolvingAgainstBaseURL: false)! + components.path = "/.well-known/jwt-vc-issuer" + components.path + return components.url! + } + + func fetch(from url: URL) async throws -> T { + + let (data, response) = try await URLSession.shared.data(from: url) + + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { + throw URLError(.badServerResponse) + } + + // Decode the JSON data into the Codable struct + let decoder = JSONDecoder() + let metadata = try decoder.decode(T.self, from: data) + + return metadata + } + + func xorValues(_ first: Any?, _ second: Any?) throws { + // Ensure that one is non-nil and the other is nil, but not both non-nil or both nil + guard (first != nil) != (second != nil) else { + throw SDJWTVerifierError.invalidJwt + } + } +} From 3927197b575fca6dff35fabcb0fcb61c682ccdb0 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 08:46:30 +0300 Subject: [PATCH 04/25] [fix] updated vc issuer metadata --- Sources/Model/SdJwtVcIssuerMetaData.swift | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/Sources/Model/SdJwtVcIssuerMetaData.swift b/Sources/Model/SdJwtVcIssuerMetaData.swift index d4f2f55..64646b1 100644 --- a/Sources/Model/SdJwtVcIssuerMetaData.swift +++ b/Sources/Model/SdJwtVcIssuerMetaData.swift @@ -15,6 +15,31 @@ */ import Foundation import JSONWebKey +import SwiftyJSON + +struct SdJwtVcIssuerMetadataTO: Decodable { + let issuer: String + let jwksUri: String? + let jwks: JWKSet? + + enum CodingKeys: String, CodingKey { + case issuer + case jwksUri = "jwks_uri" + case jwks + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + issuer = try container.decode(String.self, forKey: .issuer) + jwksUri = try container.decodeIfPresent(String.self, forKey: .jwksUri) + + if let jwksData = try container.decodeIfPresent(JWKSet.self, forKey: .jwks) { + jwks = jwksData + } else { + jwks = nil + } + } +} public struct SdJwtVcIssuerMetaData { public let issuer: URL From 0bafcb6d24662651bf202508ea9944fd32eb19e8 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 10:47:33 +0300 Subject: [PATCH 05/25] [fix] moved issuer public key source --- .../SdJwtVcIssuerPublicKeySource.swift | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Sources/Utilities/SdJwtVcIssuerPublicKeySource.swift diff --git a/Sources/Utilities/SdJwtVcIssuerPublicKeySource.swift b/Sources/Utilities/SdJwtVcIssuerPublicKeySource.swift new file mode 100644 index 0000000..617d4f1 --- /dev/null +++ b/Sources/Utilities/SdJwtVcIssuerPublicKeySource.swift @@ -0,0 +1,24 @@ +/* + * 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 + +public enum SdJwtVcIssuerPublicKeySource { + case metadata(iss: URL, kid: String?) + case x509CertChain(iss: URL, chain: [Certificate]) + case didUrl(iss: String, kid: String?) +} + From b645eebe637a57a2f5b88d25ac3bd481eb4c221b Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 10:48:43 +0300 Subject: [PATCH 06/25] [fix] add pem parse string extension --- .../Extensions/String+Extension.swift | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/Sources/Utilities/Extensions/String+Extension.swift b/Sources/Utilities/Extensions/String+Extension.swift index f226d7d..3cf1a64 100644 --- a/Sources/Utilities/Extensions/String+Extension.swift +++ b/Sources/Utilities/Extensions/String+Extension.swift @@ -60,3 +60,67 @@ extension String { return nil } } + +extension String { + + /// Converts a PEM encoded public key to `SecKey`. + /// - Returns: The corresponding `SecKey` if successful, otherwise `nil`. + func pemToSecKey() -> SecKey? { + // Remove the PEM header and footer + let keyString = self + .replacingOccurrences(of: "-----BEGIN PUBLIC KEY-----", with: "") + .replacingOccurrences(of: "-----END PUBLIC KEY-----", with: "") + .replacingOccurrences(of: "\n", with: "") + .replacingOccurrences(of: "\r", with: "") + + // Decode the Base64-encoded string + guard let keyData = Data(base64Encoded: keyString) else { + print("Invalid Base64 string") + return nil + } + + // First, try RSA + if let secKey = String.createSecKey(from: keyData, keyType: kSecAttrKeyTypeRSA) { + print("Key identified as RSA.") + return secKey + } + + // If RSA fails, try EC + if let secKey = String.createSecKey(from: keyData, keyType: kSecAttrKeyTypeEC) { + print("Key identified as EC.") + return secKey + } + + // Add more key types if needed (e.g., DSA, etc.) + + // If neither RSA nor EC works, return nil + print("Unable to identify key type.") + return nil + } + + /// Creates a `SecKey` from the provided key data. + /// - Parameters: + /// - keyData: The raw key data. + /// - keyType: The key type (e.g., RSA or EC). + /// - Returns: The `SecKey` if successful, otherwise `nil`. + private static func createSecKey(from keyData: Data, keyType: CFString) -> SecKey? { + // Define the attributes for creating the SecKey + let attributes: [CFString: Any] = [ + kSecAttrKeyType: keyType, + kSecAttrKeyClass: kSecAttrKeyClassPublic, + kSecAttrKeySizeInBits: keyData.count * 8 + ] + + // Try to create the SecKey + var error: Unmanaged? + if let secKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, &error) { + return secKey + } else { + if let err = error?.takeRetainedValue() { + print("Error creating SecKey: \(err.localizedDescription)") + } + return nil + } + } +} + From 75fcd471cdfc183c8636890c74b65fa38e90c4cf Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 10:59:30 +0300 Subject: [PATCH 07/25] [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 { - -} From 43544f8a708feb99d71790d9b3c7162c64544f59 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 12:12:04 +0300 Subject: [PATCH 08/25] [fix] added cert functions --- Sources/Utilities/TrustFunctions.swift | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 Sources/Utilities/TrustFunctions.swift diff --git a/Sources/Utilities/TrustFunctions.swift b/Sources/Utilities/TrustFunctions.swift new file mode 100644 index 0000000..4296acc --- /dev/null +++ b/Sources/Utilities/TrustFunctions.swift @@ -0,0 +1,58 @@ +/* + * 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 SwiftyJSON + +func parseCertificates(from chain: [String]) -> [Certificate] { + chain.compactMap { serializedCertificate in + guard let serializedData = Data(base64Encoded: serializedCertificate) else { + return nil + } + + if let string = String(data: serializedData, encoding: .utf8) { + guard let data = Data(base64Encoded: string.removeCertificateDelimiters()) else { + return nil + } + let derBytes = [UInt8](data) + return try? Certificate(derEncoded: derBytes) + } else { + let derBytes = [UInt8](serializedData) + return try? Certificate(derEncoded: derBytes) + } + } +} + +func parseCertificates(from data: Data) -> [Certificate] { + let header = try? JSON(data: data) + let chain = header?["x5c"].array?.compactMap { $0.stringValue } ?? [] + return chain.compactMap { serializedCertificate in + guard let serializedData = Data(base64Encoded: serializedCertificate) else { + return nil + } + + if let string = String(data: serializedData, encoding: .utf8) { + guard let data = Data(base64Encoded: string.removeCertificateDelimiters()) else { + return nil + } + let derBytes = [UInt8](data) + return try? Certificate(derEncoded: derBytes) + } else { + let derBytes = [UInt8](serializedData) + return try? Certificate(derEncoded: derBytes) + } + } +} From 6b92033d7c61321d89830c8618bef3c74244f9a6 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 12:12:51 +0300 Subject: [PATCH 09/25] [fix] parse x5c certificate chain --- Sources/Utilities/Extensions/JWS+Extension.swift | 4 ++++ Sources/Utilities/Extensions/String+Extension.swift | 6 ++++++ Sources/Verifier/SDJWTVCVerifier.swift | 12 +++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/Sources/Utilities/Extensions/JWS+Extension.swift b/Sources/Utilities/Extensions/JWS+Extension.swift index 1c19dfb..d6ea47f 100644 --- a/Sources/Utilities/Extensions/JWS+Extension.swift +++ b/Sources/Utilities/Extensions/JWS+Extension.swift @@ -29,4 +29,8 @@ extension JWS { func aud() throws -> String? { return try payloadJSON()[Keys.aud.rawValue].array?.toJSONString() ?? payloadJSON()[Keys.aud.rawValue].string } + + func iss() throws -> String? { + return try payloadJSON()[Keys.iss.rawValue].array?.toJSONString() ?? payloadJSON()[Keys.iss.rawValue].string + } } diff --git a/Sources/Utilities/Extensions/String+Extension.swift b/Sources/Utilities/Extensions/String+Extension.swift index 3cf1a64..47d572d 100644 --- a/Sources/Utilities/Extensions/String+Extension.swift +++ b/Sources/Utilities/Extensions/String+Extension.swift @@ -63,6 +63,12 @@ extension String { extension String { + func removeCertificateDelimiters() -> String { + return self.replacingOccurrences(of: "-----BEGIN CERTIFICATE-----\n", with: "") + .replacingOccurrences(of: "-----END CERTIFICATE-----", with: "") + .replacingOccurrences(of: "\n", with: "") + } + /// Converts a PEM encoded public key to `SecKey`. /// - Returns: The corresponding `SecKey` if successful, otherwise `nil`. func pemToSecKey() -> SecKey? { diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index 2237411..59fd20b 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -185,15 +185,13 @@ private extension SDJWTVCVerifier { } 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 { + guard let iss = try? jws.iss() else { throw SDJWTVerifierError.invalidIssuer } + let certChain = parseCertificates(from: jws.protectedHeaderData) + let issUrl = URL(string: iss) let issScheme = issUrl?.scheme @@ -203,7 +201,7 @@ private extension SDJWTVCVerifier { } return .metadata( iss: issUrl, - kid: kid + kid: jws.protectedHeader.keyID ) } else if issScheme == HTTPS_URI_SCHEME { guard let issUrl = issUrl else { @@ -216,7 +214,7 @@ private extension SDJWTVCVerifier { } else if issScheme == DID_URI_SCHEME && certChain.isEmpty { return .didUrl( iss: iss, - kid: kid + kid: jws.protectedHeader.keyID ) } return nil From 3255fd314e90aadec9773e3b9e776e2803637514 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 12:44:22 +0300 Subject: [PATCH 10/25] [fix] added cerificate extensions --- .../Extensions/Certificate+Extensions.swift | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 Sources/Utilities/Extensions/Certificate+Extensions.swift diff --git a/Sources/Utilities/Extensions/Certificate+Extensions.swift b/Sources/Utilities/Extensions/Certificate+Extensions.swift new file mode 100644 index 0000000..2cf3dfd --- /dev/null +++ b/Sources/Utilities/Extensions/Certificate+Extensions.swift @@ -0,0 +1,39 @@ +/* + * 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 + +extension SubjectAlternativeNames { + func rawSubjectAlternativeNames() -> [String] { + self.compactMap { generalName in + switch generalName { + case .dnsName(let name): + return name + default: return nil + } + } + } + + func rawUniformResourceIdentifiers() -> [String] { + self.compactMap { generalName in + switch generalName { + case .uniformResourceIdentifier(let identifier): + return identifier + default: return nil + } + } + } +} From bc26ddf79c6533250457a383068a748932a179bf Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 12:45:04 +0300 Subject: [PATCH 11/25] [fix] added x509 checks to vc verifier --- Sources/Verifier/SDJWTVCVerifier.swift | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index 59fd20b..fdf1b15 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -191,6 +191,7 @@ private extension SDJWTVCVerifier { } let certChain = parseCertificates(from: jws.protectedHeaderData) + let leaf = certChain.first let issUrl = URL(string: iss) let issScheme = issUrl?.scheme @@ -204,9 +205,13 @@ private extension SDJWTVCVerifier { kid: jws.protectedHeader.keyID ) } else if issScheme == HTTPS_URI_SCHEME { - guard let issUrl = issUrl else { + guard + let issUrl = issUrl, + isIssuerFQDNContained(in: leaf, issuerUrl: issUrl) || isIssuerURIContained(in: leaf, iss: iss) + else { return nil } + return .x509CertChain( iss: issUrl, chain: certChain @@ -219,4 +224,34 @@ private extension SDJWTVCVerifier { } return nil } + + private func isIssuerFQDNContained(in leaf: Certificate?, issuerUrl: URL) -> Bool { + // Get the host from the issuer URL + guard let issuerFQDN = issuerUrl.host else { + return false + } + + // Extract the DNS names from the certificate's subject alternative names + let dnsNames = try? leaf?.extensions + .subjectAlternativeNames? + .rawSubjectAlternativeNames() + + // Check if any of the DNS names match the issuer FQDN + let contains = dnsNames?.contains(where: { $0 == issuerFQDN }) ?? false + + return contains + } + + func isIssuerURIContained(in leaf: Certificate?, iss: String) -> Bool { + // Extract the URIs from the certificate's subject alternative names + let uris = try? leaf? + .extensions + .subjectAlternativeNames? + .rawUniformResourceIdentifiers() + + // Check if any of the URIs match the 'iss' string + let contains = uris?.contains(where: { $0 == iss }) ?? false + + return contains + } } From c0b687ac023bfe2f8e08ab6c2606a4456ba7791f Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 13:23:15 +0300 Subject: [PATCH 12/25] [fix] add certificate chane verifier --- Sources/Utilities/TrustFunctions.swift | 5 + Sources/Verifier/SDJWTVCVerifier.swift | 14 - .../X509CertificateChainVerifier.swift | 256 ++++++++++++++++++ 3 files changed, 261 insertions(+), 14 deletions(-) create mode 100644 Sources/Verifier/X509CertificateChainVerifier.swift diff --git a/Sources/Utilities/TrustFunctions.swift b/Sources/Utilities/TrustFunctions.swift index 4296acc..0c6eca3 100644 --- a/Sources/Utilities/TrustFunctions.swift +++ b/Sources/Utilities/TrustFunctions.swift @@ -56,3 +56,8 @@ func parseCertificates(from data: Data) -> [Certificate] { } } } + +func parseCertificateData(_ data: Data) -> [String] { + let header = try? JSON(data: data) + return header?["x5c"].array?.compactMap { $0.stringValue } ?? [] +} diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index fdf1b15..b3e4dde 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -24,20 +24,6 @@ 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. */ diff --git a/Sources/Verifier/X509CertificateChainVerifier.swift b/Sources/Verifier/X509CertificateChainVerifier.swift new file mode 100644 index 0000000..7722e56 --- /dev/null +++ b/Sources/Verifier/X509CertificateChainVerifier.swift @@ -0,0 +1,256 @@ +/* + * 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 SwiftASN1 +import Security + +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() +} + +public typealias Base64Certificate = String + +public enum ChainTrustResult: Equatable { + case success + case recoverableFailure(String) + case failure +} + +public enum DataConversionError: Error { + case conversionFailed(String) +} + +public struct X509CertificateChainVerifier: X509CertificateTrust { + + public init() {} + + public func isChainTrustResultSuccesful(_ result: ChainTrustResult) -> Bool { + return result != .failure + } + + public func isTrusted(chain: [Certificate]) async -> Bool { + let result = try? verifyCertificateChain(certificates: chain) + return result != .failure + } + + public func verifyCertificateChain(base64Certificates: [Base64Certificate]) throws -> ChainTrustResult { + + let certificates = try convertStringsToData( + base64Strings: base64Certificates + ).compactMap { + SecCertificateCreateWithData(nil, $0 as CFData) + } + + if certificates.isEmpty { + return .failure + } + + // Create a certificate trust object + var trust: SecTrust? + let policy = SecPolicyCreateBasicX509() + + // Set the certificate chain and policy for trust evaluation + SecTrustCreateWithCertificates(certificates as CFTypeRef, policy, &trust) + + // Evaluate the trust + var trustResult: SecTrustResultType = .invalid + _ = SecTrustEvaluate(trust!, &trustResult) + + // Check if the trust evaluation was successful + if trustResult == .unspecified { + return .success + + } else if trustResult == .recoverableTrustFailure { + var error: CFError? + _ = SecTrustEvaluateWithError(trust!, &error) + return .recoverableFailure(error?.localizedDescription ?? "Unknown .recoverableFailure") + + } else { + return .failure + } + } + + public func verifyCertificateChain(certificates: [Certificate]) throws -> ChainTrustResult { + + let certificates = certificates.map { + convertCertificateToBase64(certificate: $0) + }.compactMap { + $0 + }.compactMap { + SecCertificateCreateWithData(nil, $0 as CFData) + } + + if certificates.isEmpty { + return .failure + } + + // Create a certificate trust object + var trust: SecTrust? + let policy = SecPolicyCreateBasicX509() + + // Set the certificate chain and policy for trust evaluation + SecTrustCreateWithCertificates(certificates as CFTypeRef, policy, &trust) + + // Evaluate the trust + var trustResult: SecTrustResultType = .invalid + _ = SecTrustEvaluate(trust!, &trustResult) + + // Check if the trust evaluation was successful + if trustResult == .unspecified { + return .success + + } else if trustResult == .recoverableTrustFailure { + var error: CFError? + _ = SecTrustEvaluateWithError(trust!, &error) + return .recoverableFailure(error?.localizedDescription ?? "Unknown .recoverableFailure") + + } else { + return .failure + } + } + + public func checkCertificateValidAndNotRevoked(base64Certificate: Base64Certificate) throws -> Bool{ + + let certificates = try convertStringsToData( + base64Strings: [base64Certificate] + ).compactMap { + SecCertificateCreateWithData(nil, $0 as CFData) + } + + guard + certificates.count == 1 + else { + return false + } + + if let certificate = certificates.first { + + // Create a policy for certificate validation + let policy = SecPolicyCreateBasicX509() + + // Create a trust object with the certificate and policy + var trust: SecTrust? + if SecTrustCreateWithCertificates(certificate, policy, &trust) == errSecSuccess { + + // Set the OCSP responder URL + let ocspResponderURL = URL(string: "http://ocsp.example.com")! + SecTrustSetNetworkFetchAllowed(trust!, true) + SecTrustSetOCSPResponse(trust!, ocspResponderURL as CFURL) + + // Evaluate the trust + var trustResult: SecTrustResultType = .invalid + if SecTrustEvaluate(trust!, &trustResult) == errSecSuccess { + if trustResult == .proceed || trustResult == .unspecified { + return true + } else if trustResult == .deny || trustResult == .fatalTrustFailure { + return false + } else { + return false + } + } else { + return false + } + } else { + return false + } + + } else { + return false + } + } + + public func areCertificatesLinked( + rootCertificateBase64: String, + otherCertificateBase64: String + ) -> Bool { + guard + let rootCertificateData = Data(base64Encoded: rootCertificateBase64), + let otherCertificateData = Data(base64Encoded: otherCertificateBase64) + else { + return false // Invalid Base64-encoded data + } + + // Create SecCertificate objects from DER data + if let rootCertificate = SecCertificateCreateWithData(nil, rootCertificateData as CFData), + let otherCertificate = SecCertificateCreateWithData(nil, otherCertificateData as CFData) { + + // Create a trust object and evaluate it + var trust: SecTrust? + var policy: SecPolicy? + + policy = SecPolicyCreateBasicX509() + let policies = [policy!] as CFArray + + let status = SecTrustCreateWithCertificates([rootCertificate] as CFArray, policies, &trust) + + if status == errSecSuccess { + SecTrustSetAnchorCertificates(trust!, [rootCertificate] as CFArray) + + let otherCertificates = [otherCertificate] as CFArray + SecTrustSetAnchorCertificatesOnly(trust!, true) + SecTrustSetAnchorCertificates(trust!, otherCertificates) + + var trustResult: SecTrustResultType = .invalid + SecTrustEvaluate(trust!, &trustResult) + + return trustResult == .unspecified || trustResult == .proceed + } + } + + return false // The certificates are not linked + } +} + +private extension X509CertificateChainVerifier { + + func convertCertificateToBase64(certificate: Certificate) -> Data? { + do { + // Encode the certificate to DER format using SwiftASN1 + var serializer = DER.Serializer() + try serializer.serialize(certificate) + let derData = Data(serializer.serializedBytes) + return derData + } catch { + return nil + } + } + + func convertStringsToData(base64Strings: [String]) throws -> [Data] { + var dataObjects: [Data] = [] + for base64String in base64Strings { + if let data = Data(base64Encoded: base64String), + let string = String(data: data, encoding: .utf8)?.removeCertificateDelimiters(), + let encodedData = Data(base64Encoded: string) { + dataObjects.append(encodedData) + } else { + throw DataConversionError.conversionFailed("Failed to convert base64 string: \(base64String)") + } + } + + return dataObjects + } +} From c8ba2d21b81d9048fa03f6eab573c6d43f4622d1 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Tue, 1 Oct 2024 16:06:41 +0300 Subject: [PATCH 13/25] [fix] added some comments --- .../Extensions/Certificate+Extensions.swift | 16 +++++ Sources/Utilities/TrustFunctions.swift | 31 +++----- Sources/Verifier/SDJWTVCVerifier.swift | 71 +++++++++++++++++++ 3 files changed, 97 insertions(+), 21 deletions(-) diff --git a/Sources/Utilities/Extensions/Certificate+Extensions.swift b/Sources/Utilities/Extensions/Certificate+Extensions.swift index 2cf3dfd..01740a3 100644 --- a/Sources/Utilities/Extensions/Certificate+Extensions.swift +++ b/Sources/Utilities/Extensions/Certificate+Extensions.swift @@ -16,7 +16,17 @@ import Foundation import X509 +/// Extension for the `SubjectAlternativeNames` structure provided by X.509 library. +/// This extension provides utility methods for extracting DNS names and URIs from the +/// subject alternative names (SAN) field of an X.509 certificate. extension SubjectAlternativeNames { + + /// Extracts the DNS names from the subject alternative names (SAN) field of a certificate. + /// + /// This function iterates over all general names in the `SubjectAlternativeNames` structure + /// and extracts only those that are DNS names (`.dnsName`). It returns these names as an array of strings. + /// + /// - Returns: An array of DNS names found in the subject alternative names field, or an empty array if no DNS names are present. func rawSubjectAlternativeNames() -> [String] { self.compactMap { generalName in switch generalName { @@ -27,6 +37,12 @@ extension SubjectAlternativeNames { } } + /// Extracts the Uniform Resource Identifiers (URIs) from the subject alternative names (SAN) field of a certificate. + /// + /// This function iterates over all general names in the `SubjectAlternativeNames` structure + /// and extracts only those that are URIs (`.uniformResourceIdentifier`). It returns these URIs as an array of strings. + /// + /// - Returns: An array of URIs found in the subject alternative names field, or an empty array if no URIs are present. func rawUniformResourceIdentifiers() -> [String] { self.compactMap { generalName in switch generalName { diff --git a/Sources/Utilities/TrustFunctions.swift b/Sources/Utilities/TrustFunctions.swift index 0c6eca3..611ae73 100644 --- a/Sources/Utilities/TrustFunctions.swift +++ b/Sources/Utilities/TrustFunctions.swift @@ -18,27 +18,21 @@ import X509 import SwiftyJSON func parseCertificates(from chain: [String]) -> [Certificate] { - chain.compactMap { serializedCertificate in - guard let serializedData = Data(base64Encoded: serializedCertificate) else { - return nil - } - - if let string = String(data: serializedData, encoding: .utf8) { - guard let data = Data(base64Encoded: string.removeCertificateDelimiters()) else { - return nil - } - let derBytes = [UInt8](data) - return try? Certificate(derEncoded: derBytes) - } else { - let derBytes = [UInt8](serializedData) - return try? Certificate(derEncoded: derBytes) - } - } + processChain(chain) } func parseCertificates(from data: Data) -> [Certificate] { let header = try? JSON(data: data) let chain = header?["x5c"].array?.compactMap { $0.stringValue } ?? [] + return processChain(chain) +} + +func parseCertificateData(_ data: Data) -> [String] { + let header = try? JSON(data: data) + return header?["x5c"].array?.compactMap { $0.stringValue } ?? [] +} + +fileprivate func processChain(_ chain: [String]) -> [Certificate] { return chain.compactMap { serializedCertificate in guard let serializedData = Data(base64Encoded: serializedCertificate) else { return nil @@ -56,8 +50,3 @@ func parseCertificates(from data: Data) -> [Certificate] { } } } - -func parseCertificateData(_ data: Data) -> [String] { - let header = try? JSON(data: data) - return header?["x5c"].array?.compactMap { $0.stringValue } ?? [] -} diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index b3e4dde..b60a28c 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -28,24 +28,67 @@ private let SD_JWT_VC_TYPE = "vc+sd-jwt" * A protocol to look up public keys from DIDs/DID URLs. */ public protocol LookupPublicKeysFromDIDDocument { + /** + * Asynchronously looks up public keys from a DID document based on a DID or DID URL. + * + * - Parameters: + * - did: The DID identifier. + * - didUrl: The DID URL (optional). + * - Returns: An array of JWKs (public keys) or `nil` if the lookup fails. + */ func lookup(did: String, didUrl: String?) async -> [JWK]? } +/** + * A protocol defining methods for verifying SD-JWTs + */ protocol SdJwtVcVerifierType { + + /** + * Verifies the issuance of an SD-JWT from a serialized string. + * + * - Parameter unverifiedSdJwt: The unverified SD-JWT in string format. + * - Returns: A `Result` containing either the verified `SignedSDJWT` or an error. + */ func verifyIssuance( unverifiedSdJwt: String ) async throws -> Result + + /** + * Verifies the issuance of an SD-JWT from a `JSON` object. + * + * - Parameter unverifiedSdJwt: The unverified SD-JWT in `JSON` format. + * - Returns: A `Result` containing either the verified `SignedSDJWT` or an error. + */ func verifyIssuance( unverifiedSdJwt: JSON ) async throws -> Result } +/** + * A class for verifying SD-JWT Verifiable Credentials. + * This class verifies SD-JWT VCs by validating the JWT's signatures and + * using trust chains and metadata fetching. + */ public class SDJWTVCVerifier: SdJwtVcVerifierType { + /// X.509 certificate trust configuration used for verifying certificates. private let trust: X509CertificateTrust + + /// Optional service for fetching public keys from DID documents. private let lookup: LookupPublicKeysFromDIDDocument? + + /// Service for fetching issuer metadata such as public keys. private let fetcher: any SdJwtVcIssuerMetaDataFetching + /** + * Initializes the `SDJWTVCVerifier` with dependencies for metadata fetching, certificate trust, and public key lookup. + * + * - Parameters: + * - fetcher: A service responsible for fetching issuer metadata. + * - trust: The X.509 trust configuration. + * - lookup: Optional service for looking up public keys from DIDs or DID URLs. + */ public init( fetcher: SdJwtVcIssuerMetaDataFetching = SdJwtVcIssuerMetaDataFetcher( urlSession: .shared @@ -58,6 +101,12 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { self.lookup = lookup } + /** + * Verifies the issuance of an SD-JWT VC. + * + * - Parameter unverifiedSdJwt: The unverified SD-JWT in string format. + * - Returns: A `Result` containing either the verified `SignedSDJWT` or an error. + */ func verifyIssuance( unverifiedSdJwt: String ) async throws -> Result { @@ -86,6 +135,12 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { } } + /** + * Verifies the issuance of an SD-JWT VC. + * + * - Parameter unverifiedSdJwt: The unverified SD-JWT in `JSON` format. + * - Returns: A `Result` containing either the verified `SignedSDJWT` or an error. + */ func verifyIssuance( unverifiedSdJwt: JSON ) async throws -> Result { @@ -122,6 +177,16 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { } private extension SDJWTVCVerifier { + + /** + * Selects the issuer's public key from the JWS object based on metadata, X.509 certificates, or DID URLs. + * + * - Parameters: + * - jws: The JSON Web Signature object. + * - trust: The X.509 trust configuration. + * - lookup: Optional service for looking up public keys from DID documents. + * - Returns: A `Result` containing either the selected `JWK` or an error. + */ func issuerJwsKeySelector( jws: JWS, trust: X509CertificateTrust, @@ -170,6 +235,12 @@ private extension SDJWTVCVerifier { } } + /** + * Determines the source of the issuer's public key from the JWS object. + * + * - Parameter jws: The JSON Web Signature object. + * - Returns: An optional `SdJwtVcIssuerPublicKeySource` object. + */ func keySource(jws: JWS) throws -> SdJwtVcIssuerPublicKeySource? { guard let iss = try? jws.iss() else { From b0f3e6242ca97d0f32b0dafac3ad20f9bb6396fc Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Wed, 2 Oct 2024 13:26:54 +0300 Subject: [PATCH 14/25] [fix] x509 and issuer meta data unit tests --- Package.resolved | 10 +- Package.swift | 94 ++++++++++--------- .../SdJwtVcIssuerMetaDataFetcher.swift | 22 +++-- Sources/Networking/Networking.swift | 37 ++++++++ Sources/Types.swift | 1 + Sources/Verifier/SDJWTVCVerifier.swift | 4 +- Tests/Helpers/Constants.swift | 7 ++ .../LookupPublicKeysFromDIDDocumentMock.swift | 26 +++++ Tests/Mocks/NetworkingMock.swift | 58 ++++++++++++ Tests/Resources/issuer_meta_data.json | 14 +++ Tests/Stubs/Stubbable.swift | 20 ++++ Tests/Stubs/URL+Stub.swift | 28 ++++++ Tests/Verification/VcVerifierTest.swift | 86 +++++++++++++++++ Tests/Verification/VerifierTest.swift | 2 +- 14 files changed, 348 insertions(+), 61 deletions(-) create mode 100644 Sources/Networking/Networking.swift create mode 100644 Tests/Mocks/LookupPublicKeysFromDIDDocumentMock.swift create mode 100644 Tests/Mocks/NetworkingMock.swift create mode 100644 Tests/Resources/issuer_meta_data.json create mode 100644 Tests/Stubs/Stubbable.swift create mode 100644 Tests/Stubs/URL+Stub.swift create mode 100644 Tests/Verification/VcVerifierTest.swift diff --git a/Package.resolved b/Package.resolved index 155af77..f14c2e2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,17 +5,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { - "revision" : "c9c3df6ab812de32bae61fc0cd1bf6d45170ebf0", - "version" : "1.8.2" + "revision" : "678d442c6f7828def400a70ae15968aef67ef52d", + "version" : "1.8.3" } }, { "identity" : "jose-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/beatt83/jose-swift.git", + "location" : "https://github.com/dtsiflit/jose-swift.git", "state" : { - "revision" : "682002aae8d841d47bd9f641b8b85a233460d140", - "version" : "3.1.0" + "branch" : "fix/x509-chain-type", + "revision" : "7e5164025e89408398c415cff18dcb4e249d65f4" } }, { diff --git a/Package.swift b/Package.swift index 59265df..e2c9113 100644 --- a/Package.swift +++ b/Package.swift @@ -4,49 +4,53 @@ import PackageDescription let package = Package( - name: "eudi-lib-sdjwt-swift", - platforms: [ - .iOS(.v14), - .tvOS(.v12), - .watchOS(.v5), - .macOS(.v12) - - ], - products: [ - .library( - name: "eudi-lib-sdjwt-swift", - targets: ["eudi-lib-sdjwt-swift"]) - ], - dependencies: [ - .package( - url: "https://github.com/SwiftyJSON/SwiftyJSON.git", - from: "5.0.1" - ), - .package( - url: "https://github.com/beatt83/jose-swift.git", - from: "3.1.0" - ), - .package( - url: "https://github.com/apple/swift-certificates.git", - from: "1.0.0" - ) - ], - targets: [ - .target( - name: "eudi-lib-sdjwt-swift", - dependencies: [ - "jose-swift", - .product(name: "SwiftyJSON", package: "swiftyjson"), - .product(name: "X509", package: "swift-certificates"), - ], - path: "Sources", - plugins: [ - ] - ), - .testTarget( - name: "eudi-lib-sdjwt-swiftTests", - dependencies: ["eudi-lib-sdjwt-swift"], - path: "Tests") - - ] + name: "eudi-lib-sdjwt-swift", + platforms: [ + .iOS(.v14), + .tvOS(.v12), + .watchOS(.v5), + .macOS(.v12) + + ], + products: [ + .library( + name: "eudi-lib-sdjwt-swift", + targets: ["eudi-lib-sdjwt-swift"]) + ], + dependencies: [ + .package( + url: "https://github.com/SwiftyJSON/SwiftyJSON.git", + from: "5.0.1" + ), + .package( + url: "https://github.com/dtsiflit/jose-swift.git", + branch: "fix/x509-chain-type" + ), + .package( + url: "https://github.com/apple/swift-certificates.git", + from: "1.0.0" + ) + ], + targets: [ + .target( + name: "eudi-lib-sdjwt-swift", + dependencies: [ + "jose-swift", + .product(name: "SwiftyJSON", package: "swiftyjson"), + .product(name: "X509", package: "swift-certificates"), + ], + path: "Sources", + plugins: [ + ] + ), + .testTarget( + name: "eudi-lib-sdjwt-swiftTests", + dependencies: ["eudi-lib-sdjwt-swift"], + path: "Tests", + resources: [ + // Process or copy resources in your test target + .process("Resources") // Specify the folder containing resources for tests + ] + ) + ] ) diff --git a/Sources/Fetchers/SdJwtVcIssuerMetaDataFetcher.swift b/Sources/Fetchers/SdJwtVcIssuerMetaDataFetcher.swift index 447dc29..e625996 100644 --- a/Sources/Fetchers/SdJwtVcIssuerMetaDataFetcher.swift +++ b/Sources/Fetchers/SdJwtVcIssuerMetaDataFetcher.swift @@ -18,21 +18,24 @@ import SwiftyJSON import JSONWebKey public protocol SdJwtVcIssuerMetaDataFetching { - var urlSession: URLSession { get } + var session: Networking { get } func fetchIssuerMetaData(issuer: URL) async throws -> SdJwtVcIssuerMetaData? } public class SdJwtVcIssuerMetaDataFetcher: SdJwtVcIssuerMetaDataFetching { - public let urlSession: URLSession + public let session: Networking - public init(urlSession: URLSession) { - self.urlSession = urlSession + public init(session: Networking) { + self.session = session } public func fetchIssuerMetaData(issuer: URL) async throws -> SdJwtVcIssuerMetaData? { let issuerMetadataUrl = issuerMetadataUrl(for: issuer) - let metadata: SdJwtVcIssuerMetadataTO = try await fetch(from: issuerMetadataUrl) + let metadata: SdJwtVcIssuerMetadataTO = try await fetch( + from: issuerMetadataUrl, + with: session + ) guard issuer == URL(string: metadata.issuer) else { throw SDJWTVerifierError.invalidJwt @@ -46,7 +49,10 @@ public class SdJwtVcIssuerMetaDataFetcher: SdJwtVcIssuerMetaDataFetching { jwks: jwks.keys ) } else if metadata.jwksUri != nil { - let jwks: JWKSet = try await fetch(from: issuerMetadataUrl) + let jwks: JWKSet = try await fetch( + from: issuerMetadataUrl, + with: session + ) return .init( issuer: issuer, jwks: jwks.keys @@ -64,9 +70,9 @@ private extension SdJwtVcIssuerMetaDataFetcher { return components.url! } - func fetch(from url: URL) async throws -> T { + func fetch(from url: URL, with session: Networking) async throws -> T { - let (data, response) = try await URLSession.shared.data(from: url) + let (data, response) = try await session.data(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(.badServerResponse) diff --git a/Sources/Networking/Networking.swift b/Sources/Networking/Networking.swift new file mode 100644 index 0000000..aff93f6 --- /dev/null +++ b/Sources/Networking/Networking.swift @@ -0,0 +1,37 @@ +/* + * 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 + +extension URLSession: Networking {} + +public protocol Networking { + func data( + from url: URL + ) async throws -> (Data, URLResponse) + func data( + for request: URLRequest + ) async throws -> (Data, URLResponse) +} + +public extension Networking { + func data(from url: URL) async throws -> (Data, URLResponse) { + try await data(from: url) + } + + func data(for request: URLRequest) async throws -> (Data, URLResponse) { + try await data(for: request) + } +} diff --git a/Sources/Types.swift b/Sources/Types.swift index 53a2418..7364a87 100644 --- a/Sources/Types.swift +++ b/Sources/Types.swift @@ -35,6 +35,7 @@ public enum SDJWTError: Error { public enum SDJWTVerifierError: Error { case parsingError case invalidJwt + case invalidJwk case invalidIssuer case keyBindingFailed(description: String) case invalidDisclosure(disclosures: [Disclosure]) diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index b60a28c..0f9b2b4 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -91,7 +91,7 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { */ public init( fetcher: SdJwtVcIssuerMetaDataFetching = SdJwtVcIssuerMetaDataFetcher( - urlSession: .shared + session: URLSession.shared ), trust: X509CertificateTrust = X509CertificateTrustFactory.none, lookup: LookupPublicKeysFromDIDDocument? = nil @@ -206,7 +206,7 @@ private extension SDJWTVCVerifier { guard let jwk = try await fetcher.fetchIssuerMetaData( issuer: iss )?.jwks.first(where: { $0.keyID == kid }) else { - return .failure(SDJWTVerifierError.invalidJwt) + return .failure(SDJWTVerifierError.invalidJwk) } return .success(jwk) diff --git a/Tests/Helpers/Constants.swift b/Tests/Helpers/Constants.swift index efff808..86fc9f2 100644 --- a/Tests/Helpers/Constants.swift +++ b/Tests/Helpers/Constants.swift @@ -51,4 +51,11 @@ struct SDJWTConstants { static let signature = "ZfSxIFLHf7f84WIMqt7Fzme8-586WutjFnXH4TO5XuWG_peQ4hPsqDpiMBClkh2aUJdl83bwyyOriqvdFra-bg" static let protected = "eyJhbGciOiAiRVMyNTYiLCAidHlwIjogImV4YW1wbGUrc2Qtand0In0" + + static let x509_sd_jwt = """ + eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlEbmpDQ0FvYWdBd0lCQWdJVWVMVi9kOGpiOUJqODFDelZwNHJzN0pobnlnc3dEUVlKS29aSWh2Y05BUUVMQlFBd1d6RUxNQWtHQTFVRUJoTUNWVk14Q3pBSkJnTlZCQWdNQWtOQk1Rc3dDUVlEVlFRSERBSlRSakVZTUJZR0ExVUVDZ3dQU1c1MFpYSnRaV1JwWVhSbElFTkJNUmd3RmdZRFZRUUREQTlKYm5SbGNtMWxaR2xoZEdVZ1EwRXdIaGNOTWpReE1EQXlNRFkwTXpNMldoY05NalV4TURBeU1EWTBNek0yV2pCV01Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEN6QUpCZ05WQkFjTUFsTkdNUkl3RUFZRFZRUUtEQWxNWldGbUlFTmxjblF4R1RBWEJnTlZCQU1NRUd4bFlXWXVaWGhoYlhCc1pTNWpiMjB3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRFBBRFpRbTdiSWd1a2hERVFIbEhmdGQyVVJIQ3hYQ1QrMTJJc2NmaEhNSU4zS2tJdkNWTXZZQTlwMjBZZTJqeVd5UjJKZHdNZ2RQQnZBMWJJVXNnNENTaUE0M2sybWZnREo4NFRHQkNNK1BCYXQwbWhWZGc5QmtqTlBBb1BBWndSOHN5Q3B3NEszNVY4WmpmaEdJSUhVL1ZTdWsrTEFrc3l0MEpNdTAvMnFZK0l1VmtzWDh4UXlyV2lPOUlNQlZuK21JMmRsSWQwMmFzeDFxaGRkRkhPMXRSUGxTdHFpMGdGSjNtb0RDSW40dGR3d2lHV1lkUkFqalBvNVR2Wkw5SnVZQVZQS1VhdlpqZnpUSDZTeWU1dDBJVjR4MGxMSUppTUVrS0pnZGpwSmJ3OS8vL0V1ZVlVNENDYkNaR1N6THpZWTlwVTNJUWRHb05VWTdnckloYlZOQWdNQkFBR2pYekJkTUJzR0ExVWRFUVFVTUJLQ0VHeGxZV1l1WlhoaGJYQnNaUzVqYjIwd0hRWURWUjBPQkJZRUZOM3hSV3N6NHI5ZEZvSUgxNzNySGZvNEdEOWNNQjhHQTFVZEl3UVlNQmFBRkNkdTRQcXFPMHdLQTBKMWc0dTA0bzkycDRlc01BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQldJbU02TnBDaG5uUjJpUkp3M3d2SXZpTHIwWWNyZG41cTZoa3FQbk1JYXhaVy9lOFNDb29FT2o5TWpGMHJBc1RSQmxteUJJclJuTWw2b3hUNFBtVUhpVURPUXJ4Wk9saGRLOHJKVGZXVm5UVGErUFBibFZvZkdkellrQm1YVUNQYjdhY0JlYWJmQzRLNG92SEs2cFdQSzlJMUlGZmhWZ1hubHpBNWw5WE9nd1Q4OFNJbkFBRmVzRDMyNlVuTnRNUFNlb212MjI5Q1lVYU1QckhRL2RBYlBvajJnQkJCWFd0QkZSaWhMTURmWkZUQTNHZ0FhU2lWVUgwZ2tiUEtOY0R3NDRCMXpaNjdIaWFkZzBpQTBwaFRBWGxROGQxa3JaWVR6WUFlajZ3VVdoUytGRHRERTlYZE5hL3RCZEpuaXRjbFhtZnVxZDJZd1VJaUtTRUtPdGd0IiwiTUlJRGRqQ0NBbDZnQXdJQkFnSVVHYkRKQlFjbFh5YVZ1K1FXY1JnZDcrczVNU1V3RFFZSktvWklodmNOQVFFTEJRQXdTekVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUXN3Q1FZRFZRUUhEQUpUUmpFUU1BNEdBMVVFQ2d3SFVtOXZkQ0JEUVRFUU1BNEdBMVVFQXd3SFVtOXZkQ0JEUVRBZUZ3MHlOREV3TURJd05qUXpNRGxhRncweU9URXdNREV3TmpRek1EbGFNRnN4Q3pBSkJnTlZCQVlUQWxWVE1Rc3dDUVlEVlFRSURBSkRRVEVMTUFrR0ExVUVCd3dDVTBZeEdEQVdCZ05WQkFvTUQwbHVkR1Z5YldWa2FXRjBaU0JEUVRFWU1CWUdBMVVFQXd3UFNXNTBaWEp0WldScFlYUmxJRU5CTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF0dTVad3QzMzBCOE9Ccm50UU5IZWg5WDZScDZsbkxPaUErZzdIOThzWWlSN1JteTE1TVRLR3UrNFZhRmJPdVR6anBkUjk1NTlzT050L2RyMFdtTEt3bXhHamFYd3FMcFdHZmVsV1NtQnJuakZ4cjNFS0p2S0VCZU5CL2UyY2ViSjlSdU8vWGlLT0NKcFdGdlIxRlp1OFJtS1QySWFoZXlKVDhlQmx3Q1VKUzZvOGo0RkRTYXhtQThUbXo3Y1kwL1VjUnlnKzhZRVRxTDNGZkRZS0doM0NNU013RlVKb3F3UE9ZaHZlMGVRWWFGek9FTTVIUUJ4cFpOdnJZemVxTm42cmU0cjZpSHZyM2doQ3JaNW9tSnBzTVN5ZGEzeTRUMW1zOUl3TFZsWUlESVZsd0dHSmpZNGU2ejloWUpaYytQTmc4OUtGeTNMMWlhY29lVjFWc0dNNndJREFRQUJvMEl3UURBZEJnTlZIUTRFRmdRVUoyN2crcW83VEFvRFFuV0RpN1RpajNhbmg2d3dId1lEVlIwakJCZ3dGb0FVV1RzSVZBaHhIaTNObEVGSEx5YU12OFJqa2Nzd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFHZzNkMFJ3UlczbXdDN1BSZERlSExsYTZEcmhhTXRra2JkWHJqb3VjZHpkM0tmWlIrdTM1U1h4eXdxUkVHWit5cUhhQjFGOFovcXlPYVN1M3N6MjNYKyt0YWdiQ21qYklGSGdDSWpua0RsK2twWUVhOTdQYXBzSDJlMndNc095MDNVMjBXajFNUkpjT0ZrUk5sd014dWZoL05iY2pYejdGUHhOaDJ1OFluRER1R0VGVFM2L3g3WHFNMFlVOWRXNDUrdXV5V3BxTnFYcHRzL3d1SXNSZzk4ZXdHblhuMC80S0ttYjRaNGhmZ0xnNmdFU2NoYy8velJDekxqWjgvZ0lOSjFyUk00aUlrdUxIN240OFRPMm0yanlsWmRueGpoQmsvUmtuYVVSWFZ1Q0hlNW16Rks3enNKR3orOU9rY2tBYS9VUDdUa1ozTmJvY1BoUXZlU1k3UzA9IiwiTUlJRGR6Q0NBbCtnQXdJQkFnSVVRNXN2WDZ2RkpPMWY5TjMvczVvYXB4RXZwb2N3RFFZSktvWklodmNOQVFFTEJRQXdTekVMTUFrR0ExVUVCaE1DVlZNeEN6QUpCZ05WQkFnTUFrTkJNUXN3Q1FZRFZRUUhEQUpUUmpFUU1BNEdBMVVFQ2d3SFVtOXZkQ0JEUVRFUU1BNEdBMVVFQXd3SFVtOXZkQ0JEUVRBZUZ3MHlOREV3TURJd05qUXlORFphRncwek5EQTVNekF3TmpReU5EWmFNRXN4Q3pBSkJnTlZCQVlUQWxWVE1Rc3dDUVlEVlFRSURBSkRRVEVMTUFrR0ExVUVCd3dDVTBZeEVEQU9CZ05WQkFvTUIxSnZiM1FnUTBFeEVEQU9CZ05WQkFNTUIxSnZiM1FnUTBFd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURKNGxBOThlQVA3SGJ1emkvS1Nza1BwN0o4eUY0SEdSVzBZU0VRVWdId3dBbnlGZWVBUitZWVZRRmRnajJROU1FSGhjMGZlK25JWVpGSFNUVm8yQm8vNVJtcHN4RU9wNTB2SWNUYThIT0FtOFRvZWpsYzhMMWI4eTNnV1oyQUQ2NlVzeU5OZmJ2NEZPZlhFRGtLeXBsc1JIYkFMcVZDaWZ1T2xRMjZXTFdlTjNtQTJEZmVBbUxtWDZKQnBLejNEUE1ZS2RWdVFKWWZYUm9MWGRuNHRFQ1d0RkNOUm52ODRUMGE4bGUyZjJmRjBlNk9MN0JPSjJIcHo2R1FNakYxdTNkTGhwdmRCQVVBR0k5NkowQ1RrZVhsUDhadlZCZkNvK094alMrZXhRSitwV1NMSUU2WTJCMHEzbFNhTFRtK2JKMHd3Q042RTlMc3JxMVhzK09OdDJ4RkFnTUJBQUdqVXpCUk1CMEdBMVVkRGdRV0JCUlpPd2hVQ0hFZUxjMlVRVWN2Sm95L3hHT1J5ekFmQmdOVkhTTUVHREFXZ0JSWk93aFVDSEVlTGMyVVFVY3ZKb3kveEdPUnl6QVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BMEdDU3FHU0liM0RRRUJDd1VBQTRJQkFRQWErekNsU3VGMzZOV2JLR1AyTXVzQS9oYjRzdWxqaWVDOEg5RU9MVm9KeGNsZ0dnMnlXbG5tMlNQZXhRRGZTTGVtUjVJazZmemhCN1gzcnRSNmZTTm9vYk5Yd3hCTHhjVG41bzYxTm5McS8vOHF3QmVTUzdreWVmRU5nTVhtL2FONEk5YjhBTGRNdjFNWTg4aSt5cHlNTVlJc21QTy9yKzg3M1NKVXNTV2w2OENjZk8wYlRMaEphWS9BdXBRZDduaEorY0Jzak1HbDlscjhGVU9zeVN1TXljUGdNTzJuemhkUlRFSG01ZWFURkI0c1lpT2FvclZLVkdVNmJ2ZkE3Q1VqdHNpNEJFb3pvcWYrNDY2ajVBb01ZVmM4YlNwYkZ6QTdsYSt4ZGZ3ZlJRTEhSVUxZRnhMK0VobVpRYnVqNitkUytGcjQwTGZzQVlTak1aR3ZLUEoyIl19.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imh0dHBzOi8vbGVhZi5leGFtcGxlLmNvbSIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbIjN4YnVoa3R6UVZMR21rMEtrbnNMVUREQVJRNDNST2Y3YlNUWFc4OXdGVXMiXSwiTmF0aW9uYWxpdGVzIjpbeyIuLi4iOiJwZjRGTllTdVhZc0tDRFJEaXlVRmNFdGI4WHRXeWc3b2cydzhSelJFZ1lvIn0seyIuLi4iOiJtNUVZU1pRM2ZQUFhEUlJtOFVvT3IzTmJXdW1IUDNvemYzSzNkd0Zqai00In1dLCJhZHJlc3MiOnsibG9jYWxpdHkiOiJnciIsIl9zZCI6WyJFd2xqSE8teFF0N3g0UGFXcU14cl9ZOTQtYUNxZ0o3dGlxTl9TdmJIazlJIiwiV1dNS0R1bTBOcWRvZHdBSU9iaGFfSEZCWmNEanNMYmRLZTJGLUk4ODZjMCIsImJieWtDUXlYeDBBZmRrU0tfSkxfNlFDQ0hfY0hPX2lnOXdOU3AxeVNGV1EiLCJnbWZhWkF5N0xXSUgweU1LWDNmTGV1VEF1NFg5M2p4MW1wTDZFUmhvdjdJIiwibFNreWRCOEx2blhmU3FRMlZMZXlWVjFuMGgwUWJ0OHBBUjFLWkVXT0ZBayIsInRjZzNRLVdfalRzeWE0TV9pNFJEdnpjNjZjT2FHMjZkLWFmTTlOTFJWOE0iLCJ3UkkxYTY1ZE9NR3JOTzZFY0RoN0VBS0E4VDRfNGhVMzRKMnAyckRfVDFjIl19fQ.RG9UXLS6dC1ihYY9jut1a7hc4s_hOlYoAa4iwk-uFeMwz6muRvR-Hs7KyXGWSNfqmrGYHFmRWduTOH-93ZX34xA0q4P9fqOztMxbzp6Fex10NTosNpMCrNFnscsQY--4W4Dg5evQHSIiW0avzPzYvnTvMSFR7w4iigOj7WI1He_qI4YTATohT4HX26gTlDrIpuSS5iMlqLzsbjep3hA3DEc_LuFBZtDmsE3haGlIaE7Dg4GYlJH6GyHxu2HSei9T-Re-5jAPYHocXqukjgol4h8Go8wJdx-NaZ-aBcJzMtOQdzDj6sEqjQGwf6yh9jYs3CGVPYwvkwU3jltGm-E0ow~WyJSRGhCNm1YYTRvLXhtNWYzM1VKaGZnIiwiREUiXQ~ + """ + static let issuer_metadata_sd_jwt = """ + eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImEtWWFNdFYyRXZ2b09TZFZKUDdsSDU5VGQ5X2N0ckN2cUlVdTI0TEVIdXMiLCJ5IjoiUEV4aGsxb3pUUFNrN2VKc1laWFZwNVNFV01iaE9nZUhaR2dCbG5GaExqYyJ9LCJraWQiOiJBbzUwU3d6dl91V3U4MDVMY3VhVFR5c3VfNkd3b3FudkpoOXJuYzQ0VTQ4IiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imh0dHBzOi8vbGVhZi5leGFtcGxlLmNvbSIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbIjN4YnVoa3R6UVZMR21rMEtrbnNMVUREQVJRNDNST2Y3YlNUWFc4OXdGVXMiXSwiTmF0aW9uYWxpdGVzIjpbeyIuLi4iOiJwZjRGTllTdVhZc0tDRFJEaXlVRmNFdGI4WHRXeWc3b2cydzhSelJFZ1lvIn0seyIuLi4iOiJtNUVZU1pRM2ZQUFhEUlJtOFVvT3IzTmJXdW1IUDNvemYzSzNkd0Zqai00In1dLCJhZHJlc3MiOnsibG9jYWxpdHkiOiJnciIsIl9zZCI6WyJFd2xqSE8teFF0N3g0UGFXcU14cl9ZOTQtYUNxZ0o3dGlxTl9TdmJIazlJIiwiV1dNS0R1bTBOcWRvZHdBSU9iaGFfSEZCWmNEanNMYmRLZTJGLUk4ODZjMCIsImJieWtDUXlYeDBBZmRrU0tfSkxfNlFDQ0hfY0hPX2lnOXdOU3AxeVNGV1EiLCJnbWZhWkF5N0xXSUgweU1LWDNmTGV1VEF1NFg5M2p4MW1wTDZFUmhvdjdJIiwibFNreWRCOEx2blhmU3FRMlZMZXlWVjFuMGgwUWJ0OHBBUjFLWkVXT0ZBayIsInRjZzNRLVdfalRzeWE0TV9pNFJEdnpjNjZjT2FHMjZkLWFmTTlOTFJWOE0iLCJ3UkkxYTY1ZE9NR3JOTzZFY0RoN0VBS0E4VDRfNGhVMzRKMnAyckRfVDFjIl19fQ.qUqvjtwjFN36pYTEjChPo0xQ66M9GMogTYwfbddseqdhHcqNHWj_GQRdBUM5Gaf6RX3jyMNPYHsxcf15KsJX0Q~WyJSRGhCNm1YYTRvLXhtNWYzM1VKaGZnIiwiREUiXQ~ + """ } diff --git a/Tests/Mocks/LookupPublicKeysFromDIDDocumentMock.swift b/Tests/Mocks/LookupPublicKeysFromDIDDocumentMock.swift new file mode 100644 index 0000000..a771065 --- /dev/null +++ b/Tests/Mocks/LookupPublicKeysFromDIDDocumentMock.swift @@ -0,0 +1,26 @@ +/* + * 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 JSONWebKey +import XCTest + +@testable import eudi_lib_sdjwt_swift + +class LookupPublicKeysFromDIDDocumentMock: LookupPublicKeysFromDIDDocument { + func lookup(did: String, didUrl: String?) async -> [JWK]? { + [] + } +} diff --git a/Tests/Mocks/NetworkingMock.swift b/Tests/Mocks/NetworkingMock.swift new file mode 100644 index 0000000..af073cf --- /dev/null +++ b/Tests/Mocks/NetworkingMock.swift @@ -0,0 +1,58 @@ +/* + * 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 XCTest + +@testable import eudi_lib_sdjwt_swift + +class NetworkingMock: Networking { + + let path: String + let `extension`: String + let statusCode: Int + + init( + path: String, + `extension`: String, + statusCode: Int = 200 + ) { + self.path = path + self.extension = `extension` + self.statusCode = statusCode + } + + func data( + from url: URL + ) async throws -> (Data, URLResponse) { + let path = Bundle.module.path(forResource: self.path, ofType: self.extension) + let url = URL(fileURLWithPath: path!) + let data = try! Data(contentsOf: url) + let result = Result.success(data) + let response = HTTPURLResponse( + url: .stub(), + statusCode: statusCode, + httpVersion: nil, + headerFields: [:] + ) + return try (result.get(), response!) + } + + func data( + for request: URLRequest + ) async throws -> (Data, URLResponse) { + return try await data(from: URL(string: "https://www.example.com")!) + } +} diff --git a/Tests/Resources/issuer_meta_data.json b/Tests/Resources/issuer_meta_data.json new file mode 100644 index 0000000..0ad0d58 --- /dev/null +++ b/Tests/Resources/issuer_meta_data.json @@ -0,0 +1,14 @@ +{ + "issuer": "https://leaf.example.com", + "jwks": { + "keys": [ + { + "crv": "P-256", + "kid": "Ao50Swzv_uWu805LcuaTTysu_6GwoqnvJh9rnc44U48", + "kty": "EC", + "x": "a-YaMtV2EvvoOSdVJP7lH59Td9_ctrCvqIUu24LEHus", + "y": "PExhk1ozTPSk7eJsYZXVp5SEWMbhOgeHZGgBlnFhLjc" + } + ] + } +} diff --git a/Tests/Stubs/Stubbable.swift b/Tests/Stubs/Stubbable.swift new file mode 100644 index 0000000..5c2ce03 --- /dev/null +++ b/Tests/Stubs/Stubbable.swift @@ -0,0 +1,20 @@ +/* + * 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 + +protocol Stubbable: Identifiable { + static func stub() -> Self +} diff --git a/Tests/Stubs/URL+Stub.swift b/Tests/Stubs/URL+Stub.swift new file mode 100644 index 0000000..46905fa --- /dev/null +++ b/Tests/Stubs/URL+Stub.swift @@ -0,0 +1,28 @@ +/* + * 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 + +extension URL: @retroactive Identifiable {} +extension URL: Stubbable { + + public var id: ObjectIdentifier { + ObjectIdentifier(NSObject()) + } + + static func stub() -> URL { + return URL(string: "https://www.example.com/")! + } +} diff --git a/Tests/Verification/VcVerifierTest.swift b/Tests/Verification/VcVerifierTest.swift new file mode 100644 index 0000000..1f2a496 --- /dev/null +++ b/Tests/Verification/VcVerifierTest.swift @@ -0,0 +1,86 @@ +/* + * 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 JSONWebKey +import JSONWebSignature +import JSONWebToken +import SwiftyJSON +import XCTest + +@testable import eudi_lib_sdjwt_swift + + +final class VcVerifierTest: XCTestCase { + + override func setUp() async throws { + } + + override func tearDown() async throws { + } + + func testX509() async throws { + + let sdJwtString = SDJWTConstants.x509_sd_jwt.clean() + + let result = try await SDJWTVCVerifier( + fetcher: SdJwtVcIssuerMetaDataFetcher( + session: URLSession.shared + ), + trust: X509CertificateChainVerifier() + ) + .verifyIssuance( + unverifiedSdJwt: sdJwtString + ) + + XCTAssertNoThrow(try result.get()) + } + + func testIssuerMetaData() async throws { + + let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() + let result = try await SDJWTVCVerifier( + fetcher: SdJwtVcIssuerMetaDataFetcher( + session: NetworkingMock( + path: "issuer_meta_data", + extension: "json" + ) + ) + ) + .verifyIssuance( + unverifiedSdJwt: sdJwtString + ) + + XCTAssertNoThrow(try result.get()) + } + + func testDid() async throws { + + let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() + let result = try await SDJWTVCVerifier( + fetcher: SdJwtVcIssuerMetaDataFetcher( + session: NetworkingMock( + path: "issuer_meta_data", + extension: "json" + ) + ) + ) + .verifyIssuance( + unverifiedSdJwt: sdJwtString + ) + + XCTAssertNoThrow(try result.get()) + } +} diff --git a/Tests/Verification/VerifierTest.swift b/Tests/Verification/VerifierTest.swift index d5607e6..a169658 100644 --- a/Tests/Verification/VerifierTest.swift +++ b/Tests/Verification/VerifierTest.swift @@ -26,7 +26,7 @@ final class VerifierTest: XCTestCase { func testVerifierBehaviour_WhenPassedValidSignatures_ThenExpectToPassAllCriterias() throws { - let pk = try JSONDecoder.jwt.decode(JWK.self, from: key.tryToData()) + let pk = try JSONDecoder.jwt.decode(JWK.self, from: key.tryToData()) // Copied from Spec https://www.ietf.org/archive/id/draft-ietf-oauth-selective-disclosure-jwt-05.html#name-example-3-complex-structure let complexStructureSDJWTString = """ From 90b794e6586c20916b17576e678d8e272749e3c3 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Wed, 2 Oct 2024 13:46:36 +0300 Subject: [PATCH 15/25] [fix] updated DID unit test --- Sources/Verifier/SDJWTVCVerifier.swift | 4 +-- Tests/Helpers/Constants.swift | 14 ++++++++++ .../LookupPublicKeysFromDIDDocumentMock.swift | 6 +++-- Tests/Verification/VcVerifierTest.swift | 26 ++++++++++++------- 4 files changed, 36 insertions(+), 14 deletions(-) diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index 0f9b2b4..680e676 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -36,7 +36,7 @@ public protocol LookupPublicKeysFromDIDDocument { * - didUrl: The DID URL (optional). * - Returns: An array of JWKs (public keys) or `nil` if the lookup fails. */ - func lookup(did: String, didUrl: String?) async -> [JWK]? + func lookup(did: String, didUrl: String?) async throws -> [JWK]? } /** @@ -225,7 +225,7 @@ private extension SDJWTVCVerifier { } return .failure(SDJWTVerifierError.invalidJwt) case .didUrl(let iss, let kid): - guard let key = await lookup?.lookup( + guard let key = try await lookup?.lookup( did: iss, didUrl: kid )?.first(where: { $0.keyID == kid }) else { diff --git a/Tests/Helpers/Constants.swift b/Tests/Helpers/Constants.swift index 86fc9f2..ce7f760 100644 --- a/Tests/Helpers/Constants.swift +++ b/Tests/Helpers/Constants.swift @@ -58,4 +58,18 @@ struct SDJWTConstants { static let issuer_metadata_sd_jwt = """ eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImEtWWFNdFYyRXZ2b09TZFZKUDdsSDU5VGQ5X2N0ckN2cUlVdTI0TEVIdXMiLCJ5IjoiUEV4aGsxb3pUUFNrN2VKc1laWFZwNVNFV01iaE9nZUhaR2dCbG5GaExqYyJ9LCJraWQiOiJBbzUwU3d6dl91V3U4MDVMY3VhVFR5c3VfNkd3b3FudkpoOXJuYzQ0VTQ4IiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imh0dHBzOi8vbGVhZi5leGFtcGxlLmNvbSIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbIjN4YnVoa3R6UVZMR21rMEtrbnNMVUREQVJRNDNST2Y3YlNUWFc4OXdGVXMiXSwiTmF0aW9uYWxpdGVzIjpbeyIuLi4iOiJwZjRGTllTdVhZc0tDRFJEaXlVRmNFdGI4WHRXeWc3b2cydzhSelJFZ1lvIn0seyIuLi4iOiJtNUVZU1pRM2ZQUFhEUlJtOFVvT3IzTmJXdW1IUDNvemYzSzNkd0Zqai00In1dLCJhZHJlc3MiOnsibG9jYWxpdHkiOiJnciIsIl9zZCI6WyJFd2xqSE8teFF0N3g0UGFXcU14cl9ZOTQtYUNxZ0o3dGlxTl9TdmJIazlJIiwiV1dNS0R1bTBOcWRvZHdBSU9iaGFfSEZCWmNEanNMYmRLZTJGLUk4ODZjMCIsImJieWtDUXlYeDBBZmRrU0tfSkxfNlFDQ0hfY0hPX2lnOXdOU3AxeVNGV1EiLCJnbWZhWkF5N0xXSUgweU1LWDNmTGV1VEF1NFg5M2p4MW1wTDZFUmhvdjdJIiwibFNreWRCOEx2blhmU3FRMlZMZXlWVjFuMGgwUWJ0OHBBUjFLWkVXT0ZBayIsInRjZzNRLVdfalRzeWE0TV9pNFJEdnpjNjZjT2FHMjZkLWFmTTlOTFJWOE0iLCJ3UkkxYTY1ZE9NR3JOTzZFY0RoN0VBS0E4VDRfNGhVMzRKMnAyckRfVDFjIl19fQ.qUqvjtwjFN36pYTEjChPo0xQ66M9GMogTYwfbddseqdhHcqNHWj_GQRdBUM5Gaf6RX3jyMNPYHsxcf15KsJX0Q~WyJSRGhCNm1YYTRvLXhtNWYzM1VKaGZnIiwiREUiXQ~ """ + + static let did_sd_jwt = """ + eyJhbGciOiJFUzI1NiIsImp3ayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6ImEtWWFNdFYyRXZ2b09TZFZKUDdsSDU5VGQ5X2N0ckN2cUlVdTI0TEVIdXMiLCJ5IjoiUEV4aGsxb3pUUFNrN2VKc1laWFZwNVNFV01iaE9nZUhaR2dCbG5GaExqYyJ9LCJraWQiOiJBbzUwU3d6dl91V3U4MDVMY3VhVFR5c3VfNkd3b3FudkpoOXJuYzQ0VTQ4IiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6ImRpZDprZXk6bGVhZi5leGFtcGxlLmNvbSIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbIjN4YnVoa3R6UVZMR21rMEtrbnNMVUREQVJRNDNST2Y3YlNUWFc4OXdGVXMiXSwiTmF0aW9uYWxpdGVzIjpbeyIuLi4iOiJwZjRGTllTdVhZc0tDRFJEaXlVRmNFdGI4WHRXeWc3b2cydzhSelJFZ1lvIn0seyIuLi4iOiJtNUVZU1pRM2ZQUFhEUlJtOFVvT3IzTmJXdW1IUDNvemYzSzNkd0Zqai00In1dLCJhZHJlc3MiOnsibG9jYWxpdHkiOiJnciIsIl9zZCI6WyJFd2xqSE8teFF0N3g0UGFXcU14cl9ZOTQtYUNxZ0o3dGlxTl9TdmJIazlJIiwiV1dNS0R1bTBOcWRvZHdBSU9iaGFfSEZCWmNEanNMYmRLZTJGLUk4ODZjMCIsImJieWtDUXlYeDBBZmRrU0tfSkxfNlFDQ0hfY0hPX2lnOXdOU3AxeVNGV1EiLCJnbWZhWkF5N0xXSUgweU1LWDNmTGV1VEF1NFg5M2p4MW1wTDZFUmhvdjdJIiwibFNreWRCOEx2blhmU3FRMlZMZXlWVjFuMGgwUWJ0OHBBUjFLWkVXT0ZBayIsInRjZzNRLVdfalRzeWE0TV9pNFJEdnpjNjZjT2FHMjZkLWFmTTlOTFJWOE0iLCJ3UkkxYTY1ZE9NR3JOTzZFY0RoN0VBS0E4VDRfNGhVMzRKMnAyckRfVDFjIl19fQ._HsXhiv3PuCGHH2HpUfifJLuHb69nhB_YNgyzRmYZfZ9LkdnDHxnc8VKY-iFmbyflb0hb6XkM9P0fTuQTKsxGA~WyJSRGhCNm1YYTRvLXhtNWYzM1VKaGZnIiwiREUiXQ~ + """ + + static let did_key = """ + { + "crv": "P-256", + "kid": "Ao50Swzv_uWu805LcuaTTysu_6GwoqnvJh9rnc44U48", + "kty": "EC", + "x": "a-YaMtV2EvvoOSdVJP7lH59Td9_ctrCvqIUu24LEHus", + "y": "PExhk1ozTPSk7eJsYZXVp5SEWMbhOgeHZGgBlnFhLjc" + } + """ } diff --git a/Tests/Mocks/LookupPublicKeysFromDIDDocumentMock.swift b/Tests/Mocks/LookupPublicKeysFromDIDDocumentMock.swift index a771065..8bdc862 100644 --- a/Tests/Mocks/LookupPublicKeysFromDIDDocumentMock.swift +++ b/Tests/Mocks/LookupPublicKeysFromDIDDocumentMock.swift @@ -20,7 +20,9 @@ import XCTest @testable import eudi_lib_sdjwt_swift class LookupPublicKeysFromDIDDocumentMock: LookupPublicKeysFromDIDDocument { - func lookup(did: String, didUrl: String?) async -> [JWK]? { - [] + func lookup(did: String, didUrl: String?) async throws -> [JWK]? { + let data = SDJWTConstants.did_key.data(using: .utf8)! + let key = try JSONDecoder.jwt.decode(JWK.self, from: data) + return [key] } } diff --git a/Tests/Verification/VcVerifierTest.swift b/Tests/Verification/VcVerifierTest.swift index 1f2a496..9fe57bd 100644 --- a/Tests/Verification/VcVerifierTest.swift +++ b/Tests/Verification/VcVerifierTest.swift @@ -31,10 +31,12 @@ final class VcVerifierTest: XCTestCase { override func tearDown() async throws { } - func testX509() async throws { + func testVerifyIssuance_WithValidSDJWT_ShouldSucceed() async throws { + // Given let sdJwtString = SDJWTConstants.x509_sd_jwt.clean() + // When let result = try await SDJWTVCVerifier( fetcher: SdJwtVcIssuerMetaDataFetcher( session: URLSession.shared @@ -45,12 +47,16 @@ final class VcVerifierTest: XCTestCase { unverifiedSdJwt: sdJwtString ) + // Then XCTAssertNoThrow(try result.get()) } - func testIssuerMetaData() async throws { + func testVerifyIssuance_WithIssuerMetaData_ShouldSucceed() async throws { + // Given let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() + + // When let result = try await SDJWTVCVerifier( fetcher: SdJwtVcIssuerMetaDataFetcher( session: NetworkingMock( @@ -63,24 +69,24 @@ final class VcVerifierTest: XCTestCase { unverifiedSdJwt: sdJwtString ) + // Then XCTAssertNoThrow(try result.get()) } - func testDid() async throws { + func testVerifyIssuance_WithDID_ShouldSucceed() async throws { - let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() + // Given + let sdJwtString = SDJWTConstants.did_sd_jwt.clean() + + // When let result = try await SDJWTVCVerifier( - fetcher: SdJwtVcIssuerMetaDataFetcher( - session: NetworkingMock( - path: "issuer_meta_data", - extension: "json" - ) - ) + lookup: LookupPublicKeysFromDIDDocumentMock() ) .verifyIssuance( unverifiedSdJwt: sdJwtString ) + // Then XCTAssertNoThrow(try result.get()) } } From d78471ebe63598b65044cc61a44540122cafe035 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Wed, 2 Oct 2024 15:40:04 +0300 Subject: [PATCH 16/25] [fix] added some tests for json represented sdjwts --- Package.resolved | 6 +-- Package.swift | 7 ++- Sources/Parser/CompactParser.swift | 1 + Tests/Verification/VcVerifierTest.swift | 59 ++++++++++++++++++++++--- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/Package.resolved b/Package.resolved index f14c2e2..e80d228 100644 --- a/Package.resolved +++ b/Package.resolved @@ -12,10 +12,10 @@ { "identity" : "jose-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/dtsiflit/jose-swift.git", + "location" : "https://github.com/beatt83/jose-swift.git", "state" : { - "branch" : "fix/x509-chain-type", - "revision" : "7e5164025e89408398c415cff18dcb4e249d65f4" + "revision" : "c289a01f895501257e170d422a57a6edc593bbcd", + "version" : "4.0.0" } }, { diff --git a/Package.swift b/Package.swift index e2c9113..a24c0d7 100644 --- a/Package.swift +++ b/Package.swift @@ -23,8 +23,8 @@ let package = Package( from: "5.0.1" ), .package( - url: "https://github.com/dtsiflit/jose-swift.git", - branch: "fix/x509-chain-type" + url: "https://github.com/beatt83/jose-swift.git", + from: "4.0.0" ), .package( url: "https://github.com/apple/swift-certificates.git", @@ -48,8 +48,7 @@ let package = Package( dependencies: ["eudi-lib-sdjwt-swift"], path: "Tests", resources: [ - // Process or copy resources in your test target - .process("Resources") // Specify the folder containing resources for tests + .process("Resources") ] ) ] diff --git a/Sources/Parser/CompactParser.swift b/Sources/Parser/CompactParser.swift index 0e9cfdf..4f03ce7 100644 --- a/Sources/Parser/CompactParser.swift +++ b/Sources/Parser/CompactParser.swift @@ -29,6 +29,7 @@ public class CompactParser: ParserProtocol { var serialisedString: String var serialisationFormat: SerialisationFormat = .serialised + // MARK: - Lifecycle public required init(serialiserProtocol: SerialiserProtocol) { diff --git a/Tests/Verification/VcVerifierTest.swift b/Tests/Verification/VcVerifierTest.swift index 9fe57bd..e18194c 100644 --- a/Tests/Verification/VcVerifierTest.swift +++ b/Tests/Verification/VcVerifierTest.swift @@ -31,16 +31,13 @@ final class VcVerifierTest: XCTestCase { override func tearDown() async throws { } - func testVerifyIssuance_WithValidSDJWT_ShouldSucceed() async throws { + func testVerifyIssuance_WithValidSDJWT_Withx509Header_ShouldSucceed() async throws { // Given let sdJwtString = SDJWTConstants.x509_sd_jwt.clean() // When let result = try await SDJWTVCVerifier( - fetcher: SdJwtVcIssuerMetaDataFetcher( - session: URLSession.shared - ), trust: X509CertificateChainVerifier() ) .verifyIssuance( @@ -51,7 +48,7 @@ final class VcVerifierTest: XCTestCase { XCTAssertNoThrow(try result.get()) } - func testVerifyIssuance_WithIssuerMetaData_ShouldSucceed() async throws { + func testVerifyIssuance_WithValidSDJWT_WithIssuerMetaData_ShouldSucceed() async throws { // Given let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() @@ -73,7 +70,7 @@ final class VcVerifierTest: XCTestCase { XCTAssertNoThrow(try result.get()) } - func testVerifyIssuance_WithDID_ShouldSucceed() async throws { + func testVerifyIssuance_WithValidSDJWT_WithDID_ShouldSucceed() async throws { // Given let sdJwtString = SDJWTConstants.did_sd_jwt.clean() @@ -89,4 +86,54 @@ final class VcVerifierTest: XCTestCase { // Then XCTAssertNoThrow(try result.get()) } + + func testVerifyIssuance_WithValidSDJWTFlattendedJSON_Withx509Header_ShouldSucceed() async throws { + + // Given + let sdJwtString = SDJWTConstants.x509_sd_jwt.clean() + let parser = CompactParser(serialisedString: sdJwtString) + let sdJwt = try! parser.getSignedSdJwt() + + // When + let json = try sdJwt.asJwsJsonObject( + option: .flattened, + kbJwt: sdJwt.kbJwt?.compactSerialization, + getParts: parser.extractJWTParts + ) + + let result = try await SDJWTVCVerifier( + trust: X509CertificateChainVerifier() + ) + .verifyIssuance( + unverifiedSdJwt: json + ) + + // Then + XCTAssertNoThrow(try result.get()) + } + + func testVerifyIssuance_WithValidSDJWTGeneralJSON_Withx509Header_ShouldSucceed() async throws { + + // Given + let sdJwtString = SDJWTConstants.x509_sd_jwt.clean() + let parser = CompactParser(serialisedString: sdJwtString) + let sdJwt = try! parser.getSignedSdJwt() + + // When + let json = try sdJwt.asJwsJsonObject( + option: .general, + kbJwt: sdJwt.kbJwt?.compactSerialization, + getParts: parser.extractJWTParts + ) + + let result = try await SDJWTVCVerifier( + trust: X509CertificateChainVerifier() + ) + .verifyIssuance( + unverifiedSdJwt: json + ) + + // Then + XCTAssertNoThrow(try result.get()) + } } From 869f944bf14f07372f3f59ab33eedf9c95dfa504 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Wed, 2 Oct 2024 15:45:25 +0300 Subject: [PATCH 17/25] [fix] issuer metadata flattened test --- Tests/Verification/VcVerifierTest.swift | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/Tests/Verification/VcVerifierTest.swift b/Tests/Verification/VcVerifierTest.swift index e18194c..0bac520 100644 --- a/Tests/Verification/VcVerifierTest.swift +++ b/Tests/Verification/VcVerifierTest.swift @@ -136,4 +136,35 @@ final class VcVerifierTest: XCTestCase { // Then XCTAssertNoThrow(try result.get()) } + + func testVerifyIssuance_WithValidSDJWTFlattended_WithIssuerMetaData_ShouldSucceed() async throws { + + // Given + let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() + let parser = CompactParser(serialisedString: sdJwtString) + let sdJwt = try! parser.getSignedSdJwt() + + // When + let json = try sdJwt.asJwsJsonObject( + option: .general, + kbJwt: sdJwt.kbJwt?.compactSerialization, + getParts: parser.extractJWTParts + ) + + // When + let result = try await SDJWTVCVerifier( + fetcher: SdJwtVcIssuerMetaDataFetcher( + session: NetworkingMock( + path: "issuer_meta_data", + extension: "json" + ) + ) + ) + .verifyIssuance( + unverifiedSdJwt: json + ) + + // Then + XCTAssertNoThrow(try result.get()) + } } From 4998e781fe695b854a2de4084834d34424c3575a Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 3 Oct 2024 08:38:02 +0300 Subject: [PATCH 18/25] [fix] updated compact parser interface --- Sources/Parser/CompactParser.swift | 21 ++++++----- Sources/Parser/EnvelopedParser.swift | 35 ++++++++++--------- Sources/Parser/ParserProtocol.swift | 17 ++++++--- Sources/Verifier/DisclosuresVerifier.swift | 4 +-- Sources/Verifier/SDJWTVCVerifier.swift | 14 +++++--- Sources/Verifier/SDJWTVerifier.swift | 7 ++-- Tests/Issuance/SignedJwtTest.swift | 4 ++- .../JSONSerializationTest.swift | 8 ++--- Tests/SpecExamples.swift | 5 ++- Tests/Verification/SerializerTest.swift | 19 +++++----- Tests/Verification/VcVerifierTest.swift | 12 +++---- Tests/Verification/VerifierTest.swift | 30 ++++++++++------ 12 files changed, 104 insertions(+), 72 deletions(-) diff --git a/Sources/Parser/CompactParser.swift b/Sources/Parser/CompactParser.swift index 4f03ce7..4181e19 100644 --- a/Sources/Parser/CompactParser.swift +++ b/Sources/Parser/CompactParser.swift @@ -27,23 +27,22 @@ public class CompactParser: ParserProtocol { private static let TILDE = "~" - var serialisedString: String var serialisationFormat: SerialisationFormat = .serialised // MARK: - Lifecycle - public required init(serialiserProtocol: SerialiserProtocol) { - self.serialisedString = serialiserProtocol.serialised - } - - public init(serialisedString: String) { - self.serialisedString = serialisedString + public init() { } // MARK: - Methods - public func getSignedSdJwt() throws -> SignedSDJWT { - let (serialisedJWT, disclosuresInBase64, serialisedKBJWT) = try self.parseCombined() + public func getSignedSdJwt(using serialiserProtocol: SerialiserProtocol) throws -> SignedSDJWT { + let serialisedString = serialiserProtocol.serialised + return try getSignedSdJwt(serialisedString: serialisedString) + } + + public func getSignedSdJwt(serialisedString: String) throws -> SignedSDJWT { + let (serialisedJWT, disclosuresInBase64, serialisedKBJWT) = try self.parseCombined(serialisedString) return try SignedSDJWT(serializedJwt: serialisedJWT, disclosures: disclosuresInBase64, serializedKbJwt: serialisedKBJWT) } @@ -88,8 +87,8 @@ public class CompactParser: ParserProtocol { } - private func parseCombined() throws -> (String, [Disclosure], String?) { - let parts = self.serialisedString + private func parseCombined(_ serialisedString: String) throws -> (String, [Disclosure], String?) { + let parts = serialisedString .split(separator: "~") .map {String($0)} diff --git a/Sources/Parser/EnvelopedParser.swift b/Sources/Parser/EnvelopedParser.swift index 2b364c0..de5a75c 100644 --- a/Sources/Parser/EnvelopedParser.swift +++ b/Sources/Parser/EnvelopedParser.swift @@ -18,31 +18,32 @@ import Foundation public class EnvelopedParser: ParserProtocol { // MARK: - Properties + + let compactParser: ParserProtocol + + // MARK: - Lifecycle - var sdJwt: SignedSDJWT + public init( + compactParser: ParserProtocol = CompactParser() + ) { + self.compactParser = compactParser + } - // MARK: - Lifecycle + // MARK: - Methods - public init(serialiserProtocol: SerialiserProtocol) throws { + public func getSignedSdJwt(using serialiserProtocol: any SerialiserProtocol) throws -> SignedSDJWT { let jsonDecoder = JSONDecoder() let envelopedJwt = try jsonDecoder.decode(EnvelopedJwt.self, from: serialiserProtocol.data) - let compactParser = CompactParser(serialisedString: envelopedJwt.sdJwt) - self.sdJwt = try compactParser.getSignedSdJwt() + return try compactParser.getSignedSdJwt(serialisedString: envelopedJwt.sdJwt) } - - public init(data: Data) throws { + + public func getSignedSdJwt(serialisedString: String) throws -> SignedSDJWT { let jsonDecoder = JSONDecoder() - let envelopedJwt = try jsonDecoder.decode(EnvelopedJwt.self, from: data) - let compactParser = CompactParser(serialisedString: envelopedJwt.sdJwt) - self.sdJwt = try compactParser.getSignedSdJwt() + let envelopedJwt = try jsonDecoder.decode( + EnvelopedJwt.self, from: serialisedString.data(using: .utf8) ?? Data() + ) + return try compactParser.getSignedSdJwt(serialisedString: envelopedJwt.sdJwt) } - - // MARK: - Methods - - public func getSignedSdJwt() throws -> SignedSDJWT { - return sdJwt - } - } public struct EnvelopedJwt: Codable { diff --git a/Sources/Parser/ParserProtocol.swift b/Sources/Parser/ParserProtocol.swift index fddddf0..1acda25 100644 --- a/Sources/Parser/ParserProtocol.swift +++ b/Sources/Parser/ParserProtocol.swift @@ -16,15 +16,22 @@ import Foundation public protocol ParserProtocol { - - func getSignedSdJwt() throws -> SignedSDJWT - + // Existing method to support SerialiserProtocol + func getSignedSdJwt(using serialiserProtocol: SerialiserProtocol) throws -> SignedSDJWT + + // New method to support String input + func getSignedSdJwt(serialisedString: String) throws -> SignedSDJWT } struct NoParser: ParserProtocol { + var sdJWT: SignedSDJWT - - func getSignedSdJwt() throws -> SignedSDJWT { + + func getSignedSdJwt(using serialiserProtocol: any SerialiserProtocol) throws -> SignedSDJWT { + return self.sdJWT + } + + func getSignedSdJwt(serialisedString: String) throws -> SignedSDJWT { return self.sdJWT } } diff --git a/Sources/Verifier/DisclosuresVerifier.swift b/Sources/Verifier/DisclosuresVerifier.swift index 136f7d5..e636d10 100644 --- a/Sources/Verifier/DisclosuresVerifier.swift +++ b/Sources/Verifier/DisclosuresVerifier.swift @@ -62,8 +62,8 @@ public class DisclosuresVerifier: VerifierProtocol { recreatedClaims = claimExtractor.recreatedClaims } - convenience init(parser: ParserProtocol) throws { - try self.init(signedSDJWT: try parser.getSignedSdJwt()) + convenience init(parser: ParserProtocol, serialisedString: String) throws { + try self.init(signedSDJWT: try parser.getSignedSdJwt(serialisedString: serialisedString)) } // MARK: - Methods diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index 680e676..f364e34 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -81,21 +81,27 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { /// Service for fetching issuer metadata such as public keys. private let fetcher: any SdJwtVcIssuerMetaDataFetching + /// A parser conforming to `ParserProtocol`, responsible for parsing SD-JWTs. + private let parser: ParserProtocol + /** * Initializes the `SDJWTVCVerifier` with dependencies for metadata fetching, certificate trust, and public key lookup. * * - Parameters: + * - parser: A parser responsible for parsing SD-JWTs. * - fetcher: A service responsible for fetching issuer metadata. * - trust: The X.509 trust configuration. * - lookup: Optional service for looking up public keys from DIDs or DID URLs. */ public init( + parser: ParserProtocol = CompactParser(), fetcher: SdJwtVcIssuerMetaDataFetching = SdJwtVcIssuerMetaDataFetcher( session: URLSession.shared ), trust: X509CertificateTrust = X509CertificateTrustFactory.none, lookup: LookupPublicKeysFromDIDDocument? = nil ) { + self.parser = parser self.fetcher = fetcher self.trust = trust self.lookup = lookup @@ -110,8 +116,7 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { func verifyIssuance( unverifiedSdJwt: String ) async throws -> Result { - let parser = CompactParser(serialisedString: unverifiedSdJwt) - let jws = try parser.getSignedSdJwt().jwt + let jws = try parser.getSignedSdJwt(serialisedString: unverifiedSdJwt).jwt let jwk = try await issuerJwsKeySelector( jws: jws, trust: trust, @@ -121,9 +126,8 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { switch jwk { case .success(let jwk): return try SDJWTVerifier( - parser: CompactParser( - serialisedString: unverifiedSdJwt - ) + parser: parser, + serialisedString: unverifiedSdJwt ).verifyIssuance { jws in try SignatureVerifier( signedJWT: jws, diff --git a/Sources/Verifier/SDJWTVerifier.swift b/Sources/Verifier/SDJWTVerifier.swift index 6cde786..c4183cd 100644 --- a/Sources/Verifier/SDJWTVerifier.swift +++ b/Sources/Verifier/SDJWTVerifier.swift @@ -41,8 +41,11 @@ public class SDJWTVerifier { /// - parser: A parser conforming to `ParserProtocol`. /// - Throws: An error if the SDJWT cannot be obtained. /// - public init(parser: ParserProtocol) throws { - self.sdJwt = try parser.getSignedSdJwt() + public init( + parser: ParserProtocol = CompactParser(), + serialisedString: String + ) throws { + self.sdJwt = try parser.getSignedSdJwt(serialisedString: serialisedString) } /// Initializes the verifier with a pre-existing SDJWT. diff --git a/Tests/Issuance/SignedJwtTest.swift b/Tests/Issuance/SignedJwtTest.swift index fb4b1d1..8cd0a30 100644 --- a/Tests/Issuance/SignedJwtTest.swift +++ b/Tests/Issuance/SignedJwtTest.swift @@ -48,7 +48,9 @@ final class SignedJwtTest: XCTestCase { CompactSerialiser(signedSDJWT: jwt) } - let verifier = try SDJWTVerifier(parser: CompactParser(serialisedString: serialised)).verifyIssuance { jws in + let verifier = try SDJWTVerifier( + serialisedString: serialised + ).verifyIssuance { jws in try SignatureVerifier(signedJWT: jws, publicKey: keyPair.public) } claimVerifier: { _, _ in ClaimsVerifier() diff --git a/Tests/JSONSerialization/JSONSerializationTest.swift b/Tests/JSONSerialization/JSONSerializationTest.swift index 0cded15..334e717 100644 --- a/Tests/JSONSerialization/JSONSerializationTest.swift +++ b/Tests/JSONSerialization/JSONSerializationTest.swift @@ -31,8 +31,8 @@ final class JSONSerializationTest: XCTestCase { func testSdJWTGeneralSerialization() async throws { // Given - let parser = CompactParser(serialisedString: SDJWTConstants.compactSdJwt) - let sdJwt = try! parser.getSignedSdJwt() + let parser = CompactParser() + let sdJwt = try! parser.getSignedSdJwt(serialisedString: SDJWTConstants.compactSdJwt) // When let json = try sdJwt.asJwsJsonObject( @@ -69,8 +69,8 @@ final class JSONSerializationTest: XCTestCase { func testSdJWTFlattendedSerializationtest() async throws { // Given - let parser = CompactParser(serialisedString: SDJWTConstants.compactSdJwt) - let sdJwt = try! parser.getSignedSdJwt() + let parser = CompactParser() + let sdJwt = try! parser.getSignedSdJwt(serialisedString: SDJWTConstants.compactSdJwt) // When let json = try sdJwt.asJwsJsonObject( diff --git a/Tests/SpecExamples.swift b/Tests/SpecExamples.swift index b9e5376..a03c456 100644 --- a/Tests/SpecExamples.swift +++ b/Tests/SpecExamples.swift @@ -54,7 +54,10 @@ final class SpecExamples: XCTestCase { let string = CompactSerialiser(signedSDJWT: sdjwt).serialised - let disclosureVerifierOut = try DisclosuresVerifier(parser: CompactParser(serialisedString: string)).verify() + let disclosureVerifierOut = try DisclosuresVerifier( + parser: CompactParser(), + serialisedString: string + ).verify() validateObjectResults(factoryResult: output, expectedDigests: disclosureVerifierOut.digestsFoundOnPayload.count, diff --git a/Tests/Verification/SerializerTest.swift b/Tests/Verification/SerializerTest.swift index 233dd79..e8e75bc 100644 --- a/Tests/Verification/SerializerTest.swift +++ b/Tests/Verification/SerializerTest.swift @@ -34,21 +34,24 @@ final class SerialiserTest: XCTestCase { func testPareserWhenReceivingASerialisedFormatJWT_ThenConstructUnsignedSDJWT() throws { let serialisedString = try testSerializerWhenSerializedFormatIsSelected_ThenExpectSerialisedFormattedSignedSDJWT() - let parser = CompactParser(serialisedString: serialisedString) - let jwt = try parser.getSignedSdJwt().toSDJWT() + let parser = CompactParser() + let jwt = try parser.getSignedSdJwt(serialisedString: serialisedString).toSDJWT() print(jwt.disclosures) } func testSerialiseWhenChosingEnvelopeFormat_AppylingNoKeyBinding_ThenExpectACorrectJWT() throws { - let compactParser = try CompactParser(serialisedString: testSerializerWhenSerializedFormatIsSelected_ThenExpectSerialisedFormattedSignedSDJWT()) - + let compactParser = CompactParser() let envelopeSerializer = try EnvelopedSerialiser( - SDJWT: compactParser.getSignedSdJwt(), + SDJWT: compactParser.getSignedSdJwt( + serialisedString: testSerializerWhenSerializedFormatIsSelected_ThenExpectSerialisedFormattedSignedSDJWT() + ), jwTpayload: JWTBody(nonce: "", aud: "sub", iat: 1234).toJSONData()) - let parser = try EnvelopedParser(serialiserProtocol: envelopeSerializer) - - let verifier = try SDJWTVerifier(parser: parser).verifyIssuance { jws in + let parser = EnvelopedParser() + let verifier = try SDJWTVerifier( + parser: parser, + serialisedString: envelopeSerializer.serialised + ).verifyIssuance { jws in try SignatureVerifier(signedJWT: jws, publicKey: issuersKeyPair.public) } claimVerifier: { _, _ in ClaimsVerifier() diff --git a/Tests/Verification/VcVerifierTest.swift b/Tests/Verification/VcVerifierTest.swift index 0bac520..81cdb06 100644 --- a/Tests/Verification/VcVerifierTest.swift +++ b/Tests/Verification/VcVerifierTest.swift @@ -91,8 +91,8 @@ final class VcVerifierTest: XCTestCase { // Given let sdJwtString = SDJWTConstants.x509_sd_jwt.clean() - let parser = CompactParser(serialisedString: sdJwtString) - let sdJwt = try! parser.getSignedSdJwt() + let parser = CompactParser() + let sdJwt = try! parser.getSignedSdJwt(serialisedString: sdJwtString) // When let json = try sdJwt.asJwsJsonObject( @@ -116,8 +116,8 @@ final class VcVerifierTest: XCTestCase { // Given let sdJwtString = SDJWTConstants.x509_sd_jwt.clean() - let parser = CompactParser(serialisedString: sdJwtString) - let sdJwt = try! parser.getSignedSdJwt() + let parser = CompactParser() + let sdJwt = try! parser.getSignedSdJwt(serialisedString: sdJwtString) // When let json = try sdJwt.asJwsJsonObject( @@ -141,8 +141,8 @@ final class VcVerifierTest: XCTestCase { // Given let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() - let parser = CompactParser(serialisedString: sdJwtString) - let sdJwt = try! parser.getSignedSdJwt() + let parser = CompactParser() + let sdJwt = try! parser.getSignedSdJwt(serialisedString: sdJwtString) // When let json = try sdJwt.asJwsJsonObject( diff --git a/Tests/Verification/VerifierTest.swift b/Tests/Verification/VerifierTest.swift index a169658..412abab 100644 --- a/Tests/Verification/VerifierTest.swift +++ b/Tests/Verification/VerifierTest.swift @@ -61,7 +61,10 @@ final class VerifierTest: XCTestCase { AiV2VpZGVuc3RyYVx1MDBkZmUgMjIifV0~ """.clean() - let result = try SDJWTVerifier(parser: CompactParser(serialisedString: complexStructureSDJWTString)) + let result = try SDJWTVerifier( + parser: CompactParser(), + serialisedString: complexStructureSDJWTString + ) .verifyIssuance { jws in try SignatureVerifier(signedJWT: jws, publicKey: pk) } claimVerifier: { _, _ in @@ -70,8 +73,8 @@ final class VerifierTest: XCTestCase { XCTAssertNoThrow(try result.get()) - let recreatedClaimsResult = try CompactParser(serialisedString: complexStructureSDJWTString) - .getSignedSdJwt() + let recreatedClaimsResult = try CompactParser() + .getSignedSdJwt(serialisedString: complexStructureSDJWTString) .recreateClaims() XCTAssertTrue(recreatedClaimsResult.recreatedClaims.exists()) @@ -138,10 +141,12 @@ final class VerifierTest: XCTestCase { """ .clean() - let result = try SDJWTVerifier(parser: CompactParser(serialisedString: ComplexStructureSDJWTString)) - .unsingedVerify { signedSDJWT in - try DisclosuresVerifier(signedSDJWT: signedSDJWT) - } + let result = try SDJWTVerifier( + parser: CompactParser(), + serialisedString: ComplexStructureSDJWTString + ).unsingedVerify { signedSDJWT in + try DisclosuresVerifier(signedSDJWT: signedSDJWT) + } XCTAssertNoThrow(try result.get()) } @@ -337,10 +342,12 @@ final class VerifierTest: XCTestCase { func testSerialiseWhenChosingEnvelopeFormat_AppylingEnvelopeBinding_ThenExpectACorrectJWT() throws { let serializerTest = SerialiserTest() - let compactParser = try CompactParser(serialisedString: serializerTest.testSerializerWhenSerializedFormatIsSelected_ThenExpectSerialisedFormattedSignedSDJWT()) + let compactParser = CompactParser() let envelopeSerializer = try EnvelopedSerialiser( - SDJWT: compactParser.getSignedSdJwt(), + SDJWT: compactParser.getSignedSdJwt( + serialisedString: serializerTest.testSerializerWhenSerializedFormatIsSelected_ThenExpectSerialisedFormattedSignedSDJWT() + ), jwTpayload: JWTBody(nonce: "", aud: "sub", iat: 1234 ).toJSONData()) @@ -361,7 +368,10 @@ final class VerifierTest: XCTestCase { let envelopedJws = try JWS(jwsString: jwt.compactSerialization) let verifyEnvelope = - try SDJWTVerifier(parser: EnvelopedParser(data: envelopeSerializer.data)) + try SDJWTVerifier( + parser: EnvelopedParser(), + serialisedString: envelopeSerializer.serialised + ) .verifyEnvelope(envelope: envelopedJws) { jws in try SignatureVerifier(signedJWT: jws, publicKey: issuersKeyPair.public) From d7dea84397d015402a29b4b5425966141d26239d Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 3 Oct 2024 09:53:11 +0300 Subject: [PATCH 19/25] [wip] vc presentation verification --- Sources/Verifier/KeyBindingVerifier.swift | 35 +++++----- Sources/Verifier/SDJWTVCVerifier.swift | 79 +++++++++++++++++++++++ Tests/Verification/VcVerifierTest.swift | 22 +++++++ 3 files changed, 119 insertions(+), 17 deletions(-) diff --git a/Sources/Verifier/KeyBindingVerifier.swift b/Sources/Verifier/KeyBindingVerifier.swift index 8b21457..ca52e3f 100644 --- a/Sources/Verifier/KeyBindingVerifier.swift +++ b/Sources/Verifier/KeyBindingVerifier.swift @@ -19,42 +19,43 @@ import JSONWebSignature import SwiftyJSON public class KeyBindingVerifier: VerifierProtocol { - + let signatureVerifier: SignatureVerifier - - public init(iatOffset: TimeRange, - expectedAudience: String, - challenge: JWS, - extractedKey: JWK) throws { - - guard challenge.protectedHeader.type == "kb+jwt" else { + + public init( + iatOffset: TimeRange, + expectedAudience: String, + challenge: JWS, + extractedKey: JWK + ) throws { + guard challenge.protectedHeader.type == "kb+jwt" else { throw SDJWTVerifierError.keyBindingFailed(description: "no kb+jwt as typ claim") } - + let challengePayloadJson = try challenge.payloadJSON() - + guard let timeInterval = challengePayloadJson[Keys.iat].int else { throw SDJWTVerifierError.keyBindingFailed(description: "No iat claim Provided") } - + let aud = challengePayloadJson[Keys.aud] - + guard challengePayloadJson[Keys.nonce].exists() else { throw SDJWTVerifierError.keyBindingFailed(description: "No Nonce Provided") } - + self.signatureVerifier = try SignatureVerifier(signedJWT: challenge, publicKey: extractedKey) - + try verifyIat(iatOffset: iatOffset, iat: Date(timeIntervalSince1970: TimeInterval(timeInterval))) try verifyAud(aud: aud, expectedAudience: expectedAudience) } - + func verifyIat(iatOffset: TimeRange, iat: Date) throws { guard iatOffset.contains(date: iat) else { throw SDJWTVerifierError.keyBindingFailed(description: "iat not in valid time window") } } - + func verifyAud(aud: JSON, expectedAudience: String) throws { if let array = aud.array { guard array @@ -69,7 +70,7 @@ public class KeyBindingVerifier: VerifierProtocol { } } } - + @discardableResult public func verify() throws -> JWS { try signatureVerifier.verify() diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index f364e34..fa299a4 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -63,6 +63,18 @@ protocol SdJwtVcVerifierType { func verifyIssuance( unverifiedSdJwt: JSON ) async throws -> Result + + func verifyPresentation( + unverifiedSdJwt: String, + claimsVerifier: ClaimsVerifier, + keyBindingVerifier: KeyBindingVerifier? + ) async throws -> Result + + func verifyPresentation( + unverifiedSdJwt: JSON, + claimsVerifier: ClaimsVerifier, + keyBindingVerifier: KeyBindingVerifier? + ) async throws -> Result } /** @@ -178,6 +190,73 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { throw error } } + + func verifyPresentation( + unverifiedSdJwt: String, + claimsVerifier: ClaimsVerifier, + keyBindingVerifier: KeyBindingVerifier? = nil + ) async throws -> Result { + let jws = try parser.getSignedSdJwt(serialisedString: unverifiedSdJwt).jwt + let jwk = try await issuerJwsKeySelector( + jws: jws, + trust: trust, + lookup: lookup + ) + + switch jwk { + case .success(let jwk): + return try SDJWTVerifier( + parser: parser, + serialisedString: unverifiedSdJwt + ).verifyPresentation { jws in + try SignatureVerifier( + signedJWT: jws, + publicKey: jwk + ) + } claimVerifier: { _, _ in + claimsVerifier + } + case .failure(let error): + throw error + } + } + + func verifyPresentation( + unverifiedSdJwt: JSON, + claimsVerifier: ClaimsVerifier, + keyBindingVerifier: KeyBindingVerifier? + ) 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 SDJWTVerifier( + sdJwt: sdJwt + ).verifyPresentation { jws in + try SignatureVerifier( + signedJWT: jws, + publicKey: jwk + ) + } claimVerifier: { _, _ in + claimsVerifier + } + case .failure(let error): + throw error + } + } } private extension SDJWTVCVerifier { diff --git a/Tests/Verification/VcVerifierTest.swift b/Tests/Verification/VcVerifierTest.swift index 81cdb06..3bcb13b 100644 --- a/Tests/Verification/VcVerifierTest.swift +++ b/Tests/Verification/VcVerifierTest.swift @@ -167,4 +167,26 @@ final class VcVerifierTest: XCTestCase { // Then XCTAssertNoThrow(try result.get()) } + + func testPresentation() async throws { + + // Given + let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() + + // When + let result = try await SDJWTVCVerifier( + fetcher: SdJwtVcIssuerMetaDataFetcher( + session: NetworkingMock( + path: "issuer_meta_data", + extension: "json" + ) + ) + ).verifyPresentation( + unverifiedSdJwt: sdJwtString, + claimsVerifier: ClaimsVerifier() + ) + + // Then + XCTAssertNoThrow(try result.get()) + } } From a09b7ef96817482b1fc5a87761547e2c4cfbebbe Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 3 Oct 2024 13:07:47 +0300 Subject: [PATCH 20/25] [fix] claims verifier fixes --- Sources/Verifier/ClaimsVerifier.swift | 46 ++++++++++++++------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/Sources/Verifier/ClaimsVerifier.swift b/Sources/Verifier/ClaimsVerifier.swift index 391dcff..e14ea08 100644 --- a/Sources/Verifier/ClaimsVerifier.swift +++ b/Sources/Verifier/ClaimsVerifier.swift @@ -17,28 +17,30 @@ import Foundation import SwiftyJSON public class ClaimsVerifier: VerifierProtocol { - + // MARK: - Properties var iat: Date? var iatValidWindow: TimeRange? - + var nbf: Date? var exp: Date? - + var audClaim: JSON? var expectedAud: String? - + let currentDate: Date + // MARK: - Lifecycle - - public init(iat: Int? = nil, - iatValidWindow: TimeRange? = nil, - nbf: Int? = nil, - exp: Int? = nil, - audClaim: String? = nil, - expectedAud: String? = nil, - currentDate: Date = Date()) { - + + public init( + iat: Int? = nil, + iatValidWindow: TimeRange? = nil, + nbf: Int? = nil, + exp: Int? = nil, + audClaim: String? = nil, + expectedAud: String? = nil, + currentDate: Date = Date()) { + if let iat { self.iat = Date(timeIntervalSince1970: TimeInterval(iat)) } @@ -48,12 +50,12 @@ public class ClaimsVerifier: VerifierProtocol { if let exp { self.exp = Date(timeIntervalSince1970: TimeInterval(exp)) } - + self.audClaim = JSON(parseJSON: audClaim ?? "") self.expectedAud = expectedAud self.currentDate = currentDate } - + // MARK: - Methods @discardableResult public func verify() throws -> Bool { @@ -62,23 +64,23 @@ public class ClaimsVerifier: VerifierProtocol { iatValidWindow.contains(date: iat) { throw SDJWTVerifierError.invalidJwt } - + if let nbf { try self.verifyNotBefore(nbf: nbf) } - + if let exp { try self.verifyNotExpired(exp: exp) } - + if let expectedAud, let audClaim { try self.verifyAud(aud: audClaim, expectedAudience: expectedAud) } - + return true } - + private func verifyNotBefore(nbf: Date) throws { switch nbf.compare(currentDate) { case .orderedDescending: @@ -87,7 +89,7 @@ public class ClaimsVerifier: VerifierProtocol { break } } - + private func verifyNotExpired(exp: Date) throws { switch exp.compare(currentDate) { case .orderedAscending, .orderedSame: @@ -96,7 +98,7 @@ public class ClaimsVerifier: VerifierProtocol { break } } - + func verifyAud(aud: JSON, expectedAudience: String) throws { if let array = aud.array { guard array From e395c7acda374bc64b03ac49ce11baf42d52d1f2 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Thu, 3 Oct 2024 13:25:33 +0300 Subject: [PATCH 21/25] [fix] key binding verifier updates --- Sources/Verifier/KeyBindingVerifier.swift | 40 +++++++++++++++++++---- Sources/Verifier/SDJWTVCVerifier.swift | 12 +++++++ Sources/Verifier/SDJWTVerifier.swift | 9 +++-- Tests/Helpers/Constants.swift | 4 +++ Tests/Verification/VcVerifierTest.swift | 39 +++++++++++++++++++--- Tests/Verification/VerifierTest.swift | 4 ++- 6 files changed, 94 insertions(+), 14 deletions(-) diff --git a/Sources/Verifier/KeyBindingVerifier.swift b/Sources/Verifier/KeyBindingVerifier.swift index ca52e3f..b0933e0 100644 --- a/Sources/Verifier/KeyBindingVerifier.swift +++ b/Sources/Verifier/KeyBindingVerifier.swift @@ -20,9 +20,12 @@ import SwiftyJSON public class KeyBindingVerifier: VerifierProtocol { - let signatureVerifier: SignatureVerifier + private var signatureVerifier: SignatureVerifier? - public init( + public init() { + } + + public func verify( iatOffset: TimeRange, expectedAudience: String, challenge: JWS, @@ -50,6 +53,34 @@ public class KeyBindingVerifier: VerifierProtocol { try verifyAud(aud: aud, expectedAudience: expectedAudience) } + public func verify( + challenge: JWS, + extractedKey: JWK + ) throws { + guard challenge.protectedHeader.type == "kb+jwt" else { + throw SDJWTVerifierError.keyBindingFailed(description: "no kb+jwt as typ claim") + } + + let challengePayloadJson = try challenge.payloadJSON() + + guard challengePayloadJson[Keys.nonce].exists() else { + throw SDJWTVerifierError.keyBindingFailed(description: "No Nonce Provided") + } + + self.signatureVerifier = try SignatureVerifier(signedJWT: challenge, publicKey: extractedKey) + } + + @discardableResult + public func verify() throws -> JWS { + guard let verifier = signatureVerifier else { + throw SDJWTVerifierError.keyBindingFailed(description: "Invalid signature verifier") + } + return try verifier.verify() + } +} + +private extension KeyBindingVerifier { + func verifyIat(iatOffset: TimeRange, iat: Date) throws { guard iatOffset.contains(date: iat) else { throw SDJWTVerifierError.keyBindingFailed(description: "iat not in valid time window") @@ -70,9 +101,4 @@ public class KeyBindingVerifier: VerifierProtocol { } } } - - @discardableResult - public func verify() throws -> JWS { - try signatureVerifier.verify() - } } diff --git a/Sources/Verifier/SDJWTVCVerifier.swift b/Sources/Verifier/SDJWTVCVerifier.swift index fa299a4..95cbff0 100644 --- a/Sources/Verifier/SDJWTVCVerifier.swift +++ b/Sources/Verifier/SDJWTVCVerifier.swift @@ -215,6 +215,12 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { ) } claimVerifier: { _, _ in claimsVerifier + } keyBindingVerifier: { jws, jwk in + try keyBindingVerifier?.verify( + challenge: jws, + extractedKey: jwk + ) + return keyBindingVerifier } case .failure(let error): throw error @@ -252,6 +258,12 @@ public class SDJWTVCVerifier: SdJwtVcVerifierType { ) } claimVerifier: { _, _ in claimsVerifier + } keyBindingVerifier: { jws, jwk in + try keyBindingVerifier?.verify( + challenge: jws, + extractedKey: jwk + ) + return keyBindingVerifier } case .failure(let error): throw error diff --git a/Sources/Verifier/SDJWTVerifier.swift b/Sources/Verifier/SDJWTVerifier.swift index c4183cd..6d005f3 100644 --- a/Sources/Verifier/SDJWTVerifier.swift +++ b/Sources/Verifier/SDJWTVerifier.swift @@ -91,7 +91,7 @@ public class SDJWTVerifier { public func verifyPresentation( issuersSignatureVerifier: (JWS) throws -> SignatureVerifier, claimVerifier: ((_ nbf: Int?, _ exp: Int?) throws -> ClaimsVerifier)? = nil, - keyBindingVerifier: ((JWS, JWK) throws -> KeyBindingVerifier)? = nil + keyBindingVerifier: ((JWS, JWK) throws -> KeyBindingVerifier?)? = nil ) -> Result { Result { let commonVerifyResult = self.verify(issuersSignatureVerifier: issuersSignatureVerifier, claimVerifier: claimVerifier) @@ -102,7 +102,12 @@ public class SDJWTVerifier { throw SDJWTVerifierError.keyBindingFailed(description: "No KB provided") } let extractedKey = try sdjwt.extractHoldersPublicKey() - try keyBindingVerifier(kbJwt, extractedKey).verify() + + guard let result = try keyBindingVerifier(kbJwt, extractedKey) else { + throw SDJWTVerifierError.keyBindingFailed(description: "No key binding verifier provided") + } + + try result.verify() if let sdHash = try? kbJwt.payloadJSON()["sd_hash"].stringValue { if sdHash != sdjwt.delineatedCompactSerialisation { diff --git a/Tests/Helpers/Constants.swift b/Tests/Helpers/Constants.swift index ce7f760..fc12edb 100644 --- a/Tests/Helpers/Constants.swift +++ b/Tests/Helpers/Constants.swift @@ -72,4 +72,8 @@ struct SDJWTConstants { "y": "PExhk1ozTPSk7eJsYZXVp5SEWMbhOgeHZGgBlnFhLjc" } """ + + static let presentation_sd_jwt = """ +eyJhbGciOiJFUzI1NiIsImNuZiI6eyJqd2siOnsiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyIsIngiOiJSbWVCZmhsTVZjcVlJckl0VmlWRE82bVV2WTh4UVJ1UFktY3JXT095MGswIiwieSI6IlliSTRZSGwzSHU2TldZYWpaTGN1M1dfd29NZnR1NzRlR2hlbnB6cVk2X3MifX0sImtpZCI6IkFvNTBTd3p2X3VXdTgwNUxjdWFUVHlzdV82R3dvcW52Smg5cm5jNDRVNDgiLCJ0eXAiOiJKV1QifQ.eyJjbmYiOnsiandrIjp7InkiOiJZYkk0WUhsM0h1Nk5XWWFqWkxjdTNXX3dvTWZ0dTc0ZUdoZW5wenFZNl9zIiwia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJSbWVCZmhsTVZjcVlJckl0VmlWRE82bVV2WTh4UVJ1UFktY3JXT095MGswIn19LCJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlzcyI6Imh0dHBzOi8vbGVhZi5leGFtcGxlLmNvbSIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbIjN4YnVoa3R6UVZMR21rMEtrbnNMVUREQVJRNDNST2Y3YlNUWFc4OXdGVXMiXSwiTmF0aW9uYWxpdGVzIjpbeyIuLi4iOiJwZjRGTllTdVhZc0tDRFJEaXlVRmNFdGI4WHRXeWc3b2cydzhSelJFZ1lvIn0seyIuLi4iOiJtNUVZU1pRM2ZQUFhEUlJtOFVvT3IzTmJXdW1IUDNvemYzSzNkd0Zqai00In1dLCJhZHJlc3MiOnsibG9jYWxpdHkiOiJnciIsIl9zZCI6WyJFd2xqSE8teFF0N3g0UGFXcU14cl9ZOTQtYUNxZ0o3dGlxTl9TdmJIazlJIiwiV1dNS0R1bTBOcWRvZHdBSU9iaGFfSEZCWmNEanNMYmRLZTJGLUk4ODZjMCIsImJieWtDUXlYeDBBZmRrU0tfSkxfNlFDQ0hfY0hPX2lnOXdOU3AxeVNGV1EiLCJnbWZhWkF5N0xXSUgweU1LWDNmTGV1VEF1NFg5M2p4MW1wTDZFUmhvdjdJIiwibFNreWRCOEx2blhmU3FRMlZMZXlWVjFuMGgwUWJ0OHBBUjFLWkVXT0ZBayIsInRjZzNRLVdfalRzeWE0TV9pNFJEdnpjNjZjT2FHMjZkLWFmTTlOTFJWOE0iLCJ3UkkxYTY1ZE9NR3JOTzZFY0RoN0VBS0E4VDRfNGhVMzRKMnAyckRfVDFjIl19fQ.48XmC70TpafGTy5k-VSq4CcjBeQbO_p1_j0GiLS12JNSraQ35Li2y1_kcxAYFcIOw54e8guhk_SR_N0oy0pPMg~WyJSRGhCNm1YYTRvLXhtNWYzM1VKaGZnIiwiREUiXQ~eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiK2p3dCJ9.eyJub25jZSI6IjEyMzQ1Njc4OSIsImF1ZCI6ImV4YW1wbGUuY29tIiwiaWF0IjoxNzI3OTQ1ODg2LCJzZF9oYXNoIjoiTWdaUmM0bzRBWlltTGtjcmZYSEhFaU5jNm9XeFZNNDV3WnpKNkxJU0FJcyJ9.3-Nt2fgvQKSXJpg0ZpASdwD0th0qxibJHSwndjNlJ0yPfh7F6-hfQ74bHOzftx-IRBAJdAyLYnsIQkkJpzYTAQ +""" } diff --git a/Tests/Verification/VcVerifierTest.swift b/Tests/Verification/VcVerifierTest.swift index 3bcb13b..76a88d2 100644 --- a/Tests/Verification/VcVerifierTest.swift +++ b/Tests/Verification/VcVerifierTest.swift @@ -151,7 +151,6 @@ final class VcVerifierTest: XCTestCase { getParts: parser.extractJWTParts ) - // When let result = try await SDJWTVCVerifier( fetcher: SdJwtVcIssuerMetaDataFetcher( session: NetworkingMock( @@ -168,10 +167,10 @@ final class VcVerifierTest: XCTestCase { XCTAssertNoThrow(try result.get()) } - func testPresentation() async throws { + func testVerifyPresentation_WithValidSDJWTPresentation_ShouldSucceed() async throws { // Given - let sdJwtString = SDJWTConstants.issuer_metadata_sd_jwt.clean() + let sdJwtString = SDJWTConstants.presentation_sd_jwt.clean() // When let result = try await SDJWTVCVerifier( @@ -183,7 +182,39 @@ final class VcVerifierTest: XCTestCase { ) ).verifyPresentation( unverifiedSdJwt: sdJwtString, - claimsVerifier: ClaimsVerifier() + claimsVerifier: ClaimsVerifier(), + keyBindingVerifier: KeyBindingVerifier() + ) + + // Then + XCTAssertNoThrow(try result.get()) + } + + func testVerifyPresentation_WithValidSDJWT_AsFlattendedJSON_Presentation_ShouldSucceed() async throws { + + // Given + let sdJwtString = SDJWTConstants.presentation_sd_jwt.clean() + let parser = CompactParser() + let sdJwt = try! parser.getSignedSdJwt(serialisedString: sdJwtString) + + // When + let json = try sdJwt.asJwsJsonObject( + option: .general, + kbJwt: sdJwt.kbJwt?.compactSerialization, + getParts: parser.extractJWTParts + ) + + let result = try await SDJWTVCVerifier( + fetcher: SdJwtVcIssuerMetaDataFetcher( + session: NetworkingMock( + path: "issuer_meta_data", + extension: "json" + ) + ) + ).verifyPresentation( + unverifiedSdJwt: json, + claimsVerifier: ClaimsVerifier(), + keyBindingVerifier: KeyBindingVerifier() ) // Then diff --git a/Tests/Verification/VerifierTest.swift b/Tests/Verification/VerifierTest.swift index 412abab..3cfb507 100644 --- a/Tests/Verification/VerifierTest.swift +++ b/Tests/Verification/VerifierTest.swift @@ -324,7 +324,8 @@ final class VerifierTest: XCTestCase { ClaimsVerifier() } keyBindingVerifier: { jws, holdersPublicKey in - try KeyBindingVerifier( + let verifier = KeyBindingVerifier() + try verifier.verify( iatOffset: .init( startTime: Date(timeIntervalSince1970: 1694600000 - 1000), endTime: Date(timeIntervalSince1970: 1694600000) @@ -333,6 +334,7 @@ final class VerifierTest: XCTestCase { challenge: jws, extractedKey: holdersPublicKey ) + return verifier } XCTAssertEqual(sdHash, holder.delineatedCompactSerialisation) From 6bec59aee1d26d9749b6bab175a017e08dcf74d9 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 4 Oct 2024 13:57:55 +0300 Subject: [PATCH 22/25] [fix] updated readme with vc verifier --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 256a53f..1d8463f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ is implemented in Swift. Issuer - [Presentation Verification](#presentation-verification): As a Verifier verify SD-JWT in Combined Presentation Format or in Envelope Format - [Recreate initial claims](#recreate-original-claims): Given a SD-JWT recreate the original claims +* [SD-JWT VC support](#sd-jwt-vc-support) ## Issuance @@ -221,6 +222,16 @@ All examples assume that we have the following claim set - [Example 2a: Handling Structured Claims](Docs/examples/example2a-handling-structure-claims.md) - [Example 3: Complex Structured SD-JWT](Docs/examples/example3-complex-structured.md) +## SD-JWT VC support + +The library support verifying +[SD-JWT-based Verifiable Credentials](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html). +More specifically, Issuer-signed JWT Verification Key Validation support is provided by +[SDJWTVerifier](Sources/Verifier/SDJWTVerifier.swift). +Please check [VcVerifierTest](Tests/Verification/VcVerifierTest.swift) for code examples of +verifying an Issuance SD-JWT VC and a Presentation SD-JWT VC (including verification of the Key Binding JWT). + + ## How to contribute We welcome contributions to this project. To ensure that the process is smooth for everyone From 43e5b1a264d53da1f805b7d38a16bf48dd767a46 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 4 Oct 2024 14:03:18 +0300 Subject: [PATCH 23/25] [fix] updated readme --- README.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1d8463f..f500dbb 100644 --- a/README.md +++ b/README.md @@ -224,12 +224,8 @@ All examples assume that we have the following claim set ## SD-JWT VC support -The library support verifying -[SD-JWT-based Verifiable Credentials](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html). -More specifically, Issuer-signed JWT Verification Key Validation support is provided by -[SDJWTVerifier](Sources/Verifier/SDJWTVerifier.swift). -Please check [VcVerifierTest](Tests/Verification/VcVerifierTest.swift) for code examples of -verifying an Issuance SD-JWT VC and a Presentation SD-JWT VC (including verification of the Key Binding JWT). +The library supports verifying [SD-JWT-based Verifiable Credentials](https://www.ietf.org/archive/id/draft-ietf-oauth-sd-jwt-vc-04.html). +More specifically, Issuer-signed JWT Verification Key Validation support is provided by [SDJWTVerifier](Sources/Verifier/SDJWTVerifier.swift). Please check [VcVerifierTest](Tests/Verification/VcVerifierTest.swift) for code examples of verifying an Issuance SD-JWT VC and a Presentation SD-JWT VC (including verification of the Key Binding JWT). ## How to contribute From 2f81efba72208609c9f36ee17b262e782fd1e7be Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 4 Oct 2024 14:07:37 +0300 Subject: [PATCH 24/25] [fix] updated actions file --- .github/workflows/build-eudi-lib-sdjwt-swift.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-eudi-lib-sdjwt-swift.yml b/.github/workflows/build-eudi-lib-sdjwt-swift.yml index a568422..2529b57 100644 --- a/.github/workflows/build-eudi-lib-sdjwt-swift.yml +++ b/.github/workflows/build-eudi-lib-sdjwt-swift.yml @@ -8,11 +8,11 @@ tags: [ v* ] jobs: build: - runs-on: "macos-13" + runs-on: "macos-14" steps: - uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '15.0' + xcode-version: '16.0' - uses: actions/checkout@v4 - run: fastlane tests \ No newline at end of file From c3dfa1908acb52e9c9556f691c499fafaecbe6f1 Mon Sep 17 00:00:00 2001 From: dtsiflit Date: Fri, 4 Oct 2024 14:19:40 +0300 Subject: [PATCH 25/25] [fix] bundle update --- Gemfile.lock | 117 +++++++++++----------- Sources/Issuer/JWT.swift | 4 +- Sources/Verifier/KeyBindingVerifier.swift | 6 +- 3 files changed, 66 insertions(+), 61 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3b70b21..eed0ff4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,29 +1,32 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - artifactory (3.0.15) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.825.0) - aws-sdk-core (3.182.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.984.0) + aws-sdk-core (3.209.1) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.9) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.71.0) - aws-sdk-core (~> 3, >= 3.177.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.134.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-kms (1.94.0) + aws-sdk-core (~> 3, >= 3.207.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.167.0) + aws-sdk-core (~> 3, >= 3.207.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) colored (1.2) colored2 (3.1.2) @@ -32,12 +35,11 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) - excon (0.103.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -58,22 +60,22 @@ GEM faraday-httpclient (1.0.1) faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.216.0) + fastimage (2.3.1) + fastlane (2.223.1) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -85,6 +87,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -93,10 +96,10 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) terminal-table (~> 3) @@ -105,11 +108,11 @@ GEM word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.49.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.1) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -117,24 +120,23 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) @@ -145,34 +147,36 @@ GEM os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.7) domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.6.2) - json (2.6.3) - jwt (2.7.1) - mini_magick (4.12.0) + json (2.7.2) + jwt (2.9.3) + base64 + mini_magick (4.13.2) mini_mime (1.1.5) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.1) nanaimo (0.3.0) naturally (2.2.1) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.5.0) os (1.1.4) - plist (3.7.0) - public_suffix (5.0.3) - rake (13.0.6) + plist (3.7.1) + public_suffix (6.0.1) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.2.6) + rexml (3.3.8) rouge (2.0.7) ruby2_keywords (0.0.5) rubyzip (2.3.2) - security (0.1.3) - signet (0.18.0) + security (0.1.5) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -185,23 +189,19 @@ GEM unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) - unicode-display_width (2.4.2) - webrick (1.8.1) + unicode-display_width (2.6.0) word_wrap (1.0.0) - xcodeproj (1.22.0) + xcodeproj (1.25.1) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) nanaimo (~> 0.3.0) - rexml (~> 3.2.4) + rexml (>= 3.3.6, < 4.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.1) @@ -210,6 +210,7 @@ GEM PLATFORMS arm64-darwin-21 arm64-darwin-22 + arm64-darwin-23 DEPENDENCIES fastlane diff --git a/Sources/Issuer/JWT.swift b/Sources/Issuer/JWT.swift index fde0d30..10202f7 100644 --- a/Sources/Issuer/JWT.swift +++ b/Sources/Issuer/JWT.swift @@ -22,6 +22,8 @@ public struct JWT: JWTRepresentable { // MARK: - Properties + static let kbJwt = "kb+jwt" + var header: JWSRegisteredFieldsHeader var payload: JSON @@ -61,7 +63,7 @@ public struct JWT: JWTRepresentable { } mutating func addKBTyp() { - self.header.type = "kb+jwt" + self.header.type = Self.kbJwt } } diff --git a/Sources/Verifier/KeyBindingVerifier.swift b/Sources/Verifier/KeyBindingVerifier.swift index b0933e0..5508494 100644 --- a/Sources/Verifier/KeyBindingVerifier.swift +++ b/Sources/Verifier/KeyBindingVerifier.swift @@ -20,6 +20,8 @@ import SwiftyJSON public class KeyBindingVerifier: VerifierProtocol { + static let kbJwt = "kb+jwt" + private var signatureVerifier: SignatureVerifier? public init() { @@ -31,7 +33,7 @@ public class KeyBindingVerifier: VerifierProtocol { challenge: JWS, extractedKey: JWK ) throws { - guard challenge.protectedHeader.type == "kb+jwt" else { + guard challenge.protectedHeader.type == Self.kbJwt else { throw SDJWTVerifierError.keyBindingFailed(description: "no kb+jwt as typ claim") } @@ -57,7 +59,7 @@ public class KeyBindingVerifier: VerifierProtocol { challenge: JWS, extractedKey: JWK ) throws { - guard challenge.protectedHeader.type == "kb+jwt" else { + guard challenge.protectedHeader.type == Self.kbJwt else { throw SDJWTVerifierError.keyBindingFailed(description: "no kb+jwt as typ claim") }