presenting appropriate certificate according to client's SNI using NWListener

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)
Answered by Systems Engineer in 706545022

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:

  1. Create a local certificate authority that issues leaf certificates for each hostname being served by your server.

  2. 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 and https://biff.local:4433.

  3. Then set sec_protocol_options_set_challenge_block to hand the sec_protocol_metadata_t data to sec_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
Accepted Answer

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:

  1. Create a local certificate authority that issues leaf certificates for each hostname being served by your server.

  2. 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 and https://biff.local:4433.

  3. Then set sec_protocol_options_set_challenge_block to hand the sec_protocol_metadata_t data to sec_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

@meaton, It seems to do the trick 👍🏻 I'll let You know how it went with TLS 1.3. Any ideas how to set forward secrecy flag on ?

@meaton, It seems to do the trick 👍

Glad that worked out.

Regarding:

Any ideas how to set forward secrecy flag on ?

Forward Secrecy should be enabled by default so there should be no need to set the flag to on. See the discussion at the bottom of the NSExceptionRequiresForwardSecrecy.

Matt Eaton
DTS Engineering, CoreOS
meaton3@apple.com
presenting appropriate certificate according to client's SNI using NWListener
 
 
Q