-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Certificate pinning and proxy mode in
WireAPI
- WPB-10450 (#2237
) Co-authored-by: François Benaiteau <[email protected]>
- Loading branch information
1 parent
5d83439
commit 9f69bb9
Showing
32 changed files
with
899 additions
and
510 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
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
55 changes: 55 additions & 0 deletions
55
WireAPI/Sources/WireAPI/Models/Backend/BackendEnvironment.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,55 @@ | ||
// | ||
// Wire | ||
// Copyright (C) 2024 Wire Swiss GmbH | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see http://www.gnu.org/licenses/. | ||
// | ||
|
||
import Foundation | ||
|
||
/// A collection of data for connecting to a given backend environment (e.g. Production, Staging, etc). | ||
|
||
public struct BackendEnvironment { | ||
|
||
/// The `URL` of the backend. | ||
|
||
let url: URL | ||
|
||
/// The `URL` of the WebSocket endpoint. | ||
|
||
let webSocketURL: URL | ||
|
||
/// The pinned keys for the backend for use with certificate pinning. | ||
|
||
let pinnedKeys: [PinnedKey] | ||
|
||
/// The proxy settings for the backend if any. | ||
|
||
let proxySettings: ProxySettings? | ||
|
||
/// Creates a new `BackendEnvironment`. | ||
/// | ||
/// - Parameter url: The `URL` of the backend. | ||
/// - Parameter webSocketURL: The `URL` of the WebSocket endpoint. | ||
/// - Parameter pinnedKeys: The pinned keys for the backend for use with certificate pinning. | ||
/// - Parameter proxySettings: The proxy settings for the backend if any. | ||
|
||
public init(url: URL, webSocketURL: URL, pinnedKeys: [PinnedKey], proxySettings: ProxySettings?) { | ||
self.url = url | ||
self.webSocketURL = webSocketURL | ||
self.pinnedKeys = pinnedKeys | ||
self.proxySettings = proxySettings | ||
} | ||
|
||
} |
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,74 @@ | ||
// | ||
// Wire | ||
// Copyright (C) 2024 Wire Swiss GmbH | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see http://www.gnu.org/licenses/. | ||
// | ||
|
||
import Foundation | ||
@preconcurrency import Security | ||
|
||
/// Associates a list of `hosts` with a public `key`. | ||
|
||
public struct PinnedKey: Sendable { | ||
|
||
public enum Failure: Error { | ||
case invalidKeyData | ||
} | ||
|
||
public enum Host: Sendable { | ||
case endsWith(String) | ||
case equals(String) | ||
} | ||
|
||
let key: SecKey | ||
let hosts: [Host] | ||
|
||
public init(key: SecKey, hosts: [Host]) { | ||
self.key = key | ||
self.hosts = hosts | ||
} | ||
|
||
public init(key: Data, hosts: [Host]) throws(Failure) { | ||
self.key = try Self.key(for: key) | ||
self.hosts = hosts | ||
} | ||
|
||
/// Returns `true` if `host` matches any of the `hosts` in `self`. | ||
|
||
func matches(host: String) -> Bool { | ||
hosts.contains { | ||
switch $0 { | ||
case let .endsWith(suffix): | ||
host.hasSuffix(suffix) | ||
case let .equals(value): | ||
host == value | ||
} | ||
} | ||
} | ||
|
||
// MARK: - Private | ||
|
||
private static func key(for data: Data) throws(Failure) -> SecKey { | ||
guard | ||
let certificate = SecCertificateCreateWithData(nil, data as CFData), | ||
let publicKey = SecCertificateCopyKey(certificate) | ||
else { | ||
throw Failure.invalidKeyData | ||
} | ||
|
||
return publicKey | ||
} | ||
|
||
} |
60 changes: 60 additions & 0 deletions
60
WireAPI/Sources/WireAPI/Models/Backend/ProxySettings.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,60 @@ | ||
// | ||
// Wire | ||
// Copyright (C) 2024 Wire Swiss GmbH | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see http://www.gnu.org/licenses/. | ||
// | ||
|
||
import Foundation | ||
|
||
/// Proxy settings for communicating with a backend server. | ||
|
||
public enum ProxySettings { | ||
|
||
/// Settings for an unauthenticated proxy. | ||
|
||
case unauthenticated(host: String, port: Int) | ||
|
||
/// Settings for an authenticated proxy. | ||
|
||
case authenticated(host: String, port: Int, username: String, password: String) | ||
|
||
/// Dictionary to be used with `URLSessionConfiguration.connectionProxyDictionary`. | ||
|
||
func proxyDictionary() -> [AnyHashable: Any] { | ||
let socksEnable = "SOCKSEnable" | ||
let socksProxy = "SOCKSProxy" | ||
let socksPort = "SOCKSPort" | ||
|
||
var result: [AnyHashable: Any] = [ | ||
socksEnable: 1, | ||
kCFProxyTypeKey: kCFProxyTypeSOCKS, | ||
kCFStreamPropertySOCKSVersion: kCFStreamSocketSOCKSVersion5 | ||
] | ||
|
||
switch self { | ||
case let .unauthenticated(host, port): | ||
result[socksProxy] = host | ||
result[socksPort] = port | ||
case let .authenticated(host, port, username, password): | ||
result[socksProxy] = host | ||
result[socksPort] = port | ||
result[kCFStreamPropertySOCKSUser] = username | ||
result[kCFStreamPropertySOCKSPassword] = password | ||
} | ||
|
||
return result | ||
} | ||
|
||
} |
98 changes: 98 additions & 0 deletions
98
WireAPI/Sources/WireAPI/Models/Backend/ServerTrustValidator.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,98 @@ | ||
// | ||
// Wire | ||
// Copyright (C) 2024 Wire Swiss GmbH | ||
// | ||
// This program is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// This program is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with this program. If not, see http://www.gnu.org/licenses/. | ||
// | ||
|
||
import Foundation | ||
@preconcurrency import Security | ||
|
||
struct ServerTrustValidator: Sendable { | ||
|
||
enum Failure: Error, Equatable { | ||
case evaluatingServerTrustFailed | ||
case noPublicKeyOnServerTrust | ||
case noMatchingPublicKey | ||
} | ||
|
||
private let pinnedKeys: [PinnedKey] | ||
|
||
init(pinnedKeys: [PinnedKey]) { | ||
self.pinnedKeys = pinnedKeys | ||
} | ||
|
||
/// Verifies the server `trust` for the given `host`. | ||
/// | ||
/// - Parameter trust: The `SecTrust` of the server. | ||
/// - Parameter host: The host of the server. | ||
/// - Throws: An error if server certificate should not be trusted. | ||
/// - Note: If no pinned keys are found for the `host`, the server certificate is trusted. | ||
|
||
func validate(trust: SecTrust, host: String) async throws { | ||
let matchingKeys = pinnedKeys.filter { $0.matches(host: host) }.map(\.key) | ||
|
||
// If no keys are pinned for `host`, we trust the server certificate | ||
guard !matchingKeys.isEmpty else { return } | ||
|
||
try await Self.verifyServerCertificateTrusted(trust) | ||
|
||
let publicKey = try Self.publicKeyAssociatedWithServerTrust(trust) | ||
|
||
guard matchingKeys.contains(publicKey) else { | ||
throw Failure.noMatchingPublicKey | ||
} | ||
} | ||
|
||
// MARK: - Private | ||
|
||
private static func verifyServerCertificateTrusted(_ serverTrust: SecTrust) async throws { | ||
try await withCheckedThrowingContinuation { continuation in | ||
// `SecTrustEvaluateAsyncWithError` requires the completion queue to be the same as the queue on which it | ||
// is called. | ||
let queue = DispatchQueue.global() | ||
queue.async { | ||
SecTrustEvaluateAsyncWithError(serverTrust, queue) { _, success, error in | ||
if success { | ||
continuation.resume() | ||
} else { | ||
print("Server trust evaluation failed: \(String(describing: error))") | ||
continuation.resume(throwing: Failure.evaluatingServerTrustFailed) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// Returns the public key of the leaf certificate associated with `serverTrust`. | ||
/// | ||
/// - Parameter serverTrust: SecTrust of server | ||
/// - Returns: public key from `serverTrust` | ||
|
||
private static func publicKeyAssociatedWithServerTrust(_ serverTrust: SecTrust) throws -> SecKey { | ||
let certificates = SecTrustCopyCertificateChain(serverTrust) ?? [] as CFArray | ||
let policy = SecPolicyCreateBasicX509() | ||
var secTrust: SecTrust? | ||
|
||
guard SecTrustCreateWithCertificates(certificates, policy, &secTrust) == noErr, | ||
let trust = secTrust, | ||
let result = SecTrustCopyKey(trust) | ||
else { | ||
throw Failure.noPublicKeyOnServerTrust | ||
} | ||
|
||
return result | ||
} | ||
|
||
} |
Oops, something went wrong.