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 <keeshux@gmail.com>
This commit is contained in:
Jaroslav_ 2020-05-09 01:02:16 +03:00 committed by GitHub
parent 56eda2720e
commit 1ceeb8ddbb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 129 additions and 4 deletions

View File

@ -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)

View File

@ -50,6 +50,7 @@ typedef NS_ENUM(NSInteger, TunnelKitErrorCode) {
TunnelKitErrorCodeTLSClientKey = 205,
TunnelKitErrorCodeTLSServerCertificate = 206,
TunnelKitErrorCodeTLSServerEKU = 207,
TunnelKitErrorCodeTLSServerHost = 208,
TunnelKitErrorCodeDataPathOverflow = 301,
TunnelKitErrorCodeDataPathPeerIdMismatch = 302,
TunnelKitErrorCodeDataPathCompression = 303,

View File

@ -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")
}

View File

@ -842,7 +842,7 @@ extension OpenVPNTunnelProvider {
case .tlsCertificateAuthority, .tlsClientCertificate, .tlsClientKey:
return .tlsInitialization
case .tlsServerCertificate, .tlsServerEKU:
case .tlsServerCertificate, .tlsServerEKU, .tlsServerHost:
return .tlsServerVerification
case .tlsHandshake:

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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