debugging issues with Shared Private Key Bonjour connections

Like the post at https://forums.developer.apple.com/forums/thread/118035, I'm hitting an issue where I'm receiving:

boringssl_session_set_peer_verification_state_from_session(448) [C1.1.1.1:2][0x12b667210] Unable to extract cached certificates from the SSL_SESSION object

In my app logs. I tried to pin the SSL version to TLS 1.2 per Quinn's advice in that post, and then started digging further enabling CFNETWORK_DIAGNOSTICS=3 to see what was exposed on the Console.log (since it didn't show up in the Xcode console)

The related log lines:

0	debug	boringssl	15:43:04.978874-0700	MeetingNotes	boringssl_context_log_message(2206) [C5:2][0x11080a760] Reading SSL3_RT_HANDSHAKE 16 bytes
0	debug	boringssl	15:43:04.979007-0700	MeetingNotes	boringssl_context_log_message(2206) [C5:2][0x11080a760] Writing SSL3_RT_CHANGE_CIPHER_SPEC 1 bytes
0	debug	boringssl	15:43:04.979141-0700	MeetingNotes	boringssl_context_log_message(2206) [C5:2][0x11080a760] Writing SSL3_RT_HANDSHAKE 16 bytes
0	debug	boringssl	15:43:04.979260-0700	MeetingNotes	nw_protocol_boringssl_write_bytes(87) [C5:2][0x11080a760] write request: 51
0	debug	boringssl	15:43:04.979387-0700	MeetingNotes	nw_protocol_boringssl_write_bytes(158) [C5:2][0x11080a760] total bytes written: 51
921460	debug	boringssl	15:43:09.937961-0700	MeetingNotes	boringssl_context_log_message(2206) [C5:2][0x11080a760] Writing SSL3_RT_ALERT 2 bytes
0	error	boringssl	15:43:04.979630-0700	MeetingNotes	boringssl_session_set_peer_verification_state_from_session(448) [C5:2][0x11080a760] Unable to extract cached certificates from the SSL_SESSION object

Have a number of references to SSL3_RT in the messages, and I was curious if that indicated that I was using TLS1.3, which apparently doesn't support private shared keys.

The constraints that I used riffs on the sample code from the tic-tac-toe example project:

  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: "MeetingNotes".data(using: .utf8)!,
            using: authenticationKey
        )

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

        // Private Shared Key (https://datatracker.ietf.org/doc/html/rfc4279) is *not* supported in
        // TLS 1.3 [https://tools.ietf.org/html/rfc8446], so this pins the TLS options to use version 1.2:
        // @constant tls_protocol_version_TLSv12 TLS 1.2 [https://tools.ietf.org/html/rfc5246]
        sec_protocol_options_set_max_tls_protocol_version(tlsOptions.securityProtocolOptions, .TLSv12)
        sec_protocol_options_set_min_tls_protocol_version(tlsOptions.securityProtocolOptions, .TLSv12)

        sec_protocol_options_add_pre_shared_key(
            tlsOptions.securityProtocolOptions,
            authenticationDispatchData as __DispatchData,
            stringToDispatchData("MeetingNotes")! as __DispatchData
        )

        /* RFC 5487 - PSK with SHA-256/384 and AES GCM */
        // Forcing non-standard cipher suite value to UInt16 because for
        // whatever reason, it can get returned as UInt32 - such as in
        // GitHub actions CI.
        let ciphersuiteValue = UInt16(TLS_PSK_WITH_AES_128_GCM_SHA256)
        sec_protocol_options_append_tls_ciphersuite(
            tlsOptions.securityProtocolOptions,
            tls_ciphersuite_t(rawValue: ciphersuiteValue)!
        )
        return tlsOptions
    }

Is there something I'm missing in setting up the proper constraints to request TLS version 1.2 with a private shared key to be used? And beyond that, any suggestions for debugging or narrowing down what might be failing?

Replies

If you build and run the TicTacToe sample in your environment, does it work?

Share and Enjoy

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

Thanks Quinn,

Yeah, I killed the watchOS companion bits and ran it without issue. I'll go through with a fine-toothed comb and see if I can spot the (any) difference(s) in how it's set up vs anything I might have incorrectly changed.

The logs from the app also show that same error - so I'm begininng to think that the message Unable to extract cached certificates from the SSL_SESSION object a red herring:

boringssl_session_set_peer_verification_state_from_session(448) [C1.1.1.1:2][0x106208510] Unable to extract cached certificates from the SSL_SESSION object
[C1 connected joe._tictactoe._tcp.local. tcp, tls, attribution: developer, path satisfied (Path is satisfied), viable, interface: en8] established

Yeah, I killed the watchOS companion bits

That’s always my first step (-:

and ran it without issue.

Cool. That gels with my experience as well.

I'll go through with a fine-toothed comb and see if I can spot the (any) difference(s)

OK. Let us know how you get along.

Share and Enjoy

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

Your question about the sample code kicked me in the right direction. The error message is a complete red-herring. I was also doing some fiddling to make my peer to peer connection instance async/await, and in the listener setup, I did something that didn't fully enable the state update handler, so I never heard the message of it coming live, and I'd based a bunch of work off that. I re-worked the code back to using a closure for capturing and reacting to that state, and that sorted the issue entirely.

I also learned that it's apparently acceptable to queue a single call to receive() even before the state is ready, that callback just won't be called until the state is ready. I wasn't 100% sure of the constraints expected by Network.NWConnection between registering sends() and or receive() callbacks and the state reported by the framework. After I backed off the idea of "wait until it's ready before doing ANYTHING" with async calls, the whole thing worked a lot more smoothly.

(I had been setting up an AsyncStream queue with a continuation to handle the state update callbacks, but it ended up before a lot more ceremony without a whole lot of value - I do still want to wrangle something in there that's async aware at a higher level to allow initiated connections to retry on failure, but I'm in a way better space for that now.)

Glad to hear you got it working.

I also learned that it's apparently acceptable to queue a single call to receive() even before the state is ready

Yep. Likewise for sending.

Share and Enjoy

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