From 69ce1e4490cf52fdf358380b6374f3dc0f33275d Mon Sep 17 00:00:00 2001 From: Joseph Milan Date: Fri, 26 Jul 2024 21:10:08 +0530 Subject: [PATCH] Fix - Expiry and signature validation --- .../CredentialValidaorProtocol.swift | 2 +- .../CredentialValidatorService.swift | 89 +---------- .../ExpiryValidator.swift | 17 ++ .../SignatureValidator.swift | 148 ++++++++++++++++++ .../Service/DidService.swift | 25 +++ 5 files changed, 195 insertions(+), 86 deletions(-) diff --git a/Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidaorProtocol.swift b/Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidaorProtocol.swift index 7d629d9..74b2502 100644 --- a/Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidaorProtocol.swift +++ b/Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidaorProtocol.swift @@ -8,5 +8,5 @@ import Foundation protocol CredentialValidaorProtocol { - func validateCredential(jwt: String?) async throws + func validateCredential(jwt: String?, jwksURI: String?) async throws } diff --git a/Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidatorService.swift b/Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidatorService.swift index 99da883..3740d41 100644 --- a/Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidatorService.swift +++ b/Sources/eudiWalletOidcIos/Service/CredentialValidation/CredentialValidatorService.swift @@ -17,10 +17,10 @@ public class CredentialValidatorService: CredentialValidaorProtocol { public static var shared = CredentialValidatorService() public init() {} - public func validateCredential(jwt: String?) async throws { - let isJWTExpired = validateExpiryDate(jwt: jwt) ?? false - let isSignatureExpied = await validateSign(jwt: jwt) ?? false - if !isJWTExpired { + public func validateCredential(jwt: String?, jwksURI: String?) async throws { + let isJWTExpired = ExpiryValidator.validateExpiryDate(jwt: jwt) ?? false + let isSignatureExpied = await SignatureValidator.validateSign(jwt: jwt, jwksURI: jwksURI) ?? false + if isJWTExpired { throw ValidationError.JWTExpired } if !isSignatureExpied { @@ -28,85 +28,4 @@ public class CredentialValidatorService: CredentialValidaorProtocol { } } - public func validateExpiryDate(jwt: String?) -> Bool? { - guard let split = jwt?.split(separator: "."), - let jsonString = "\(split[1])".decodeBase64(), - let jsonObject = UIApplicationUtils.shared.convertStringToDictionary(text: jsonString) else { return false } - guard let vc = jsonObject["vc"] as? [String: Any], let expirationDate = vc["expirationDate"] as? String else { return true} - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" - guard let expiryDate = dateFormatter.date(from: expirationDate) else { return false} - let currentDate = Date() - if currentDate <= expiryDate { - return true - } else { - return false - } - } - - public func validateSign(jwt: String?) async -> Bool? { - guard let split = jwt?.split(separator: "."), - let jsonString = "\(split[0])".decodeBase64(), - let jsonObject = UIApplicationUtils.shared.convertStringToDictionary(text: jsonString) else { return false } - guard let kid = jsonObject["kid"] as? String else { return true} - var jwk: [String: Any] = [:] - if kid.hasPrefix("did:key:z") { - jwk = processJWKfromKid(did: kid) - } else if kid.hasPrefix("did:ebsi:z") { - jwk = await processJWKforEBSI(did: kid) - } else { - - } - return true - } - - - func processJWKfromKid(did: String?) -> [String: Any] { - do { - guard let did = did else { return [:]} - let components = did.split(separator: "#") - guard let didPart = components.first else { - return [:] - } - let multibaseString = String(didPart.dropFirst("did:key:z".count)) - - guard let decodedData = Base58.base58Decode(multibaseString) else { - print("Failed to decode Multibase string") - return [:] - } - - let multicodecPrefixLength = 3 - guard decodedData.count > multicodecPrefixLength else { - print("Invalid decoded data length") - return [:] - } - let jsonData = Data(decodedData.dropFirst(multicodecPrefixLength)) - - let jwk = try JSONSerialization.jsonObject(with: jsonData, options: []) - return jwk as? [String: Any] ?? [:] - } catch { - print("Error: \(error)") - return [:] - } - } - - func processJWKforEBSI(did: String?) async -> [String: Any]{ - guard let did = did else { return [:]} - let ebsiEndPoint = "https://api-conformance.ebsi.eu/did-registry/v5/identifiers/\(did)" - do { - guard let url = URL(string: ebsiEndPoint) else { return [:]} - let (data, response) = try await URLSession.shared.data(from: url) - guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { return [:]} - guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let verificationMethods = jsonObject["verificationMethod"] as? [[String: Any]] else { return [:]} - for data in verificationMethods { - if let publicKeyJwk = data["publicKeyJwk"] as? [String: Any], let crv = publicKeyJwk["crv"] as? String, crv == "P-256" { - return publicKeyJwk - } - } - } catch { - print("error") - } - return [:] - } - } diff --git a/Sources/eudiWalletOidcIos/Service/CredentialValidation/ExpiryValidator.swift b/Sources/eudiWalletOidcIos/Service/CredentialValidation/ExpiryValidator.swift index 329fbd6..9e4da40 100644 --- a/Sources/eudiWalletOidcIos/Service/CredentialValidation/ExpiryValidator.swift +++ b/Sources/eudiWalletOidcIos/Service/CredentialValidation/ExpiryValidator.swift @@ -8,5 +8,22 @@ import Foundation class ExpiryValidator { + + static func validateExpiryDate(jwt: String?) -> Bool? { + guard let split = jwt?.split(separator: "."), + let jsonString = "\(split[1])".decodeBase64(), + let jsonObject = UIApplicationUtils.shared.convertStringToDictionary(text: jsonString) else { return false } + guard let vc = jsonObject["vc"] as? [String: Any], let expirationDate = vc["expirationDate"] as? String else { return false } + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + guard let expiryDate = dateFormatter.date(from: expirationDate) else { return false} + let currentDate = Date() + if currentDate <= expiryDate { + return false + } else { + return true + } + } } diff --git a/Sources/eudiWalletOidcIos/Service/CredentialValidation/SignatureValidator.swift b/Sources/eudiWalletOidcIos/Service/CredentialValidation/SignatureValidator.swift index 06cb021..e5fb0c2 100644 --- a/Sources/eudiWalletOidcIos/Service/CredentialValidation/SignatureValidator.swift +++ b/Sources/eudiWalletOidcIos/Service/CredentialValidation/SignatureValidator.swift @@ -6,7 +6,155 @@ // import Foundation +import Base58Swift +import Security +import CryptoKit class SignatureValidator { + static func validateSign(jwt: String?, jwksURI: String?) async -> Bool? { + var jwk: [String: Any] = [:] + guard let split = jwt?.split(separator: "."), + let jsonString = "\(split[0])".decodeBase64(), + let jsonObject = UIApplicationUtils.shared.convertStringToDictionary(text: jsonString) else { return false } + if let kid = jsonObject["kid"] as? String { + if kid.hasPrefix("did:key:z") { + jwk = processJWKfromKid(did: kid) + } else if kid.hasPrefix("did:ebsi:z") { + jwk = await processJWKforEBSI(did: kid) + } else { + jwk = await processJWKFromJwksURI2(kid: kid, jwksURI: jwksURI) + } + } else { + let kid = jsonObject["kid"] as? String + jwk = await processJWKFromJwksURI2(kid: kid, jwksURI: jwksURI) + } + return validateSignature(jwt: jwt, jwk: jwk) + } + + + static func processJWKfromKid(did: String?) -> [String: Any] { + guard let did = did else { return [:]} + let components = did.split(separator: "#") + guard let didPart = components.first else { + return [:] + } + return DidService.shared.createJWKfromDID(did: String(didPart)) + } + + static func processJWKforEBSI(did: String?) async -> [String: Any]{ + guard let did = did else { return [:]} + let ebsiEndPoint = "https://api-conformance.ebsi.eu/did-registry/v5/identifiers/\(did)" + do { + guard let url = URL(string: ebsiEndPoint) else { return [:]} + let (data, response) = try await URLSession.shared.data(from: url) + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { return [:]} + guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let verificationMethods = jsonObject["verificationMethod"] as? [[String: Any]] else { return [:]} + for data in verificationMethods { + if let publicKeyJwk = data["publicKeyJwk"] as? [String: Any], let crv = publicKeyJwk["crv"] as? String, crv == "P-256" { + return publicKeyJwk + } + } + } catch { + print("error") + } + return [:] + } + + static func processJWKFromJwksURI2(kid: String?, jwksURI: String?) async -> [String: Any] { + guard let jwksURI = jwksURI else {return [:]} + return await fetchJwkData(kid: kid, jwksUri: jwksURI) + } + + static func fetchJwkData(kid: String?, jwksUri: String)async -> [String: Any] { + guard let url = URL(string: jwksUri) else { + return [:] + } + do { + let (data, response) = try await URLSession.shared.data(from: url) + guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { return [:]} + guard let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let keys = jsonObject["keys"] as? [[String: Any]] else { return [:]} + + var jwkKey: [String: Any]? = keys.first { $0["use"] as? String == "sig" } + + if jwkKey == nil, let kid = kid { + jwkKey = keys.first { $0["kid"] as? String == kid } + } + return jwkKey ?? [:] + + } catch { + print("error") + } + return [:] + } + + static private func validateSignature(jwt: String?, jwk: [String: Any]) -> Bool? { + let segments = jwt?.split(separator: ".") + guard segments?.count == 3 else { + return true + } + let headerData = String(segments?[0] ?? "") + let payloadData = String(segments?[1] ?? "") + var sigatureData = String(segments?[2] ?? "") + if sigatureData.contains("~") { + let splitData = sigatureData.split(separator: "~") + sigatureData = String(splitData[0]) + } + guard let headerEncoded = Data(base64URLEncoded: headerData) else { return false } + guard let signatureEncoded = Data(base64URLEncoded: sigatureData) else { return false } + guard let headerJson = try? JSONSerialization.jsonObject(with: headerEncoded, options: []) as? [String: Any], let alg = headerJson["alg"] as? String else { + return false + } + guard let crv = jwk["crv"] as? String else { + return false + } + let algToCrvMap: [String: String] = [ + "ES256": "P-256", + "ES384": "P-384", + "ES512": "P-521" + ] + if let expectedCrv = algToCrvMap[alg], expectedCrv != crv { + return false + } + guard let publicKey = extractPublicKey(from: jwk) else { + return false + } + + let signedData = "\(headerData).\(payloadData)".data(using: .utf8)! + let isVerified = verifySignature(signature: signatureEncoded, for: signedData, using: publicKey) + + return isVerified + } + + static private func extractPublicKey(from jwk: [String: Any]) -> P256.Signing.PublicKey? { + guard let crv = jwk["crv"] as? String, crv == "P-256", + let x = jwk["x"] as? String, + let y = jwk["y"] as? String, + let xData = Data(base64URLEncoded: x), + let yData = Data(base64URLEncoded: y) else { + return nil + } + + var publicKeyData = Data() + publicKeyData.append(0x04) + publicKeyData.append(xData) + publicKeyData.append(yData) + + do { + let publicKey = try P256.Signing.PublicKey(x963Representation: publicKeyData) + return publicKey + } catch { + print("Error creating public key: \(error)") + return nil + } + } + + static private func verifySignature(signature: Data, for data: Data, using publicKey: P256.Signing.PublicKey) -> Bool { + guard let ecdsaSignature = try? P256.Signing.ECDSASignature(rawRepresentation: signature) else { + print("Error converting signature to ECDSASignature") + return false + } + return publicKey.isValidSignature(ecdsaSignature, for: data) + } + } diff --git a/Sources/eudiWalletOidcIos/Service/DidService.swift b/Sources/eudiWalletOidcIos/Service/DidService.swift index 2ab6af2..9777097 100644 --- a/Sources/eudiWalletOidcIos/Service/DidService.swift +++ b/Sources/eudiWalletOidcIos/Service/DidService.swift @@ -146,4 +146,29 @@ public class DidService { } return nil } + + public func createJWKfromDID(did: String?) -> [String: Any] { + do { + guard let did = did else { return [:]} + let multibaseString = String(did.dropFirst("did:key:z".count)) + + guard let decodedData = Base58.base58Decode(multibaseString) else { + print("Failed to decode Multibase string") + return [:] + } + + let multicodecPrefixLength = 3 + guard decodedData.count > multicodecPrefixLength else { + print("Invalid decoded data length") + return [:] + } + let jsonData = Data(decodedData.dropFirst(multicodecPrefixLength)) + + let jwk = try JSONSerialization.jsonObject(with: jsonData, options: []) + return jwk as? [String: Any] ?? [:] + } catch { + print("Error: \(error)") + return [:] + } + } }