Using Network Framework + Bonjour + QUIC + TLS

Hello,

I was able to use the TicTackToe code base and modify it such that I have a toggle at the top of the screen that allows me to start / stop the NWBrowser and NWListener. I have it setup so when the browser finds another device it attempts to connect to it. I support N devices / connections. I am able to use the NWParameters extension that is in the TickTackToe game that uses a passcode and TLS. I am able to send messages between devices just fine. Here is what I used

extension NWParameters {
    // Create parameters for use in PeerConnection and PeerListener.
    convenience init(passcode: String) {
        // Customize TCP options to enable keepalives.
        let tcpOptions = NWProtocolTCP.Options()
        tcpOptions.enableKeepalive = true
        tcpOptions.keepaliveIdle = 2

        // Create parameters with custom TLS and TCP options.
        self.init(tls: NWParameters.tlsOptions(passcode: passcode), tcp: tcpOptions)

        // Enable using a peer-to-peer link.
        self.includePeerToPeer = true
    }

    // Create TLS options using a passcode to derive a preshared key.
    private static func tlsOptions(passcode: String) -> NWProtocolTLS.Options {
        let tlsOptions = NWProtocolTLS.Options()

        let authenticationKey = SymmetricKey(data: passcode.data(using: .utf8)!)
        let authenticationCode = HMAC<SHA256>.authenticationCode(for: "HI".data(using: .utf8)!, using: authenticationKey)

        let authenticationDispatchData = authenticationCode.withUnsafeBytes {
            DispatchData(bytes: $0)
        }

        sec_protocol_options_add_pre_shared_key(tlsOptions.securityProtocolOptions,
                                                authenticationDispatchData as __DispatchData,
                                                stringToDispatchData("HI")! as __DispatchData)
        sec_protocol_options_append_tls_ciphersuite(tlsOptions.securityProtocolOptions,
                                                    tls_ciphersuite_t(rawValue: TLS_PSK_WITH_AES_128_GCM_SHA256)!)
        return tlsOptions
    }

    // Create a utility function to encode strings as preshared key data.
    private static func stringToDispatchData(_ string: String) -> DispatchData? {
        guard let stringData = string.data(using: .utf8) else {
            return nil
        }
        let dispatchData = stringData.withUnsafeBytes {
            DispatchData(bytes: $0)
        }
        return dispatchData
    }
}

When I try to modify it to use QUIC and TLS 1.3 like so

extension NWParameters {
    // Create parameters for use in PeerConnection and PeerListener.
    convenience init(psk: String) {
        self.init(quic: NWParameters.quicOptions(psk: psk))

        self.includePeerToPeer = true
    }
    
    private static func quicOptions(psk: String) -> NWProtocolQUIC.Options {
        let quicOptions = NWProtocolQUIC.Options(alpn: ["h3"])

        let authenticationKey = SymmetricKey(data: psk.data(using: .utf8)!)
        let authenticationCode = HMAC<SHA256>.authenticationCode(for: "hello".data(using: .utf8)!, using: authenticationKey)

        let authenticationDispatchData = authenticationCode.withUnsafeBytes {
            DispatchData(bytes: $0)
        }
        sec_protocol_options_set_min_tls_protocol_version(quicOptions.securityProtocolOptions, .TLSv13)
        sec_protocol_options_set_max_tls_protocol_version(quicOptions.securityProtocolOptions, .TLSv13)
        
        sec_protocol_options_add_pre_shared_key(quicOptions.securityProtocolOptions,
                                                authenticationDispatchData as __DispatchData,
                                                stringToDispatchData("hello")! as __DispatchData)

        sec_protocol_options_append_tls_ciphersuite(quicOptions.securityProtocolOptions,
                                                    tls_ciphersuite_t(rawValue: TLS_AES_128_GCM_SHA256)!)
        
        sec_protocol_options_set_verify_block(quicOptions.securityProtocolOptions, { _, _, sec_protocol_verify_complete in
            sec_protocol_verify_complete(true)
        }, .main)
        
        return quicOptions
    }

    // Create a utility function to encode strings as preshared key data.
    private static func stringToDispatchData(_ string: String) -> DispatchData? {
        guard let stringData = string.data(using: .utf8) else {
            return nil
        }
        let dispatchData = stringData.withUnsafeBytes {
            DispatchData(bytes: $0)
        }
        return dispatchData
    }
}

I get the following errors in the console

boringssl_session_handshake_incomplete(241) [C3:1][0x109d0c600] SSL library error boringssl_session_handshake_error_print(44) [C3:1][0x109d0c600] Error: 4459057536:error:100000ae:SSL routines:OPENSSL_internal:NO_CERTIFICATE_SET:/Library/Caches/com.apple.xbs/Sources/boringssl/ssl/tls13_server.cc:882: boringssl_session_handshake_incomplete(241) [C4:1][0x109d0d200] SSL library error boringssl_session_handshake_error_print(44) [C4:1][0x109d0d200] Error: 4459057536:error:100000ae:SSL routines:OPENSSL_internal:NO_CERTIFICATE_SET:/Library/Caches/com.apple.xbs/Sources/boringssl/ssl/tls13_server.cc:882: nw_endpoint_flow_failed_with_error [C3 fe80::1884:2662:90ca:b011%en0.65328 in_progress channel-flow (satisfied (Path is satisfied), viable, interface: en0[802.11], scoped, ipv4, dns, uses wifi)] already failing, returning nw_endpoint_flow_failed_with_error [C4 192.168.0.98:65396 in_progress channel-flow (satisfied (Path is satisfied), viable, interface: en0[802.11], scoped, ipv4, dns, uses wifi)] already failing, returning quic_crypto_connection_state_handler [C1:1] [2ae0263d7dc186c7-] TLS error -9858 (state failed) nw_connection_copy_connected_local_endpoint_block_invoke [C3] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection nw_connection_copy_connected_remote_endpoint_block_invoke [C3] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection nw_connection_copy_protocol_metadata_internal_block_invoke [C3] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection quic_crypto_connection_state_handler [C2:1] [84fdc1e910f59f0a-] TLS error -9858 (state failed) nw_connection_copy_connected_local_endpoint_block_invoke [C4] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection nw_connection_copy_connected_remote_endpoint_block_invoke [C4] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection nw_connection_copy_protocol_metadata_internal_block_invoke [C4] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection

Am I missing some configuration? I noticed with the working code that uses TCP and TLS that there is an NWParameters initializer that accepts tls options and tcp option but there isnt one that accepts tls and quic.

Thank you for any help :)

Answered by DTS Engineer in 814642022

The obvious path forward here won’t work. You’re trying to combine two features:

  • QUIC

  • TLS-PSK

The issue is that QUIC requires TLS 1.3 but we only support TLS-PSK with TLS 1.2.

Thus, to work with QUIC you need to use standard TLS, that is, your server must have a digital identity. It’s possible to do that in a peer-to-peer environment, but it’s not trivial.

The best path forward depends on the nature of your product and where you are in its development. If you’re just getting started and want to explore QUIC, I recommend that you hard code a digital identity into your server. That’ll get QUIC working, and allow you to verify that it offers the features that you need. If you then decide that you really do want to use QUIC, we can talk about how to generate a digital identity for your server.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I came here for help and advise and I got just that. Thank you. I want to head your input so I'll be attempting to get the basics working with QUIC. In my mind they are as follows:

  • Generate a digital identity per user and store it on my backend
  • Be able to send generated digital identity to ios app after the user authenticates
  • Store digital identity in keychain
  • Use digital identity to setup TLS for QUIC NWListener and NWConnection
  • Use SHA256(user.id + "TXT_RECORD" + user.email + "TXT_RECORD" + user.id) for the TXT Record
  • Be able to validate / verify the certificate chain while in sec_protocol_options_set_verify_block

Am I missing anything?

Can I please have some assistance on how to properly setup an NWParameters extension to accept a base64Encoded public certificate and then use it to correctly secure connections?

I have this so far

extension NWParameters {
    convenience init(base64EncodedCert: String) throws {
        // Create QUIC parameters with the TLS options
        let quicOptions = NWProtocolQUIC.Options(alpn: ["h3"])
        
        // Convert the base64 string to a string to handle PEM format
        guard let pemData = Data(base64Encoded: base64EncodedCert),
              let pemString = String(data: pemData, encoding: .utf8)
        else {
            print("Failed to decode initial base64 string")
            throw CertificateError.invalidBase64String
        }
        
        // Extract the certificate content between the PEM markers
        let lines = pemString.components(separatedBy: .newlines)
        let certificateLines = lines.filter { line in
            !line.contains("BEGIN CERTIFICATE") && !line.contains("END CERTIFICATE") && !line.isEmpty
        }
        
        // Join the lines and create certificate data
        let certificateString = certificateLines.joined()
        guard let certificateData = Data(base64Encoded: certificateString) else {
            print("Failed to decode certificate content")
            throw CertificateError.invalidBase64String
        }
        
        print("Successfully decoded certificate content, data length: \(certificateData.count)")
        
        // Try to create certificate
        guard let certificate = SecCertificateCreateWithData(nil, certificateData as CFData) else {
            print("Failed to create certificate from data")
            throw CertificateError.certificateCreationFailed
        }
        
        sec_protocol_options_set_min_tls_protocol_version(quicOptions.securityProtocolOptions, .TLSv13)
        sec_protocol_options_set_max_tls_protocol_version(quicOptions.securityProtocolOptions, .TLSv13)
        
        print("Successfully created certificate")
        
        // Get certificate summary for verification
        let certificateSummary = SecCertificateCopySubjectSummary(certificate) as? String
        print("Certificate Summary: \(certificateSummary ?? "No summary available")")
        
        // Create identity from certificate
        sec_protocol_options_set_verify_block(quicOptions.securityProtocolOptions, { _, sec_trust, sec_protocol_verify_complete in
            
            // Get the certificates from the trust object
            let trust = sec_trust_copy_ref(sec_trust).takeRetainedValue()
            
            // Get the certificate chain
            guard let certificates = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
                  let peerCertificate = certificates.first
            else {
                print("Failed to get peer certificate from chain")
                sec_protocol_verify_complete(false)
                return
            }
            
            // Compare the peer's certificate with our pinned certificate
            let peerCertificateData = SecCertificateCopyData(peerCertificate) as Data
            let pinnedCertificateData = SecCertificateCopyData(certificate) as Data
            
            // Verify the certificates match
            let certificatesMatch = peerCertificateData == pinnedCertificateData
            print("Certificate verification result: \(certificatesMatch)")
            
            sec_protocol_verify_complete(certificatesMatch)
        }, DispatchQueue(label: "com.example.certificate.verification"))
        
        self.init(quic: quicOptions)
    }
}

I am seeing "Certificate Summary: " printed to the console but I am getting the following errors

Connection state changed to: failed(-9858: handshake failed)
Connection failed: -9858: handshake failed

You said this previously

If you want to authenticate the device, then each device needs its own identity. In that case, your identity generation code will need to include something device specific into the identity’s certificate so that a peer can tell that remote peer is the device that it’s expecting.

If you want to authenticate the user, then you can use a single identity for that. A peer can tell that the remote peer is the same user by checking that the certificate matches the certificate in the digital identity that it’s using.

The tricky part of about the latter is that your server has to store the digital identity (so, the certificate and the private key) so that new clients can access it. OTOH, if you authenticate the device then each device retains its own private key and the server only has to issue the certificate and store that.

I am getting stuck on this step. I used this online resource to create a certificate for testing https://www.samltool.com/self_signed_certs.php. I tried to base64 encode it and make it a constant in my codebase. I then tried to base64 decode it and then use it to create a certificate. I was able to do that successfully. The connection failed and I think it's because I don't have an identity but when I go to create an identity it's asking for a private key.

Between the two options you shared above I would think id want the "authenticate the user" because I don't care about the device - just that the same user is trying to connect.

When you say "then you can use a single identity" does that mean I send the client the certificate I created on my backend and then the client uses a private key it creates on its device to then create an identity? Do I send both the private key and certificate used on the backend to the client which is then used to create an identity.

If the "authenticate the device" is easier im open to that. The goal is as i shared above "bob's devices cant connect to tom's devices and vice versa". The "something device specific into the identity’s certificate" could be some hash identifier similar to what I shared about TXT_RECORD.

You are mixing up certificate with digital identity. This is a common point of confusion. I talk about this, in a different context, in TN3161 Inside Code Signing: Certificates. I also touch on it, in the context of TLS, in TLS for App Developers.

To configure your server you’ll need a digital identity. That means a certificate plus the private key associated with the public key in that certificate. On Apple platforms that’s represented by the SecIdentity type.

Right now you’re working with just a certificate, via the SecCertificate type. You’ll need that code for the client side, but for the server side you need a digital identity. You usually get that in a PKCS#12 file (.p12 or .pfx) and import it using SecPKCS12Import.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Using Network Framework + Bonjour + QUIC + TLS
 
 
Q