From 079645d6cb82487ab601f6ef5bbd173e4830b102 Mon Sep 17 00:00:00 2001 From: Max Backevich Date: Mon, 15 Jul 2024 17:05:55 +0200 Subject: [PATCH] fix(GiniMerchantSDK): Fix init issues, update ssl manager fix sonar issue, update ssl manager to check domain, update log level # Conflicts: # MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift --- .../GiniMerchantSDK/Core/GiniMerchant.swift | 10 +++++-- .../Core/SSLPinning/GiniSessionDelegate.swift | 4 +-- .../Core/SSLPinning/SSLPinningManager.swift | 30 +++++++++++++++---- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift index d4ebeeb952..ed0085d50c 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/GiniMerchant.swift @@ -96,10 +96,14 @@ public struct DataForReview { - domain: The domain associated with your client credentials. This is used to scope the client credentials to a specific domain. - pinningConfig: Configuration for certificate pinning. Format ["PinnedDomains" : ["PublicKeyHashes"]] */ - public init(id: String, secret: String, domain: String, pinningConfig: [String: [String]]) { - let delegate = GiniSessionDelegate(pinnedKeyHashes: pinningConfig.values.flatMap { $0 }) + public init(id: String, + secret: String, + domain: String, + pinningConfig: [String: [String]]) { let client = Client(id: id, secret: secret, domain: domain) - self.giniApiLib = GiniHealthAPI.Builder(client: client, logLevel: .debug, sessionDelegate: delegate).build() + self.giniApiLib = GiniHealthAPI.Builder(client: client, + logLevel: .none, + sessionDelegate: GiniSessionDelegate(pinningConfig: pinningConfig)).build() self.documentService = DefaultDocumentService(docService: giniApiLib.documentService()) self.paymentService = giniApiLib.paymentService() } diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/GiniSessionDelegate.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/GiniSessionDelegate.swift index 71cacae291..90d2f8f269 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/GiniSessionDelegate.swift +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/GiniSessionDelegate.swift @@ -10,8 +10,8 @@ import Foundation class GiniSessionDelegate: NSObject, URLSessionDelegate { private let pinningManager: SSLPinningManager - init(pinnedKeyHashes: [String]) { - self.pinningManager = SSLPinningManager(pinnedKeyHashes: pinnedKeyHashes) + internal init(pinningConfig: [String: [String]]) { + self.pinningManager = SSLPinningManager(pinningConfig: pinningConfig) } func urlSession(_ session: URLSession, diff --git a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/SSLPinningManager.swift b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/SSLPinningManager.swift index 8e780476f7..17a224dd3f 100644 --- a/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/SSLPinningManager.swift +++ b/MerchantSDK/GiniMerchantSDK/Sources/GiniMerchantSDK/Core/SSLPinning/SSLPinningManager.swift @@ -10,6 +10,7 @@ import Foundation import CommonCrypto struct SSLPinningManager { + // Custom error types for SSL pinning private enum PinningError: Error { case noCertificatesFromServer case failedToGetPublicKey @@ -17,17 +18,20 @@ struct SSLPinningManager { case receivedWrongCertificate } + // ASN.1 header for RSA 2048-bit keys. The same for all keys private static let rsa2048ASN1Header: [UInt8] = [ 0x30, 0x82, 0x01, 0x22, 0x30, 0x0D, 0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0F, 0x00 ] - private let pinnedKeyHashes: [String] - - init(pinnedKeyHashes: [String]) { - self.pinnedKeyHashes = pinnedKeyHashes - } + // Dictionary mapping domain names to their expected public key hashes + private let pinningConfig: [String: [String]] + init(pinningConfig: [String: [String]]) { + self.pinningConfig = pinningConfig + } + + // Function to validate the server's certificate func validate(challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { do { @@ -42,17 +46,31 @@ struct SSLPinningManager { //MARK: - private methods private extension SSLPinningManager { + // Validate the server's certificate and return the trust object if valid func validateAndGetTrust(with challenge: URLAuthenticationChallenge) throws -> SecTrust { + // Step 1: Retrieve the server's trust object and its certificate chain guard let trust = challenge.protectionSpace.serverTrust, let trustCertificateChain = trustCopyCertificateChain(trust), !trustCertificateChain.isEmpty else { throw PinningError.noCertificatesFromServer } + + // Step 2: Get the domain from the challenge and check if it has a pinning configuration + guard let domain = challenge.protectionSpace.host.lowercased() as String? else { + throw PinningError.receivedWrongCertificate + } + + // Step 3: Retrieve the pinned key hashes from pinning config for the domain + guard let pinnedKeyHashes = pinningConfig[domain] else { + throw PinningError.receivedWrongCertificate + } + // Step 4: Iterate over the server's certificates to find a matching public key hash for serverCertificate in trustCertificateChain { let publicKey = try getPublicKey(for: serverCertificate) let publicKeyHash = try getKeyHash(of: publicKey) + // If a matching hash is found, the certificate is valid if pinnedKeyHashes.contains(publicKeyHash) { return trust } @@ -60,6 +78,7 @@ private extension SSLPinningManager { throw PinningError.receivedWrongCertificate } + // Extract the public key from the server's certificate func getPublicKey(for certificate: SecCertificate) throws -> SecKey { let policy = SecPolicyCreateBasicX509() var trust: SecTrust? @@ -72,6 +91,7 @@ private extension SSLPinningManager { return publicKey } + // Generate a SHA-256 hash of the public key func getKeyHash(of publicKey: SecKey) throws -> String { guard let publicKeyCFData = SecKeyCopyExternalRepresentation(publicKey, nil) else { throw PinningError.failedToGetDataFromPublicKey