-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
20 changed files
with
439 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Ignore everything in this directory | ||
* | ||
# Except this file | ||
!.gitignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
# Ignore everything in this directory | ||
* | ||
# Except this file | ||
!.gitignore |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import Foundation | ||
|
||
extension Bundle { | ||
static var app: Bundle { | ||
var components = main.bundleURL.path.split(separator: "/") | ||
var bundle: Bundle? | ||
|
||
if let index = components.lastIndex(where: { $0.hasSuffix(".app") }) { | ||
components.removeLast((components.count - 1) - index) | ||
bundle = Bundle(path: components.joined(separator: "/")) | ||
} | ||
|
||
return bundle ?? main | ||
} | ||
} |
120 changes: 108 additions & 12 deletions
120
ios/Gekidou/Sources/Gekidou/Networking/Network+Delegate.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,24 +1,120 @@ | ||
import Foundation | ||
import os.log | ||
|
||
extension Network: URLSessionDelegate, URLSessionTaskDelegate { | ||
typealias ChallengeEvaluation = (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?, error: NetworkError?) | ||
|
||
public func urlSession(_ session: URLSession, | ||
task: URLSessionTask, | ||
didReceive challenge: URLAuthenticationChallenge, | ||
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { | ||
var credential: URLCredential? = nil | ||
var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling | ||
var evaluation: ChallengeEvaluation | ||
switch challenge.protectionSpace.authenticationMethod { | ||
#if canImport(Security) | ||
case NSURLAuthenticationMethodServerTrust: | ||
evaluation = attemptServerTrustAuthentication(with: challenge) | ||
case NSURLAuthenticationMethodClientCertificate: | ||
evaluation = attemptClientAuthentication(with: challenge) | ||
#endif | ||
default: | ||
evaluation = (.performDefaultHandling, nil, nil) | ||
} | ||
|
||
if let error = evaluation.error { | ||
os_log("Gekidou: %{public}@", | ||
log: .default, | ||
type: .error, | ||
error.localizedDescription | ||
) | ||
} | ||
|
||
completionHandler(evaluation.disposition, evaluation.credential) | ||
} | ||
|
||
func attemptClientAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation{ | ||
let host = challenge.protectionSpace.host | ||
|
||
guard let (identity, certificate) = try? Keychain.default.getClientIdentityAndCertificate(for: host) else { | ||
return (.performDefaultHandling, nil, nil) | ||
} | ||
|
||
return (.useCredential, URLCredential(identity: identity, | ||
certificates: [certificate], | ||
persistence: URLCredential.Persistence.permanent | ||
), nil) | ||
} | ||
|
||
func attemptServerTrustAuthentication(with challenge: URLAuthenticationChallenge) -> ChallengeEvaluation { | ||
let host = challenge.protectionSpace.host | ||
|
||
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, | ||
let trust = challenge.protectionSpace.serverTrust | ||
else { | ||
return (.performDefaultHandling, nil, nil) | ||
} | ||
|
||
do { | ||
guard let certs = certificates[host], !certs.isEmpty else { | ||
return (.performDefaultHandling, nil, nil) | ||
} | ||
|
||
let authMethod = challenge.protectionSpace.authenticationMethod | ||
if authMethod == NSURLAuthenticationMethodClientCertificate { | ||
let host = task.currentRequest!.url!.host! | ||
if let (identity, certificate) = try? Keychain.default.getClientIdentityAndCertificate(for: host) { | ||
credential = URLCredential(identity: identity, | ||
certificates: [certificate], | ||
persistence: URLCredential.Persistence.permanent) | ||
try performDefaultValidation(trust) | ||
|
||
try performValidation(trust, forHost: host) | ||
|
||
try evaluate(trust, forHost: host, withCerts: certs) | ||
|
||
return (.useCredential, URLCredential(trust: trust), nil) | ||
} catch { | ||
os_log("Gekidou: %{public}@", | ||
log: .default, | ||
type: .error, | ||
error.localizedDescription | ||
) | ||
return (.cancelAuthenticationChallenge, nil, error as? NetworkError) | ||
} | ||
} | ||
|
||
private func getServerTrustCertificates(_ trust: SecTrust) -> [SecCertificate] { | ||
if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, visionOS 1, *) { | ||
return (SecTrustCopyCertificateChain(trust) as? [SecCertificate]) ?? [] | ||
} else { | ||
return (0..<SecTrustGetCertificateCount(trust)).compactMap { index in | ||
SecTrustGetCertificateAtIndex(trust, index) | ||
} | ||
disposition = .useCredential | ||
} | ||
|
||
completionHandler(disposition, credential) | ||
} | ||
|
||
private func performDefaultValidation(_ trust: SecTrust) throws { | ||
let policy = SecPolicyCreateSSL(true, nil) | ||
try evaluate(trust, afterApplying: policy) | ||
} | ||
|
||
private func performValidation(_ trust: SecTrust, forHost host: String) throws { | ||
let policy = SecPolicyCreateSSL(true, host as CFString) | ||
try evaluate(trust, afterApplying: policy) | ||
} | ||
|
||
private func evaluate(_ trust: SecTrust, afterApplying policy: SecPolicy) throws { | ||
let status = SecTrustSetPolicies(trust, policy) | ||
guard status == errSecSuccess else { | ||
throw NetworkError.serverTrustEvaluationFailed(reason: .policyApplicationFailed(trust: trust, policy: policy, status: status)) | ||
} | ||
|
||
var error: CFError? | ||
let evaluationSucceeded = SecTrustEvaluateWithError(trust, &error) | ||
if !evaluationSucceeded { | ||
throw NetworkError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: error)) | ||
} | ||
} | ||
|
||
private func evaluate(_ trust: SecTrust, forHost host: String, withCerts certs: [SecCertificate]) throws { | ||
let serverCertificates = getServerTrustCertificates(trust) | ||
let serverCertificatesData = Set(serverCertificates.map { SecCertificateCopyData($0) as Data }) | ||
let pinnedCertificatesData = Set(certs.map { SecCertificateCopyData($0) as Data }) | ||
let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData) | ||
if !pinnedCertificatesInServerData { | ||
throw NetworkError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host, trust: trust, pinnedCertificates: certs, serverCertificates: serverCertificates)) | ||
} | ||
} | ||
} |
68 changes: 68 additions & 0 deletions
68
ios/Gekidou/Sources/Gekidou/Networking/Network+Error.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import Foundation | ||
|
||
public enum NetworkError: Error { | ||
public enum ServerTrustFailureReason { | ||
/// The output of a server trust evaluation. | ||
public struct Output { | ||
/// The host for which the evaluation was performed. | ||
public let host: String | ||
/// The `SecTrust` value which was evaluated. | ||
public let trust: SecTrust | ||
/// The `OSStatus` of evaluation operation. | ||
public let status: OSStatus | ||
/// The result of the evaluation operation. | ||
public let result: SecTrustResultType | ||
|
||
/// Creates an `Output` value from the provided values. | ||
init(_ host: String, _ trust: SecTrust, _ status: OSStatus, _ result: SecTrustResultType) { | ||
self.host = host | ||
self.trust = trust | ||
self.status = status | ||
self.result = result | ||
} | ||
} | ||
|
||
/// No certificates were found with which to perform the trust evaluation. | ||
case noCertificatesFound | ||
/// During evaluation, application of the associated `SecPolicy` failed. | ||
case policyApplicationFailed(trust: SecTrust, policy: SecPolicy, status: OSStatus) | ||
/// `SecTrust` evaluation failed with the associated `Error`, if one was produced. | ||
case trustEvaluationFailed(error: Error?) | ||
/// Default evaluation failed with the associated `Output`. | ||
case defaultEvaluationFailed(output: Output) | ||
/// Host validation failed with the associated `Output`. | ||
case hostValidationFailed(output: Output) | ||
/// Certificate pinning failed. | ||
case certificatePinningFailed(host: String, trust: SecTrust, pinnedCertificates: [SecCertificate], serverCertificates: [SecCertificate]) | ||
} | ||
|
||
case serverTrustEvaluationFailed(reason: ServerTrustFailureReason) | ||
} | ||
|
||
extension NetworkError: LocalizedError { | ||
public var errorDescription: String? { | ||
switch self { | ||
case let .serverTrustEvaluationFailed(reason): | ||
return "Server trust evaluation failed due to reason: \(reason.localizedDescription)" | ||
} | ||
} | ||
} | ||
|
||
extension NetworkError.ServerTrustFailureReason { | ||
var localizedDescription: String { | ||
switch self { | ||
case .noCertificatesFound: | ||
return "No certificates were found or provided for evaluation." | ||
case .policyApplicationFailed: | ||
return "Attempting to set a SecPolicy failed." | ||
case let .trustEvaluationFailed(error): | ||
return "SecTrust evaluation failed with error: \(error?.localizedDescription ?? "None")" | ||
case let .defaultEvaluationFailed(output): | ||
return "Default evaluation failed for host \(output.host)." | ||
case let .hostValidationFailed(output): | ||
return "Host validation failed for host \(output.host)." | ||
case let .certificatePinningFailed(host, _, pinnedCertificates, _): | ||
return "Certificate pinning failed for host \(host) after evaluating \(pinnedCertificates.count) installed certificates." | ||
} | ||
} | ||
} |
Oops, something went wrong.