diff options
| author | Andrej Mihajlov <and@mullvad.net> | 2021-05-13 11:01:04 +0200 |
|---|---|---|
| committer | Andrej Mihajlov <and@mullvad.net> | 2021-05-18 10:51:40 +0200 |
| commit | 1364ed1ea6b6897509e7f9ae792fbcf8312f141f (patch) | |
| tree | f3406a8f60a0dbdabc448c9b3b2ca769c1bf9bfc | |
| parent | 5bdce484c807042d1319ca11b123c220795e134c (diff) | |
| download | mullvadvpn-1364ed1ea6b6897509e7f9ae792fbcf8312f141f.tar.xz mullvadvpn-1364ed1ea6b6897509e7f9ae792fbcf8312f141f.zip | |
Add SSL pinning
| -rw-r--r-- | ios/Assets/new_le_root_cert.cer | bin | 0 -> 1391 bytes | |||
| -rw-r--r-- | ios/Assets/old_le_root_cert.cer | bin | 0 -> 846 bytes | |||
| -rw-r--r-- | ios/BuildInstructions.md | 9 | ||||
| -rw-r--r-- | ios/CHANGELOG.md | 1 | ||||
| -rw-r--r-- | ios/MullvadVPN.xcodeproj/project.pbxproj | 28 | ||||
| -rw-r--r-- | ios/MullvadVPN/MullvadRest.swift | 23 | ||||
| -rw-r--r-- | ios/MullvadVPN/SSLPinningURLSessionDelegate.swift | 73 | ||||
| -rwxr-xr-x | ios/update-relays.sh | 6 |
8 files changed, 137 insertions, 3 deletions
diff --git a/ios/Assets/new_le_root_cert.cer b/ios/Assets/new_le_root_cert.cer Binary files differnew file mode 100644 index 0000000000..9d2132e7f1 --- /dev/null +++ b/ios/Assets/new_le_root_cert.cer diff --git a/ios/Assets/old_le_root_cert.cer b/ios/Assets/old_le_root_cert.cer Binary files differnew file mode 100644 index 0000000000..95500f6bd1 --- /dev/null +++ b/ios/Assets/old_le_root_cert.cer diff --git a/ios/BuildInstructions.md b/ios/BuildInstructions.md index 13f84867ce..4c73601ee3 100644 --- a/ios/BuildInstructions.md +++ b/ios/BuildInstructions.md @@ -173,3 +173,12 @@ where `<KEYCHAIN>` is the name of the target Keychain where the signing credenti This guide does not use a separate Keychain store, so use `login.keychain-db` then. Reference: https://docs.travis-ci.com/user/common-build-problems/#mac-macos-sierra-1012-code-signing-errors + +# SSL pinning + +The iOS app utilizes SSL pinning. Root certificates can be updated by using the source certificates shipped along with `mullvad-rpc`: + +``` +openssl x509 -in ../mullvad-rpc/new_le_root_cert.pem -outform der -out Assets/new_le_root_cert.cer +openssl x509 -in ../mullvad-rpc/old_le_root_cert.pem -outform der -out Assets/old_le_root_cert.cer +``` diff --git a/ios/CHANGELOG.md b/ios/CHANGELOG.md index 5c089e7252..f27785e79f 100644 --- a/ios/CHANGELOG.md +++ b/ios/CHANGELOG.md @@ -29,6 +29,7 @@ Line wrap the file at 100 chars. Th - Add interactive map. - Reduce network traffic consumption by leveraging HTTP caching via ETag HTTP header to avoid re-downloading the relay list if it hasn't changed. +- Pin root SSL certificates. ### Fixed - Fix bug which caused the tunnel manager to become unresponsive in the rare event of failure to diff --git a/ios/MullvadVPN.xcodeproj/project.pbxproj b/ios/MullvadVPN.xcodeproj/project.pbxproj index 91ba67a7ff..4ec0dcc42a 100644 --- a/ios/MullvadVPN.xcodeproj/project.pbxproj +++ b/ios/MullvadVPN.xcodeproj/project.pbxproj @@ -73,6 +73,14 @@ 584592612639B4A200EF967F /* ConsentContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584592602639B4A200EF967F /* ConsentContentView.swift */; }; 5845F842236CBACD00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; }; 5845F843236CBDAB00B2D93C /* PacketTunnelIpc.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */; }; + 584789B8264D4A2A000E45FB /* old_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */; }; + 584789B9264D4A2A000E45FB /* old_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */; }; + 584789BE264D4A2A000E45FB /* new_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */; }; + 584789BF264D4A2A000E45FB /* new_le_root_cert.cer in Resources */ = {isa = PBXBuildFile; fileRef = 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */; }; + 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; + 584789E126529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; + 584789E626529DEF000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */; }; + 584789EC2652A1A2000E45FB /* Logging in Frameworks */ = {isa = PBXBuildFile; productRef = 584789EB2652A1A2000E45FB /* Logging */; }; 584E96BC240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; 584E96BD240FD4DA00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; 584E96BE240FD4DB00D3334F /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58A1AA8623F43901009F7EA6 /* Location.swift */; }; @@ -318,6 +326,9 @@ 5840250322B11AB700E4CFEC /* MullvadEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MullvadEndpoint.swift; sourceTree = "<group>"; }; 584592602639B4A200EF967F /* ConsentContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsentContentView.swift; sourceTree = "<group>"; }; 5845F841236CBACD00B2D93C /* PacketTunnelIpc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PacketTunnelIpc.swift; sourceTree = "<group>"; }; + 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = old_le_root_cert.cer; sourceTree = "<group>"; }; + 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = new_le_root_cert.cer; sourceTree = "<group>"; }; + 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSLPinningURLSessionDelegate.swift; sourceTree = "<group>"; }; 584B26F3237434D00073B10E /* RelaySelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySelectorTests.swift; sourceTree = "<group>"; }; 5850366725A47AC700A43E93 /* IPAddressRange+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IPAddressRange+Codable.swift"; sourceTree = "<group>"; }; 58561C98239A5D1500BD6B5E /* IPEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IPEndpoint.swift; sourceTree = "<group>"; }; @@ -432,6 +443,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 584789EC2652A1A2000E45FB /* Logging in Frameworks */, 58871D1E25D535A3002297FA /* WireGuardKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -665,6 +677,7 @@ 58B993B02608A34500BA7811 /* LoginContentView.swift */, 58FEEB57260B662E00A621A8 /* AutomaticKeyboardResponder.swift */, 5868BD32261DCD2600E6027F /* CustomSplitViewController.swift */, + 584789DF26529D72000E45FB /* SSLPinningURLSessionDelegate.swift */, ); path = MullvadVPN; sourceTree = "<group>"; @@ -703,6 +716,8 @@ 58F3C0A824A50C0E003E76BE /* Assets */ = { isa = PBXGroup; children = ( + 584789B7264D4A2A000E45FB /* new_le_root_cert.cer */, + 584789B4264D4A2A000E45FB /* old_le_root_cert.cer */, 58F3C0A524A50155003E76BE /* relays.json */, ); path = Assets; @@ -744,6 +759,7 @@ name = MullvadVPNTests; packageProductDependencies = ( 58871D1D25D535A3002297FA /* WireGuardKit */, + 584789EB2652A1A2000E45FB /* Logging */, ); productName = MullvadVPNTests; productReference = 58B0A2A0238EE67E00BC001D /* MullvadVPNTests.xctest */; @@ -897,6 +913,8 @@ 586ADD4723FC13F400CE9E87 /* countries.geo.json in Resources */, 58CE5E6E224146210008646E /* LaunchScreen.storyboard in Resources */, 58CE5E6B224146210008646E /* Assets.xcassets in Resources */, + 584789B8264D4A2A000E45FB /* old_le_root_cert.cer in Resources */, + 584789BE264D4A2A000E45FB /* new_le_root_cert.cer in Resources */, 58CE5E69224146200008646E /* Main.storyboard in Resources */, 58E5BC2624FEB6DB00A53A76 /* AccountViewController.xib in Resources */, 58B9814E24FEA70D00C0D59E /* WireguardKeysViewController.xib in Resources */, @@ -907,7 +925,9 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 584789B9264D4A2A000E45FB /* old_le_root_cert.cer in Resources */, 58F3C0A724A50C02003E76BE /* relays.json in Resources */, + 584789BF264D4A2A000E45FB /* new_le_root_cert.cer in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -937,6 +957,7 @@ 5807E2C3243203E700F5FF30 /* String+Split.swift in Sources */, 5896AE82246ACE84005B36CB /* KeychainReturn.swift in Sources */, 58B0A2A8238EE68200BC001D /* RelaySelectorTests.swift in Sources */, + 584789E626529DEF000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, 584E96BE240FD4DB00D3334F /* Location.swift in Sources */, 5857F23F24C844AD00CF6F47 /* Locking.swift in Sources */, 5857F23424C8443700CF6F47 /* AsyncOperation.swift in Sources */, @@ -985,6 +1006,7 @@ 580EE21524B3231200F9D8A1 /* OperationBlockObserver.swift in Sources */, 58BFA5C622A7C97F00A6173D /* RelayCache.swift in Sources */, 582BB1B1229569620055B6EF /* CustomNavigationBar.swift in Sources */, + 584789E026529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, 588D2FE3248AC27F00E313F7 /* AsyncOperation.swift in Sources */, 5877153023981F7B001F8237 /* WireguardKeysViewController.swift in Sources */, 5850367F25A481D800A43E93 /* IPAddressRange+Codable.swift in Sources */, @@ -1092,6 +1114,7 @@ buildActionMask = 2147483647; files = ( 58CB0EE124B86751001EF0D8 /* MullvadRest.swift in Sources */, + 584789E126529D72000E45FB /* SSLPinningURLSessionDelegate.swift in Sources */, 580EE21F24B3237F00F9D8A1 /* OutputOperation.swift in Sources */, 5850366825A47AC700A43E93 /* IPAddressRange+Codable.swift in Sources */, 58F7D310250FA12E0097BE4E /* AnyIPEndpoint.swift in Sources */, @@ -1618,6 +1641,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 584789EB2652A1A2000E45FB /* Logging */ = { + isa = XCSwiftPackageProductDependency; + package = 585834F624D2BC1F00A8AF56 /* XCRemoteSwiftPackageReference "swift-log" */; + productName = Logging; + }; 585834F724D2BC1F00A8AF56 /* Logging */ = { isa = XCSwiftPackageProductDependency; package = 585834F624D2BC1F00A8AF56 /* XCRemoteSwiftPackageReference "swift-log" */; diff --git a/ios/MullvadVPN/MullvadRest.swift b/ios/MullvadVPN/MullvadRest.swift index ae002dab75..4d6b6da45d 100644 --- a/ios/MullvadVPN/MullvadRest.swift +++ b/ios/MullvadVPN/MullvadRest.swift @@ -8,6 +8,7 @@ import Foundation import Network +import Security import WireGuardKit /// REST API v1 base URL @@ -415,8 +416,26 @@ struct RestSessionEndpoint<Input, Response> where Input: RestPayload { // MARK: - REST interface -struct MullvadRest { - let session = URLSession(configuration: .ephemeral) +class MullvadRest { + let session: URLSession + + private let sessionDelegate: SSLPinningURLSessionDelegate + + /// Returns array of trusted root certificates + private static var trustedRootCertificates: [SecCertificate] { + let oldRootCertificate = Bundle.main.path(forResource: "old_le_root_cert", ofType: "cer")! + let newRootCertificate = Bundle.main.path(forResource: "new_le_root_cert", ofType: "cer")! + + return [oldRootCertificate, newRootCertificate].map { (path) -> SecCertificate in + let data = FileManager.default.contents(atPath: path)! + return SecCertificateCreateWithData(nil, data as CFData)! + } + } + + init() { + sessionDelegate = SSLPinningURLSessionDelegate(trustedRootCertificates: Self.trustedRootCertificates) + session = URLSession(configuration: .ephemeral, delegate: sessionDelegate, delegateQueue: nil) + } func createAccount() -> RestSessionEndpoint<EmptyPayload, AccountResponse> { return RestSessionEndpoint(session: session, endpoint: Self.createAccount()) diff --git a/ios/MullvadVPN/SSLPinningURLSessionDelegate.swift b/ios/MullvadVPN/SSLPinningURLSessionDelegate.swift new file mode 100644 index 0000000000..73df25f55d --- /dev/null +++ b/ios/MullvadVPN/SSLPinningURLSessionDelegate.swift @@ -0,0 +1,73 @@ +// +// SSLPinningURLSessionDelegate.swift +// MullvadVPN +// +// Created by pronebird on 17/05/2021. +// Copyright © 2021 Mullvad VPN AB. All rights reserved. +// + +import Foundation +import Logging + +class SSLPinningURLSessionDelegate: NSObject, URLSessionDelegate { + + private let trustedRootCertificates: [SecCertificate] + private let logger = Logger(label: "SSLPinningURLSessionDelegate") + + init(trustedRootCertificates: [SecCertificate]) { + self.trustedRootCertificates = trustedRootCertificates + } + + // MARK: - URLSessionDelegate + + func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + let evaluation: (disposition: URLSession.AuthChallengeDisposition, credential: URLCredential?) + + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let serverTrust = challenge.protectionSpace.serverTrust, self.verifyServerTrust(serverTrust) { + evaluation = (.useCredential, URLCredential(trust: serverTrust)) + } else { + evaluation = (.cancelAuthenticationChallenge, nil) + } + } else { + evaluation = (.rejectProtectionSpace, nil) + } + + completionHandler(evaluation.disposition, evaluation.credential) + } + + + // MARK: - Private + + private func verifyServerTrust(_ serverTrust: SecTrust) -> Bool { + // Set trusted root certificates + var secResult = SecTrustSetAnchorCertificates(serverTrust, trustedRootCertificates as CFArray) + guard secResult == errSecSuccess else { + self.logger.error("SecTrustSetAnchorCertificates failure: \(self.formatErrorMessage(code: secResult))") + return false + } + + // Tell security framework to only trust the provided root certificates + secResult = SecTrustSetAnchorCertificatesOnly(serverTrust, true) + guard secResult == errSecSuccess else { + self.logger.error("SecTrustSetAnchorCertificatesOnly failure: \(self.formatErrorMessage(code: secResult))") + return false + } + + var error: CFError? + if SecTrustEvaluateWithError(serverTrust, &error) { + return true + } else { + self.logger.error("SecTrustEvaluateWithError failure: \(error?.localizedDescription ?? "<nil>")") + return false + } + } + + private func formatErrorMessage(code: OSStatus) -> String { + let message = SecCopyErrorMessageString(code, nil) as String? ?? "<nil>" + + return "\(message) (code: \(code))" + } + + +} diff --git a/ios/update-relays.sh b/ios/update-relays.sh index efb0a8a65c..97fd3beafc 100755 --- a/ios/update-relays.sh +++ b/ios/update-relays.sh @@ -9,7 +9,11 @@ RELAYS_FILE="$PROJECT_DIR/Assets/relays.json" if [ $CONFIGURATION == "Release" ]; then echo "Remove relays file" - rm "$RELAYS_FILE" || true + if [ -f "$RELAYS_FILE" ]; then + rm "$RELAYS_FILE" + else + echo "Relays file does not exist" + fi fi if [ ! -f "$RELAYS_FILE" ]; then |
