From 1ceeb8ddbbf4238224d1adbd4dbfbeaa46bfdfe8 Mon Sep 17 00:00:00 2001 From: Jaroslav_ <1903354+jaroslavas@users.noreply.github.com> Date: Sat, 9 May 2020 01:02:16 +0300 Subject: [PATCH] SAN host check (#168) * Check if host is present in certificates SAN list * Save .tlsServerHost error as .tlsServerVerification into last error Co-authored-by: Davide De Rosa --- CHANGELOG.md | 4 + TunnelKit/Sources/Core/Errors.h | 1 + .../OpenVPNTunnelProvider+Configuration.swift | 23 +++++- .../AppExtension/OpenVPNTunnelProvider.swift | 2 +- .../Protocols/OpenVPN/Configuration.swift | 16 ++++ .../Protocols/OpenVPN/OpenVPNSession.swift | 4 +- TunnelKit/Sources/Protocols/OpenVPN/TLSBox.h | 4 +- TunnelKit/Sources/Protocols/OpenVPN/TLSBox.m | 79 +++++++++++++++++++ 8 files changed, 129 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bfe89e..ebb23fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Support for SAN hostname in certificates (jaroslavas). [#168](https://github.com/passepartoutvpn/tunnelkit/pull/168) + ### Fixed - IPv6 traffic broken on Mojave. [#146](https://github.com/passepartoutvpn/tunnelkit/issues/146), [#169](https://github.com/passepartoutvpn/tunnelkit/pull/169) diff --git a/TunnelKit/Sources/Core/Errors.h b/TunnelKit/Sources/Core/Errors.h index a2fcd81..f8c9fdd 100644 --- a/TunnelKit/Sources/Core/Errors.h +++ b/TunnelKit/Sources/Core/Errors.h @@ -50,6 +50,7 @@ typedef NS_ENUM(NSInteger, TunnelKitErrorCode) { TunnelKitErrorCodeTLSClientKey = 205, TunnelKitErrorCodeTLSServerCertificate = 206, TunnelKitErrorCodeTLSServerEKU = 207, + TunnelKitErrorCodeTLSServerHost = 208, TunnelKitErrorCodeDataPathOverflow = 301, TunnelKitErrorCodeDataPathPeerIdMismatch = 302, TunnelKitErrorCodeDataPathCompression = 303, diff --git a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider+Configuration.swift b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider+Configuration.swift index 69aa44d..397e8ab 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider+Configuration.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider+Configuration.swift @@ -170,7 +170,11 @@ extension OpenVPNTunnelProvider { static let renegotiatesAfter = "RenegotiatesAfter" static let checksEKU = "ChecksEKU" - + + static let checksSANHost = "checksSANHost" + + static let sanHost = "sanHost" + static let randomizeEndpoint = "RandomizeEndpoint" static let usesPIAPatches = "UsesPIAPatches" @@ -510,6 +514,12 @@ private extension OpenVPN.Configuration { if let checksEKU = providerConfiguration[S.checksEKU] as? Bool { builder.checksEKU = checksEKU } + if let checksSANHost = providerConfiguration[S.checksSANHost] as? Bool { + builder.checksSANHost = checksSANHost + } + if let sanHost = providerConfiguration[S.sanHost] as? String { + builder.sanHost = sanHost + } if let randomizeEndpoint = providerConfiguration[S.randomizeEndpoint] as? Bool { builder.randomizeEndpoint = randomizeEndpoint } @@ -590,6 +600,12 @@ private extension OpenVPN.Configuration { if let checksEKU = checksEKU { dict[S.checksEKU] = checksEKU } + if let checksSANHost = checksSANHost { + dict[S.checksSANHost] = checksSANHost + } + if let sanHost = sanHost { + dict[S.sanHost] = sanHost + } if let randomizeEndpoint = randomizeEndpoint { dict[S.randomizeEndpoint] = randomizeEndpoint } @@ -667,6 +683,11 @@ private extension OpenVPN.Configuration { } else { log.info("\tServer EKU verification: disabled") } + if checksSANHost ?? false { + log.info("\tHost SAN verification: enabled (\(sanHost ?? "-"))") + } else { + log.info("\tHost SAN verification: disabled") + } if randomizeEndpoint ?? false { log.info("\tRandomize endpoint: true") } diff --git a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider.swift b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider.swift index 979a79b..d3e804b 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/AppExtension/OpenVPNTunnelProvider.swift @@ -842,7 +842,7 @@ extension OpenVPNTunnelProvider { case .tlsCertificateAuthority, .tlsClientCertificate, .tlsClientKey: return .tlsInitialization - case .tlsServerCertificate, .tlsServerEKU: + case .tlsServerCertificate, .tlsServerEKU, .tlsServerHost: return .tlsServerVerification case .tlsHandshake: diff --git a/TunnelKit/Sources/Protocols/OpenVPN/Configuration.swift b/TunnelKit/Sources/Protocols/OpenVPN/Configuration.swift index ac691e1..f5df49c 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/Configuration.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/Configuration.swift @@ -219,6 +219,12 @@ extension OpenVPN { /// If true, checks EKU of server certificate. public var checksEKU: Bool? + /// If true, checks if hostname (sanHost) is present in certificates SAN. + public var checksSANHost: Bool? + + /// The server hostname used for checking certificate SAN. + public var sanHost: String? + /// Picks endpoint from `remotes` randomly. public var randomizeEndpoint: Bool? @@ -300,6 +306,8 @@ extension OpenVPN { hostname: hostname, endpointProtocols: endpointProtocols, checksEKU: checksEKU, + checksSANHost: checksSANHost, + sanHost: sanHost, randomizeEndpoint: randomizeEndpoint, usesPIAPatches: usesPIAPatches, authToken: authToken, @@ -382,6 +390,12 @@ extension OpenVPN { /// - Seealso: `ConfigurationBuilder.checksEKU` public let checksEKU: Bool? + /// - Seealso: `ConfigurationBuilder.checksSANHost` + public let checksSANHost: Bool? + + /// - Seealso: `ConfigurationBuilder.sanHost` + public let sanHost: String? + /// - Seealso: `ConfigurationBuilder.randomizeEndpoint` public let randomizeEndpoint: Bool? @@ -466,6 +480,8 @@ extension OpenVPN.Configuration { builder.hostname = hostname builder.endpointProtocols = endpointProtocols builder.checksEKU = checksEKU + builder.checksSANHost = checksSANHost + builder.sanHost = sanHost builder.randomizeEndpoint = randomizeEndpoint builder.usesPIAPatches = usesPIAPatches builder.authToken = authToken diff --git a/TunnelKit/Sources/Protocols/OpenVPN/OpenVPNSession.swift b/TunnelKit/Sources/Protocols/OpenVPN/OpenVPNSession.swift index bb45a12..d781147 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/OpenVPNSession.swift +++ b/TunnelKit/Sources/Protocols/OpenVPN/OpenVPNSession.swift @@ -787,7 +787,9 @@ public class OpenVPNSession: Session { caPath: caURL.path, clientCertificatePath: (configuration.clientCertificate != nil) ? clientCertificateURL.path : nil, clientKeyPath: (configuration.clientKey != nil) ? clientKeyURL.path : nil, - checksEKU: configuration.checksEKU ?? false + checksEKU: configuration.checksEKU ?? false, + checksSANHost: configuration.checksSANHost ?? false, + hostname: configuration.sanHost ) if let tlsSecurityLevel = configuration.tlsSecurityLevel { tls.securityLevel = tlsSecurityLevel diff --git a/TunnelKit/Sources/Protocols/OpenVPN/TLSBox.h b/TunnelKit/Sources/Protocols/OpenVPN/TLSBox.h index bd47a0c..558aea9 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/TLSBox.h +++ b/TunnelKit/Sources/Protocols/OpenVPN/TLSBox.h @@ -61,7 +61,9 @@ extern const NSInteger TLSBoxDefaultSecurityLevel; - (instancetype)initWithCAPath:(NSString *)caPath clientCertificatePath:(nullable NSString *)clientCertificatePath clientKeyPath:(nullable NSString *)clientKeyPath - checksEKU:(BOOL)checksEKU; + checksEKU:(BOOL)checksEKU + checksSANHost:(BOOL)checksSANHost + hostname:(nullable NSString *)hostname; - (BOOL)startWithError:(NSError **)error; diff --git a/TunnelKit/Sources/Protocols/OpenVPN/TLSBox.m b/TunnelKit/Sources/Protocols/OpenVPN/TLSBox.m index bdda89c..ce0c171 100644 --- a/TunnelKit/Sources/Protocols/OpenVPN/TLSBox.m +++ b/TunnelKit/Sources/Protocols/OpenVPN/TLSBox.m @@ -69,6 +69,8 @@ const NSInteger TLSBoxDefaultSecurityLevel = -1; @property (nonatomic, strong) NSString *clientCertificatePath; @property (nonatomic, strong) NSString *clientKeyPath; @property (nonatomic, assign) BOOL checksEKU; +@property (nonatomic, assign) BOOL checksSANHost; +@property (nonatomic, strong) NSString *hostname; @property (nonatomic, assign) BOOL isConnected; @property (nonatomic, unsafe_unretained) SSL_CTX *ctx; @@ -174,14 +176,18 @@ const NSInteger TLSBoxDefaultSecurityLevel = -1; clientCertificatePath:(NSString *)clientCertificatePath clientKeyPath:(NSString *)clientKeyPath checksEKU:(BOOL)checksEKU + checksSANHost:(BOOL)checksSANHost + hostname:(nullable NSString *)hostname { if ((self = [super init])) { self.caPath = caPath; self.clientCertificatePath = clientCertificatePath; self.clientKeyPath = clientKeyPath; self.checksEKU = checksEKU; + self.checksSANHost = checksSANHost; self.bufferCipherText = allocate_safely(TLSBoxMaxBufferLength); self.securityLevel = TLSBoxDefaultSecurityLevel; + self.hostname = hostname; } return self; } @@ -275,6 +281,13 @@ const NSInteger TLSBoxDefaultSecurityLevel = -1; } return nil; } + + if (self.checksSANHost && ![self verifySANHostWithSSL:self.ssl]) { + if (error) { + *error = TunnelKitErrorWithCode(TunnelKitErrorCodeTLSServerHost); + } + return nil; + } } if (ret > 0) { return [NSData dataWithBytes:self.bufferCipherText length:ret]; @@ -397,4 +410,70 @@ const NSInteger TLSBoxDefaultSecurityLevel = -1; return isValid; } +#pragma mark SAN + +- (BOOL)verifySANHostWithSSL:(SSL *)ssl { + X509 *cert = SSL_get_peer_certificate(self.ssl); + if (!cert) { + return NO; + } + + GENERAL_NAMES* names = NULL; + unsigned char* utf8 = NULL; + names = X509_get_ext_d2i(cert, NID_subject_alt_name, 0, 0 ); + if(!names) { + X509_free(cert); + return NO; + } + + int i = 0, count = sk_GENERAL_NAME_num(names); + if(!count) { + X509_free(cert); + GENERAL_NAMES_free(names); + return NO; + } + BOOL isValid = NO; + + for( i = 0; i < count; ++i ) { + GENERAL_NAME* entry = sk_GENERAL_NAME_value(names, i); + if(!entry) { + continue; + } + if(GEN_DNS != entry->type) { + continue; + } + + int len1 = 0, len2 = -1; + len1 = ASN1_STRING_to_UTF8(&utf8, entry->d.dNSName); + if(!utf8) { + continue; + } + len2 = (int)strlen((const char*)utf8); + + if(len1 != len2) { + OPENSSL_free(utf8); + utf8 = NULL; + continue; + } + + if(utf8 && len1 && len2 && (len1 == len2) && strcmp((const char *)utf8, self.hostname.UTF8String) == 0) { + isValid = YES; + break; + } + + OPENSSL_free(utf8); + utf8 = NULL; + } + + X509_free(cert); + + if(names) { + GENERAL_NAMES_free(names); + } + if(utf8) { + OPENSSL_free(utf8); + } + return isValid; +} + @end