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:
parent
56eda2720e
commit
1ceeb8ddbb
|
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Support for SAN hostname in certificates (jaroslavas). [#168](https://github.com/passepartoutvpn/tunnelkit/pull/168)
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- IPv6 traffic broken on Mojave. [#146](https://github.com/passepartoutvpn/tunnelkit/issues/146), [#169](https://github.com/passepartoutvpn/tunnelkit/pull/169)
|
- IPv6 traffic broken on Mojave. [#146](https://github.com/passepartoutvpn/tunnelkit/issues/146), [#169](https://github.com/passepartoutvpn/tunnelkit/pull/169)
|
||||||
|
|
|
@ -50,6 +50,7 @@ typedef NS_ENUM(NSInteger, TunnelKitErrorCode) {
|
||||||
TunnelKitErrorCodeTLSClientKey = 205,
|
TunnelKitErrorCodeTLSClientKey = 205,
|
||||||
TunnelKitErrorCodeTLSServerCertificate = 206,
|
TunnelKitErrorCodeTLSServerCertificate = 206,
|
||||||
TunnelKitErrorCodeTLSServerEKU = 207,
|
TunnelKitErrorCodeTLSServerEKU = 207,
|
||||||
|
TunnelKitErrorCodeTLSServerHost = 208,
|
||||||
TunnelKitErrorCodeDataPathOverflow = 301,
|
TunnelKitErrorCodeDataPathOverflow = 301,
|
||||||
TunnelKitErrorCodeDataPathPeerIdMismatch = 302,
|
TunnelKitErrorCodeDataPathPeerIdMismatch = 302,
|
||||||
TunnelKitErrorCodeDataPathCompression = 303,
|
TunnelKitErrorCodeDataPathCompression = 303,
|
||||||
|
|
|
@ -171,6 +171,10 @@ extension OpenVPNTunnelProvider {
|
||||||
|
|
||||||
static let checksEKU = "ChecksEKU"
|
static let checksEKU = "ChecksEKU"
|
||||||
|
|
||||||
|
static let checksSANHost = "checksSANHost"
|
||||||
|
|
||||||
|
static let sanHost = "sanHost"
|
||||||
|
|
||||||
static let randomizeEndpoint = "RandomizeEndpoint"
|
static let randomizeEndpoint = "RandomizeEndpoint"
|
||||||
|
|
||||||
static let usesPIAPatches = "UsesPIAPatches"
|
static let usesPIAPatches = "UsesPIAPatches"
|
||||||
|
@ -510,6 +514,12 @@ private extension OpenVPN.Configuration {
|
||||||
if let checksEKU = providerConfiguration[S.checksEKU] as? Bool {
|
if let checksEKU = providerConfiguration[S.checksEKU] as? Bool {
|
||||||
builder.checksEKU = checksEKU
|
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 {
|
if let randomizeEndpoint = providerConfiguration[S.randomizeEndpoint] as? Bool {
|
||||||
builder.randomizeEndpoint = randomizeEndpoint
|
builder.randomizeEndpoint = randomizeEndpoint
|
||||||
}
|
}
|
||||||
|
@ -590,6 +600,12 @@ private extension OpenVPN.Configuration {
|
||||||
if let checksEKU = checksEKU {
|
if let checksEKU = checksEKU {
|
||||||
dict[S.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 {
|
if let randomizeEndpoint = randomizeEndpoint {
|
||||||
dict[S.randomizeEndpoint] = randomizeEndpoint
|
dict[S.randomizeEndpoint] = randomizeEndpoint
|
||||||
}
|
}
|
||||||
|
@ -667,6 +683,11 @@ private extension OpenVPN.Configuration {
|
||||||
} else {
|
} else {
|
||||||
log.info("\tServer EKU verification: disabled")
|
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 {
|
if randomizeEndpoint ?? false {
|
||||||
log.info("\tRandomize endpoint: true")
|
log.info("\tRandomize endpoint: true")
|
||||||
}
|
}
|
||||||
|
|
|
@ -842,7 +842,7 @@ extension OpenVPNTunnelProvider {
|
||||||
case .tlsCertificateAuthority, .tlsClientCertificate, .tlsClientKey:
|
case .tlsCertificateAuthority, .tlsClientCertificate, .tlsClientKey:
|
||||||
return .tlsInitialization
|
return .tlsInitialization
|
||||||
|
|
||||||
case .tlsServerCertificate, .tlsServerEKU:
|
case .tlsServerCertificate, .tlsServerEKU, .tlsServerHost:
|
||||||
return .tlsServerVerification
|
return .tlsServerVerification
|
||||||
|
|
||||||
case .tlsHandshake:
|
case .tlsHandshake:
|
||||||
|
|
|
@ -219,6 +219,12 @@ extension OpenVPN {
|
||||||
/// If true, checks EKU of server certificate.
|
/// If true, checks EKU of server certificate.
|
||||||
public var checksEKU: Bool?
|
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.
|
/// Picks endpoint from `remotes` randomly.
|
||||||
public var randomizeEndpoint: Bool?
|
public var randomizeEndpoint: Bool?
|
||||||
|
|
||||||
|
@ -300,6 +306,8 @@ extension OpenVPN {
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
endpointProtocols: endpointProtocols,
|
endpointProtocols: endpointProtocols,
|
||||||
checksEKU: checksEKU,
|
checksEKU: checksEKU,
|
||||||
|
checksSANHost: checksSANHost,
|
||||||
|
sanHost: sanHost,
|
||||||
randomizeEndpoint: randomizeEndpoint,
|
randomizeEndpoint: randomizeEndpoint,
|
||||||
usesPIAPatches: usesPIAPatches,
|
usesPIAPatches: usesPIAPatches,
|
||||||
authToken: authToken,
|
authToken: authToken,
|
||||||
|
@ -382,6 +390,12 @@ extension OpenVPN {
|
||||||
/// - Seealso: `ConfigurationBuilder.checksEKU`
|
/// - Seealso: `ConfigurationBuilder.checksEKU`
|
||||||
public let checksEKU: Bool?
|
public let checksEKU: Bool?
|
||||||
|
|
||||||
|
/// - Seealso: `ConfigurationBuilder.checksSANHost`
|
||||||
|
public let checksSANHost: Bool?
|
||||||
|
|
||||||
|
/// - Seealso: `ConfigurationBuilder.sanHost`
|
||||||
|
public let sanHost: String?
|
||||||
|
|
||||||
/// - Seealso: `ConfigurationBuilder.randomizeEndpoint`
|
/// - Seealso: `ConfigurationBuilder.randomizeEndpoint`
|
||||||
public let randomizeEndpoint: Bool?
|
public let randomizeEndpoint: Bool?
|
||||||
|
|
||||||
|
@ -466,6 +480,8 @@ extension OpenVPN.Configuration {
|
||||||
builder.hostname = hostname
|
builder.hostname = hostname
|
||||||
builder.endpointProtocols = endpointProtocols
|
builder.endpointProtocols = endpointProtocols
|
||||||
builder.checksEKU = checksEKU
|
builder.checksEKU = checksEKU
|
||||||
|
builder.checksSANHost = checksSANHost
|
||||||
|
builder.sanHost = sanHost
|
||||||
builder.randomizeEndpoint = randomizeEndpoint
|
builder.randomizeEndpoint = randomizeEndpoint
|
||||||
builder.usesPIAPatches = usesPIAPatches
|
builder.usesPIAPatches = usesPIAPatches
|
||||||
builder.authToken = authToken
|
builder.authToken = authToken
|
||||||
|
|
|
@ -787,7 +787,9 @@ public class OpenVPNSession: Session {
|
||||||
caPath: caURL.path,
|
caPath: caURL.path,
|
||||||
clientCertificatePath: (configuration.clientCertificate != nil) ? clientCertificateURL.path : nil,
|
clientCertificatePath: (configuration.clientCertificate != nil) ? clientCertificateURL.path : nil,
|
||||||
clientKeyPath: (configuration.clientKey != nil) ? clientKeyURL.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 {
|
if let tlsSecurityLevel = configuration.tlsSecurityLevel {
|
||||||
tls.securityLevel = tlsSecurityLevel
|
tls.securityLevel = tlsSecurityLevel
|
||||||
|
|
|
@ -61,7 +61,9 @@ extern const NSInteger TLSBoxDefaultSecurityLevel;
|
||||||
- (instancetype)initWithCAPath:(NSString *)caPath
|
- (instancetype)initWithCAPath:(NSString *)caPath
|
||||||
clientCertificatePath:(nullable NSString *)clientCertificatePath
|
clientCertificatePath:(nullable NSString *)clientCertificatePath
|
||||||
clientKeyPath:(nullable NSString *)clientKeyPath
|
clientKeyPath:(nullable NSString *)clientKeyPath
|
||||||
checksEKU:(BOOL)checksEKU;
|
checksEKU:(BOOL)checksEKU
|
||||||
|
checksSANHost:(BOOL)checksSANHost
|
||||||
|
hostname:(nullable NSString *)hostname;
|
||||||
|
|
||||||
- (BOOL)startWithError:(NSError **)error;
|
- (BOOL)startWithError:(NSError **)error;
|
||||||
|
|
||||||
|
|
|
@ -69,6 +69,8 @@ const NSInteger TLSBoxDefaultSecurityLevel = -1;
|
||||||
@property (nonatomic, strong) NSString *clientCertificatePath;
|
@property (nonatomic, strong) NSString *clientCertificatePath;
|
||||||
@property (nonatomic, strong) NSString *clientKeyPath;
|
@property (nonatomic, strong) NSString *clientKeyPath;
|
||||||
@property (nonatomic, assign) BOOL checksEKU;
|
@property (nonatomic, assign) BOOL checksEKU;
|
||||||
|
@property (nonatomic, assign) BOOL checksSANHost;
|
||||||
|
@property (nonatomic, strong) NSString *hostname;
|
||||||
@property (nonatomic, assign) BOOL isConnected;
|
@property (nonatomic, assign) BOOL isConnected;
|
||||||
|
|
||||||
@property (nonatomic, unsafe_unretained) SSL_CTX *ctx;
|
@property (nonatomic, unsafe_unretained) SSL_CTX *ctx;
|
||||||
|
@ -174,14 +176,18 @@ const NSInteger TLSBoxDefaultSecurityLevel = -1;
|
||||||
clientCertificatePath:(NSString *)clientCertificatePath
|
clientCertificatePath:(NSString *)clientCertificatePath
|
||||||
clientKeyPath:(NSString *)clientKeyPath
|
clientKeyPath:(NSString *)clientKeyPath
|
||||||
checksEKU:(BOOL)checksEKU
|
checksEKU:(BOOL)checksEKU
|
||||||
|
checksSANHost:(BOOL)checksSANHost
|
||||||
|
hostname:(nullable NSString *)hostname
|
||||||
{
|
{
|
||||||
if ((self = [super init])) {
|
if ((self = [super init])) {
|
||||||
self.caPath = caPath;
|
self.caPath = caPath;
|
||||||
self.clientCertificatePath = clientCertificatePath;
|
self.clientCertificatePath = clientCertificatePath;
|
||||||
self.clientKeyPath = clientKeyPath;
|
self.clientKeyPath = clientKeyPath;
|
||||||
self.checksEKU = checksEKU;
|
self.checksEKU = checksEKU;
|
||||||
|
self.checksSANHost = checksSANHost;
|
||||||
self.bufferCipherText = allocate_safely(TLSBoxMaxBufferLength);
|
self.bufferCipherText = allocate_safely(TLSBoxMaxBufferLength);
|
||||||
self.securityLevel = TLSBoxDefaultSecurityLevel;
|
self.securityLevel = TLSBoxDefaultSecurityLevel;
|
||||||
|
self.hostname = hostname;
|
||||||
}
|
}
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
@ -275,6 +281,13 @@ const NSInteger TLSBoxDefaultSecurityLevel = -1;
|
||||||
}
|
}
|
||||||
return nil;
|
return nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.checksSANHost && ![self verifySANHostWithSSL:self.ssl]) {
|
||||||
|
if (error) {
|
||||||
|
*error = TunnelKitErrorWithCode(TunnelKitErrorCodeTLSServerHost);
|
||||||
|
}
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (ret > 0) {
|
if (ret > 0) {
|
||||||
return [NSData dataWithBytes:self.bufferCipherText length:ret];
|
return [NSData dataWithBytes:self.bufferCipherText length:ret];
|
||||||
|
@ -397,4 +410,70 @@ const NSInteger TLSBoxDefaultSecurityLevel = -1;
|
||||||
return isValid;
|
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
|
@end
|
||||||
|
|
Loading…
Reference in New Issue