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 plan to only allow up to 6 connections (ie 7 devices all connected to each other) and the connections will be used for the following

  • send command messages (ie "start recording", "stop recording", "take photo")
  • fetch thumbnails of media on device and its URI to fetch
  • download media content (ie photos and videos) from Device A to Device B
  • streaming video (ie I will also want to stream video from Device B, C and D to Device A (ie Device A can get a preview of what Device B, C and D are seeing))

There could be a scenario where Device A is the "controller" and its connected to 3 other devices (B, C, D) and Device A requests a video from each other device - so it would be downloading 3 videos from 3 different devices.

Is TCP "enough" for my use cases and is it worth it to try and use QUIC for the performance gains.

Also, both the physical devices I am running the app on (iphone and ipad) are on iOS 18

If it's easier I can post my demo code in a public github repo.

Oh, I will also want to stream video from Device B, C and D to Device A (ie Device A can get a preview of what Device B, C and D are seeing)

Also, both the physical devices I am running the app on (iphone and ipad) are on iOS 18

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"

Thanks for the reply and information. I am early in the development of my app so things can change. I want to be careful to not get captivated by what QUIC can offer if I don't really need it.

A high level description of the app:

  • up to 7 devices can all discover each other and connect to each other
  • command messages are sent between the devices (ie Device A sends out a "take photo" command and it's sent to all the connected devices)
  • media content is downloaded between devices (ie Device A download the photo that was taken by each peer)
  • stream what the camera is see so the main / controller device can get a preview of what the other devices are seeing

As I'm describing what the app does I'm thinking when a device is discovered two connections are opened up - a TCP connection used for commands and downloading and a UDP connection used for camera preview.

When it comes to QUIC and your advice of hardcoding a digital identify - would that be ill-advised in production? Would a user be able to inspect the app payload and extract the hard coded digital identity and be a bad actor with it? No sensitive PPI data will be sent over the QUIC connection.

I appreciate your input and advice.

would doing this be wrong in a production app?

sec_protocol_options_set_verify_block(options.securityProtocolOptions, { (sec_protocol_metadata, sec_trust, sec_protocol_verify_complete) in
    sec_protocol_verify_complete(true)
}, queue)

I found this from this app https://github.com/paxsonsa/quic-swift-demo/blob/main/Sources/main.swift - I think he posted on this forum before.

Hard coding a credential in product code is always a mistake IMO. It’s only slightly better than disabling TLS entirely.

Would a user be able to inspect the app payload and extract the hard coded digital identity and be a bad actor with it?

If you put something in your app, you should assume that the user will be able to access it. You can do stuff to try to prevent that, but that means you’re essentially building a DRM scheme, and those are never 100% effective.

However, it sounds like you’re just testing out QUIC right now, and hard coding a digital identity for the purposes of your test is fine. You don’t want to spend time building the digital identity generation code and then find that QUIC doesn’t work for you. And if you find that QUIC does work for you, it’s certainly feasible to do the digital identity stuff correctly (it’s a pain, but definitely doable).

would doing this be wrong in a production app?

That is effectively disabling TLS server trust evaluation, which is only slightly better than disabling TLS entirely.

If it comes time to do this correctly, you’ll want to find a way to:

  • Generate the digital identity on the server side.

  • Distribute that digital identity to all the clients.

How you do that depends on the details of your product. I outline a number of potential options in TLS For Accessory Developers, but you might be able to do better depending on the specifics of your app.

IMO it’s best to defer this discussion until you decide that QUIC is for you or not.


Of course there’s a meta question here: If you don’t use QUIC, how do you implement security with the protocols you do use? For TCP you can add TLS-PSK and that gives you more flexibility in how you deal with trust. For UDP, there’s DTLS but I’ve never looked as to whether that supports PSK or not.

Share and Enjoy

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

Using Network Framework + Bonjour + QUIC + TLS
 
 
Q