From 897e8243407cada9351665093d6c432986962b3f Mon Sep 17 00:00:00 2001 From: Davide De Rosa Date: Thu, 23 Aug 2018 11:32:38 +0200 Subject: [PATCH] Enforce use of non-preset CA certificates --- .jazzy.yaml | 1 + TunnelKit.xcodeproj/project.pbxproj | 6 + .../Sources/AppExtension/Certificate.swift | 32 +++++ .../TunnelKitProvider+Configuration.swift | 110 +++--------------- .../AppExtension/TunnelKitProvider.swift | 19 +-- TunnelKit/Sources/Core/SessionProxy.swift | 10 +- TunnelKit/Sources/Core/TLSBox.h | 4 +- TunnelKit/Sources/Core/TLSBox.m | 4 +- TunnelKitTests/AppExtensionTests.swift | 4 +- 9 files changed, 79 insertions(+), 111 deletions(-) create mode 100644 TunnelKit/Sources/AppExtension/Certificate.swift diff --git a/.jazzy.yaml b/.jazzy.yaml index ca7503b..a78374e 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -23,4 +23,5 @@ custom_categories: - SessionError - name: AppExtension children: + - Certificate - TunnelKitProvider diff --git a/TunnelKit.xcodeproj/project.pbxproj b/TunnelKit.xcodeproj/project.pbxproj index 76ad5c6..dcc6dbd 100644 --- a/TunnelKit.xcodeproj/project.pbxproj +++ b/TunnelKit.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ 0EC1BBA620D712DE007C4C7B /* DNSResolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */; }; 0EC1BBA820D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; }; 0EC1BBA920D7D803007C4C7B /* ConnectionStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */; }; + 0ECE3528212EB7770040F253 /* Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* Certificate.swift */; }; + 0ECE352A212EB88E0040F253 /* Certificate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECE3527212EB7770040F253 /* Certificate.swift */; }; 0EE7A79520F61EDC00B42E6A /* PacketMacros.h in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7A79420F61EDC00B42E6A /* PacketMacros.h */; }; 0EE7A79620F61EDC00B42E6A /* PacketMacros.h in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7A79420F61EDC00B42E6A /* PacketMacros.h */; }; 0EE7A79820F6296F00B42E6A /* PacketMacros.m in Sources */ = {isa = PBXBuildFile; fileRef = 0EE7A79720F6296F00B42E6A /* PacketMacros.m */; }; @@ -213,6 +215,7 @@ 0EBBF2FF2085196000E36B40 /* NWTCPConnectionState+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NWTCPConnectionState+Description.swift"; sourceTree = ""; }; 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSResolver.swift; sourceTree = ""; }; 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionStrategy.swift; sourceTree = ""; }; + 0ECE3527212EB7770040F253 /* Certificate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Certificate.swift; sourceTree = ""; }; 0EE7A79420F61EDC00B42E6A /* PacketMacros.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PacketMacros.h; sourceTree = ""; }; 0EE7A79720F6296F00B42E6A /* PacketMacros.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PacketMacros.m; sourceTree = ""; }; 0EE7A79D20F6488400B42E6A /* DataPathEncryption.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DataPathEncryption.h; sourceTree = ""; }; @@ -459,6 +462,7 @@ isa = PBXGroup; children = ( 0EBBF2E32084FDF400E36B40 /* Transport */, + 0ECE3527212EB7770040F253 /* Certificate.swift */, 0EC1BBA720D7D803007C4C7B /* ConnectionStrategy.swift */, 0EC1BBA420D71190007C4C7B /* DNSResolver.swift */, 0EBBF2E42084FE6F00E36B40 /* GenericSocket.swift */, @@ -887,6 +891,7 @@ 0EFEB4AC200760EC00F81029 /* InterfaceObserver.swift in Sources */, 0EFEB46D2006D3C800F81029 /* Data+Manipulation.swift in Sources */, 0EFEB47B2006D3C800F81029 /* TunnelKitProvider.swift in Sources */, + 0ECE3528212EB7770040F253 /* Certificate.swift in Sources */, 0EFEB4742006D3C800F81029 /* CoreConfiguration.swift in Sources */, 0E07595F20EF6D1400F38FD8 /* CryptoCBC.m in Sources */, 0EC1BBA820D7D803007C4C7B /* ConnectionStrategy.swift in Sources */, @@ -935,6 +940,7 @@ 0EFEB4A22006D7F300F81029 /* CoreConfiguration.swift in Sources */, 0EFEB4952006D7F300F81029 /* SecureRandom.swift in Sources */, 0EFEB49A2006D7F300F81029 /* MSS.m in Sources */, + 0ECE352A212EB88E0040F253 /* Certificate.swift in Sources */, 0EFEB48D2006D7F300F81029 /* EncryptionProxy.swift in Sources */, 0EFEB4922006D7F300F81029 /* ZeroingData.m in Sources */, 0E07596020EF6D1400F38FD8 /* CryptoCBC.m in Sources */, diff --git a/TunnelKit/Sources/AppExtension/Certificate.swift b/TunnelKit/Sources/AppExtension/Certificate.swift new file mode 100644 index 0000000..a337b3f --- /dev/null +++ b/TunnelKit/Sources/AppExtension/Certificate.swift @@ -0,0 +1,32 @@ +// +// Certificate.swift +// TunnelKit +// +// Created by Davide De Rosa on 22/08/2018. +// Copyright © 2018 Davide De Rosa. All rights reserved. +// + +import Foundation + +/// Represents a TLS certificate in PEM format. +public struct Certificate: Equatable { + + /// The content of the certificates in PEM format (ASCII). + public let pem: String + + /// :nodoc: + public init(pem: String) { + self.pem = pem + } + + func write(to url: URL) throws { + try pem.write(to: url, atomically: true, encoding: .ascii) + } + + // MARK: Equatable + + /// :nodoc: + public static func ==(lhs: Certificate, rhs: Certificate) -> Bool { + return lhs.pem == rhs.pem + } +} diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift index ba06b06..3acc3fb 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider+Configuration.swift @@ -45,64 +45,6 @@ extension TunnelKitProvider { /// SHA256 message digest. case sha256 = "SHA256" } - - /// The available certificates for handshake. - public enum Handshake: String { - - /// Certificate with RSA 2048-bit key. - case rsa2048 = "RSA-2048" - - /// Certificate with RSA 3072-bit key. - case rsa3072 = "RSA-3072" - - /// Certificate with RSA 4096-bit key. - case rsa4096 = "RSA-4096" - - /// Certificate with ECC based on secp256r1 curve. - case ecc256r1 = "ECC-256r1" - - /// Certificate with ECC based on secp256k1 curve. - case ecc256k1 = "ECC-256k1" - - /// Certificate with ECC based on secp521r1 curve. - case ecc521r1 = "ECC-521r1" - - /// Custom certificate. - /// - /// - Seealso: - case custom = "Custom" - - private static let allDigests: [Handshake: String] = [ - .rsa2048: "e2fccccaba712ccc68449b1c56427ac1", - .rsa3072: "2fcdb65712df9db7dae34a1f4a84e32d", - .rsa4096: "ec085790314aa0ad4b01dda7b756a932", - .ecc256r1: "6f0f23a616479329ce54614f76b52254", - .ecc256k1: "80c3b0f34001e4101e34fde9eb1dfa87", - .ecc521r1: "82446e0c80706e33e6e793cebf1b0c59" - ] - - var digest: String? { - return Handshake.allDigests[self] - } - - func write(to url: URL, custom: String? = nil) throws { - precondition((self != .custom) || (custom != nil)) - - // custom certificate? - if self == .custom, let content = custom { - try content.write(to: url, atomically: true, encoding: .ascii) - return - } - - let bundle = Bundle(for: TunnelKitProvider.self) - let certName = "PIA-\(rawValue)" - guard let certUrl = bundle.url(forResource: certName, withExtension: "pem") else { - fatalError("Could not find \(certName) TLS certificate") - } - let content = try String(contentsOf: certUrl) - try content.write(to: url, atomically: true, encoding: .ascii) - } - } } extension TunnelKitProvider { @@ -215,16 +157,13 @@ extension TunnelKitProvider { /// The message digest algorithm. public var digest: Digest - /// The handshake certificate. - public var handshake: Handshake - - /// The custom CA certificate in PEM format in case `handshake == .custom`. Ignored otherwise. - public var ca: String? + /// The optional CA certificate to validate server against. Set to `nil` to disable CA validation (default). + public var ca: Certificate? /// The MTU of the tunnel. public var mtu: NSNumber - /// The number of seconds after which a renegotiation is started. Set to `nil` to disable renegotiation. + /// The number of seconds after which a renegotiation is started. Set to `nil` to disable renegotiation (default). public var renegotiatesAfterSeconds: Int? // MARK: Debugging @@ -252,7 +191,6 @@ extension TunnelKitProvider { endpointProtocols = [EndpointProtocol(.udp, 1194)] cipher = .aes128cbc digest = .sha1 - handshake = .rsa2048 ca = nil mtu = 1500 renegotiatesAfterSeconds = nil @@ -274,20 +212,12 @@ extension TunnelKitProvider { throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.digestAlgorithm)]") } - // fallback to .rsa2048 in < 0.7 configurations (ca/caDigest) - let fallbackHandshake: Handshake = .rsa2048 - var handshake: Handshake = fallbackHandshake - if let handshakeCertificate = providerConfiguration[S.handshakeCertificate] as? String { - handshake = Handshake(rawValue: handshakeCertificate) ?? fallbackHandshake + let ca: Certificate? + if let caPEM = providerConfiguration[S.ca] as? String { + ca = Certificate(pem: caPEM) + } else { + ca = nil } - if handshake == .custom { - guard let ca = providerConfiguration[S.ca] as? String else { - throw ProviderError.configuration(field: "protocolConfiguration.providerConfiguration[\(S.ca)]") - } - self.ca = ca - } - - self.appGroup = appGroup prefersResolvedAddresses = providerConfiguration[S.prefersResolvedAddresses] as? Bool ?? false resolvedAddresses = providerConfiguration[S.resolvedAddresses] as? [String] @@ -310,9 +240,10 @@ extension TunnelKitProvider { return EndpointProtocol(socketType, port) } + self.appGroup = appGroup self.cipher = cipher self.digest = digest - self.handshake = handshake + self.ca = ca mtu = providerConfiguration[S.mtu] as? NSNumber ?? 1500 renegotiatesAfterSeconds = providerConfiguration[S.renegotiatesAfter] as? Int @@ -345,7 +276,6 @@ extension TunnelKitProvider { endpointProtocols: endpointProtocols, cipher: cipher, digest: digest, - handshake: handshake, ca: ca, mtu: mtu, renegotiatesAfterSeconds: renegotiatesAfterSeconds, @@ -371,8 +301,6 @@ extension TunnelKitProvider { static let digestAlgorithm = "DigestAlgorithm" - static let handshakeCertificate = "HandshakeCertificate" - static let ca = "CA" static let mtu = "MTU" @@ -404,11 +332,8 @@ extension TunnelKitProvider { /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.digest` public let digest: Digest - /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.handshake` - public let handshake: Handshake - /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.ca` - public let ca: String? + public let ca: Certificate? /// - Seealso: `TunnelKitProvider.ConfigurationBuilder.mtu` public let mtu: NSNumber @@ -468,12 +393,11 @@ extension TunnelKitProvider { }, S.cipherAlgorithm: cipher.rawValue, S.digestAlgorithm: digest.rawValue, - S.handshakeCertificate: handshake.rawValue, S.mtu: mtu, S.debug: shouldDebug ] if let ca = ca { - dict[S.ca] = ca + dict[S.ca] = ca.pem } if let resolvedAddresses = resolvedAddresses { dict[S.resolvedAddresses] = resolvedAddresses @@ -526,7 +450,11 @@ extension TunnelKitProvider { log.info("Protocols: \(endpointProtocols)") log.info("Cipher: \(cipher.rawValue)") log.info("Digest: \(digest.rawValue)") - log.info("Handshake: \(handshake.rawValue)") + if let _ = ca { + log.info("CA verification: enabled") + } else { + log.info("CA verification: disabled") + } log.info("MTU: \(mtu)") if let renegotiatesAfterSeconds = renegotiatesAfterSeconds { log.info("Renegotiation: \(renegotiatesAfterSeconds) seconds") @@ -552,7 +480,7 @@ extension TunnelKitProvider.Configuration: Equatable { builder.endpointProtocols = endpointProtocols builder.cipher = cipher builder.digest = digest - builder.handshake = handshake + builder.ca = ca builder.mtu = mtu builder.renegotiatesAfterSeconds = renegotiatesAfterSeconds builder.shouldDebug = shouldDebug @@ -566,7 +494,7 @@ extension TunnelKitProvider.Configuration: Equatable { (lhs.endpointProtocols == rhs.endpointProtocols) && (lhs.cipher == rhs.cipher) && (lhs.digest == rhs.digest) && - (lhs.handshake == rhs.handshake) && + (lhs.ca == rhs.ca) && (lhs.mtu == rhs.mtu) && (lhs.renegotiatesAfterSeconds == rhs.renegotiatesAfterSeconds) ) diff --git a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift index b3b09be..681b22f 100644 --- a/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift +++ b/TunnelKit/Sources/AppExtension/TunnelKitProvider.swift @@ -139,18 +139,23 @@ open class TunnelKitProvider: NEPacketTunnelProvider { return } - do { - try cfg.handshake.write(to: tmpCaURL, custom: cfg.ca) - } catch { - completionHandler(ProviderError.certificateSerialization) - return + let caPath: String? + if let ca = cfg.ca { + do { + try ca.write(to: tmpCaURL) + caPath = tmpCaURL.path + } catch { + completionHandler(ProviderError.certificateSerialization) + return + } + } else { + caPath = nil } cfg.print(appVersion: appVersion) - let caPath = tmpCaURL.path // log.info("Temporary CA is stored to: \(caPath)") - let encryption = SessionProxy.EncryptionParameters(cfg.cipher.rawValue, cfg.digest.rawValue, caPath, cfg.handshake.digest) + let encryption = SessionProxy.EncryptionParameters(cfg.cipher.rawValue, cfg.digest.rawValue, caPath) let credentials = SessionProxy.Credentials(endpoint.username, endpoint.password) let proxy: SessionProxy diff --git a/TunnelKit/Sources/Core/SessionProxy.swift b/TunnelKit/Sources/Core/SessionProxy.swift index e9aaabb..efb4731 100644 --- a/TunnelKit/Sources/Core/SessionProxy.swift +++ b/TunnelKit/Sources/Core/SessionProxy.swift @@ -91,17 +91,13 @@ public class SessionProxy { public let digestName: String /// The path to the CA for TLS negotiation (PEM format). - public let caPath: String - - /// The MD5 digest of the CA (computed from DER format). - public let caDigest: String? + public let caPath: String? /// :nodoc: - public init(_ cipherName: String, _ digestName: String, _ caPath: String, _ caDigest: String?) { + public init(_ cipherName: String, _ digestName: String, _ caPath: String?) { self.cipherName = cipherName self.digestName = digestName self.caPath = caPath - self.caDigest = caDigest } } @@ -834,7 +830,7 @@ public class SessionProxy { negotiationKey.tlsOptional = TLSBox(caPath: encryption.caPath) do { - try negotiationKey.tls.start(withPeerVerification: true) + try negotiationKey.tls.start() } catch let e { deferStop(.shutdown, e) return diff --git a/TunnelKit/Sources/Core/TLSBox.h b/TunnelKit/Sources/Core/TLSBox.h index 7e92c73..96d7eb4 100644 --- a/TunnelKit/Sources/Core/TLSBox.h +++ b/TunnelKit/Sources/Core/TLSBox.h @@ -20,9 +20,9 @@ extern NSString *const TLSBoxPeerVerificationErrorNotification; // @interface TLSBox : NSObject -- (nonnull instancetype)initWithCAPath:(NSString *)caPath; +- (nonnull instancetype)initWithCAPath:(nullable NSString *)caPath; -- (BOOL)startWithPeerVerification:(BOOL)peerVerification error:(NSError **)error; +- (BOOL)startWithError:(NSError **)error; - (NSData *)pullCipherTextWithError:(NSError **)error; // WARNING: text must be able to hold plain text output diff --git a/TunnelKit/Sources/Core/TLSBox.m b/TunnelKit/Sources/Core/TLSBox.m index cac4f96..c5b77b7 100644 --- a/TunnelKit/Sources/Core/TLSBox.m +++ b/TunnelKit/Sources/Core/TLSBox.m @@ -74,7 +74,7 @@ int TLSBoxVerifyPeer(int ok, X509_STORE_CTX *ctx) { free(self.bufferCipherText); } -- (BOOL)startWithPeerVerification:(BOOL)peerVerification error:(NSError *__autoreleasing *)error +- (BOOL)startWithError:(NSError *__autoreleasing *)error { if (!TLSBoxIsOpenSSLLoaded) { // OPENSSL_init_ssl(0, NULL); @@ -84,7 +84,7 @@ int TLSBoxVerifyPeer(int ok, X509_STORE_CTX *ctx) { self.ctx = SSL_CTX_new(TLS_client_method()); SSL_CTX_set_options(self.ctx, SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_COMPRESSION); - if (peerVerification && self.caPath) { + if (self.caPath) { SSL_CTX_set_verify(self.ctx, SSL_VERIFY_PEER, TLSBoxVerifyPeer); if (!SSL_CTX_load_verify_locations(self.ctx, [self.caPath cStringUsingEncoding:NSASCIIStringEncoding], NULL)) { ERR_print_errors_fp(stdout); diff --git a/TunnelKitTests/AppExtensionTests.swift b/TunnelKitTests/AppExtensionTests.swift index 37f0912..58a6f70 100644 --- a/TunnelKitTests/AppExtensionTests.swift +++ b/TunnelKitTests/AppExtensionTests.swift @@ -39,7 +39,7 @@ class AppExtensionTests: XCTestCase { builder.cipher = .aes128cbc builder.digest = .sha256 - builder.handshake = .rsa3072 + builder.ca = Certificate(pem: "abcdef") cfg = builder.build() let proto = try? cfg.generatedTunnelProtocol(withBundleIdentifier: identifier, endpoint: endpoint) @@ -58,7 +58,7 @@ class AppExtensionTests: XCTestCase { XCTAssertEqual(proto?.providerConfiguration?[K.appGroup] as? String, cfg.appGroup) XCTAssertEqual(proto?.providerConfiguration?[K.cipherAlgorithm] as? String, cfg.cipher.rawValue) XCTAssertEqual(proto?.providerConfiguration?[K.digestAlgorithm] as? String, cfg.digest.rawValue) - XCTAssertEqual(proto?.providerConfiguration?[K.handshakeCertificate] as? String, cfg.handshake.rawValue) + XCTAssertEqual(proto?.providerConfiguration?[K.ca] as? String, cfg.ca?.pem) XCTAssertEqual(proto?.providerConfiguration?[K.mtu] as? NSNumber, cfg.mtu) XCTAssertEqual(proto?.providerConfiguration?[K.renegotiatesAfter] as? Int, cfg.renegotiatesAfterSeconds) XCTAssertEqual(proto?.providerConfiguration?[K.debug] as? Bool, cfg.shouldDebug)