We're developing HTTP server which server multiple hostnames, thus we need to presenting certificates according to requested names. It all should be handled on same listening port(443). What are options to analyze client's TLS requested ServerName Identifier(SNI) and present certificate accordingly ?
So far we were successful when using single certificate but all this is done when before starting listener.
let parameters = NWParameters(tls: tlsOptions, tcp: tcpOptions )
if let secIdentity = getSecIdentity(), let identity = sec_identity_create(secIdentity) {
sec_protocol_options_set_min_tls_protocol_version(tlsOptions.securityProtocolOptions, .TLSv13)
sec_protocol_options_set_local_identity(tlsOptions.securityProtocolOptions, identity)
sec_protocol_options_append_tls_ciphersuite( tlsOptions.securityProtocolOptions, tls_ciphersuite_t(rawValue: UInt16(TLS_AES_128_GCM_SHA256))! )
}
}
let listener = try NWListener(using: parameters, on: 443)
We're developing HTTP server which server multiple hostnames, thus we need to presenting certificates according to requested names. It all should be handled on same listening port(443). What are options to analyze client's TLS requested ServerName Identifier(SNI) and present certificate accordingly ?
I just spun up a local test program on macOS and was able to do this using the following technique:
-
Create a local certificate authority that issues leaf certificates for each hostname being served by your server.
-
Install the root and the leaf identities into the Keychain. Note, I am using a testing strategy that sets the identity name in the Keychain as the servername. For example,
https://sully.local:4433
andhttps://biff.local:4433
. -
Then set
sec_protocol_options_set_challenge_block
to hand thesec_protocol_metadata_t
data tosec_protocol_metadata_get_server_name
and use this to extract the identity and use it on the connection.
func getSecIdentity(name: String) -> SecIdentity? {
var identity: SecIdentity?
let getquery = [kSecClass: kSecClassCertificate,
kSecAttrLabel: name,
kSecReturnRef: true] as NSDictionary
var item: CFTypeRef?
let status = SecItemCopyMatching(getquery as CFDictionary, &item)
guard status == errSecSuccess else {
print("Failed to get cetificate: \(status)")
return nil
}
let certificate = item as! SecCertificate
let identityStatus = SecIdentityCreateWithCertificate(nil, certificate, &identity)
guard identityStatus == errSecSuccess else {
print("Failed to get sec identity: \(identityStatus)")
return nil
}
return identity
}
sec_protocol_options_set_challenge_block(tlsOptions.securityProtocolOptions, { (metadata, completion) in
guard let name = sec_protocol_metadata_get_server_name(metadata) else {
completion(nil)
print("Failed to get the server name, returning nil")
return
}
let serverName = String(cString: name)
guard let secIdentity = self.getSecIdentity(name: serverName),
let identity = sec_identity_create(secIdentity) else {
print("Failed to get the correct secIdentity, returning nil")
completion(nil)
return
}
completion(identity)
}, .main)
Now, the case I did not try this with was TLS 1.3. I just noticed that you have that set as your protocol version, so let me know how this goes.
Note, after you test case, make sure to clean up your Keychain with any lingering test identities.
Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com