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
|
||||
|
||||
### 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)
|
||||
|
|
|
@ -50,6 +50,7 @@ typedef NS_ENUM(NSInteger, TunnelKitErrorCode) {
|
|||
TunnelKitErrorCodeTLSClientKey = 205,
|
||||
TunnelKitErrorCodeTLSServerCertificate = 206,
|
||||
TunnelKitErrorCodeTLSServerEKU = 207,
|
||||
TunnelKitErrorCodeTLSServerHost = 208,
|
||||
TunnelKitErrorCodeDataPathOverflow = 301,
|
||||
TunnelKitErrorCodeDataPathPeerIdMismatch = 302,
|
||||
TunnelKitErrorCodeDataPathCompression = 303,
|
||||
|
|
|
@ -171,6 +171,10 @@ extension OpenVPNTunnelProvider {
|
|||
|
||||
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")
|
||||
}
|
||||
|
|
|
@ -842,7 +842,7 @@ extension OpenVPNTunnelProvider {
|
|||
case .tlsCertificateAuthority, .tlsClientCertificate, .tlsClientKey:
|
||||
return .tlsInitialization
|
||||
|
||||
case .tlsServerCertificate, .tlsServerEKU:
|
||||
case .tlsServerCertificate, .tlsServerEKU, .tlsServerHost:
|
||||
return .tlsServerVerification
|
||||
|
||||
case .tlsHandshake:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue