Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

TLS communication error between iPhone and iPad

We are implementing a connection between iPad and iPhone devices using LocalPushConnectivity, and have introduced SimplePushProvider into the project. We will have it switch between roles of Server and Client within a single project. ※ iPad will be Server and the iPhone will be Client.

Communication between Server and Client is via TLS, with Server reading p12 file and Client setting public key. Currently, a TLS error code of "-9836" (invalid protocol version) is occurring when communicating from Client's SimplePushProvider to Server. I believe that Client is sending TLS1.3, and Server is set to accept TLS1.2 to 1.3. Therefore, I believe that the actual error is not due to TLS protocol version, but is an error that is related to security policy or TLS communication setting.

Example:

  • P12 file does not meet some requirement
  • NWProtocolTLS.Options setting is insufficient
  • etc...

I'm not sure what the problem is, so please help. For reference, I will attach you implementation of TLS communication settings. P12 file is self-signed and was created by exporting it from Keychain Access.

Test environment:

  • iPad (OS: 16.6)
  • iPhone (OS: 18.3.2)

ConnectionOptions: TLS communication settings

public enum ConnectionOptions {
    public enum TCP {
        public static var options: NWProtocolTCP.Options {
            let options = NWProtocolTCP.Options()
            options.noDelay = true
            options.enableFastOpen
            return options
        }
    }
    
    public enum TLS {
        public enum Error: Swift.Error {
            case invalidP12
            case unableToExtractIdentity
            case unknown
        }
        
        public class Server {
            public let p12: URL
            public let passphrase: String
            
            public init(p12 url: URL, passphrase: String) {
                self.p12 = url
                self.passphrase = passphrase
            }
            
            public var options: NWProtocolTLS.Options? {
                guard let data = try? Data(contentsOf: p12) else {
                    return nil
                }
                
                let pkcs12Options = [kSecImportExportPassphrase: passphrase]
                var importItems: CFArray?
                let status = SecPKCS12Import(data as CFData, pkcs12Options as CFDictionary, &importItems)

                guard status == errSecSuccess,
                    let items = importItems as? [[String: Any]],
                    let importItemIdentity = items.first?[kSecImportItemIdentity as String],
                    let identity = sec_identity_create(importItemIdentity as! SecIdentity)
                else {
                    return nil
                }

                let options = NWProtocolTLS.Options()
                sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, .TLSv12)
                sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, .TLSv13)
                sec_protocol_options_set_local_identity(options.securityProtocolOptions, identity)
                sec_protocol_options_append_tls_ciphersuite(options.securityProtocolOptions, tls_ciphersuite_t.RSA_WITH_AES_128_GCM_SHA256)

                return options
            }
        }
        
        public class Client {
            public let publicKeyHash: String
            private let dispatchQueue = DispatchQueue(label: "ConnectionParameters.TLS.Client.dispatchQueue")
            
            public init(publicKeyHash: String) {
                self.publicKeyHash = publicKeyHash
            }
            
            // Attempt to verify the pinned certificate.
            public var options: NWProtocolTLS.Options {
                let options = NWProtocolTLS.Options()
                sec_protocol_options_set_min_tls_protocol_version(options.securityProtocolOptions, .TLSv12)
                sec_protocol_options_set_max_tls_protocol_version(options.securityProtocolOptions, .TLSv13)

                sec_protocol_options_set_verify_block(
                    options.securityProtocolOptions,
                    verifyClosure,
                    dispatchQueue
                )

                return options
            }

            private func verifyClosure(
                secProtocolMetadata: sec_protocol_metadata_t,
                secTrust: sec_trust_t,
                secProtocolVerifyComplete: @escaping sec_protocol_verify_complete_t
            ) {
                let trust = sec_trust_copy_ref(secTrust).takeRetainedValue()

                guard let serverPublicKeyData = publicKey(from: trust) else {
                    secProtocolVerifyComplete(false)
                    return
                }

                let keyHash = cryptoKitSHA256(data: serverPublicKeyData)

                guard keyHash == publicKeyHash else {
                    // Presented certificate doesn't match.
                    secProtocolVerifyComplete(false)
                    return
                }

                // Presented certificate matches the pinned cert.
                secProtocolVerifyComplete(true)
            }


            private func cryptoKitSHA256(data: Data) -> String {
                let rsa2048Asn1Header: [UInt8] = [
                   0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
                   0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
                ]
                let data = Data(rsa2048Asn1Header) + data
                let hash = SHA256.hash(data: data)
                return Data(hash).base64EncodedString()
            }
            
            private func publicKey(from trust: SecTrust) -> Data? {
                guard let certificateChain = SecTrustCopyCertificateChain(trust) as? [SecCertificate],
                    let serverCertificate = certificateChain.first else {
                    return nil
                }

                let publicKey = SecCertificateCopyKey(serverCertificate)
                return SecKeyCopyExternalRepresentation(publicKey!, nil)! as Data
            }
        }
    }
}
Answered by DTS Engineer in 841306022

There’s a lot of crypto stuff in here that you don’t need, at least for your initial bring up. I recommend that you replace verifyClosure(…) with temporary code that just completely the request with true. That disables all TLS server trust evaluation completely. You wouldn’t want to ship that way, but it’s great when you’re just trying to get things to work.

The above means that the only Crypto Gunk™ you have to worry about is the server digital identity. You wrote:

P12 file is self-signed and was created by exporting it from Keychain Access.

Self-signed is fine in this scenario, although my general advice is that you set up a CA and have it issue a certificate for your server. For info on how to do that, see:

I also have TLS for App Developers and TLS For Accessory Developers, with the latter likely to be more interesting in your setup.

Finally, make sure your the server’s certificate has:

  • A Key Usage extension that includes Digital Signature

  • An Extended Key Usage extension that includes Server Authentication


It’s always good to split these problems apart. Rather than try to debug your client and server simultaneously, first debug the one and then the other.

To debug your client, connect to a known server whose identity has a self-signed certificate. I typically use badssl.com for this sort of thing. For example, this program:

import Foundation
import Network

func main() {
    print("will start")
    let tls = NWProtocolTLS.Options()
    sec_protocol_options_set_verify_block(tls.securityProtocolOptions, { _, _, completionHandler in
        print("did disable security")
        completionHandler(true)
    }, .main)
    let parameters = NWParameters(tls: tls)
    let connection = NWConnection(host: "self-signed.badssl.com", port: 443, using: parameters)
    connection.stateUpdateHandler = { newState in
        print("did change state, new: \(newState)")
    }
    connection.start(queue: .main)
    print("did start")
    withExtendedLifetime(connection) {
        dispatchMain()
    }
}

main()

prints:

will start
did start
did change state, new: preparing
did disable security
did change state, new: ready

You can do the same thing with your server, using the openssl command-line tool to connect to it:

% openssl s_client -connect fluffycom:443

Note The s_client subcommand disables TLS server trust evaluation by default.

Once you have this basic setup working for both the client and server, you can then combine them and see what happens.

Share and Enjoy

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

Considering the effect of the cache, I initialized the iPad and iPhone and checked the operation, and the following error log was confirmed.

nw_protocol_boringssl_error(1973) [C1:58][0x105964400] Lower protocol stack error pre TLS handshake. [60: <private>]

Next, I updated OS and checked the operation, and the following error log was confirmed. (new OS version: 18.5)

nw_protocol_boringssl_handshake_negotiate_proceed(787) [C2:26][0x101590a00] handshake failed at state 12288: not completed

Although the investigation is still in progress, it seems that there is a problem with the options.

I look forward to your help.

There’s a lot of crypto stuff in here that you don’t need, at least for your initial bring up. I recommend that you replace verifyClosure(…) with temporary code that just completely the request with true. That disables all TLS server trust evaluation completely. You wouldn’t want to ship that way, but it’s great when you’re just trying to get things to work.

The above means that the only Crypto Gunk™ you have to worry about is the server digital identity. You wrote:

P12 file is self-signed and was created by exporting it from Keychain Access.

Self-signed is fine in this scenario, although my general advice is that you set up a CA and have it issue a certificate for your server. For info on how to do that, see:

I also have TLS for App Developers and TLS For Accessory Developers, with the latter likely to be more interesting in your setup.

Finally, make sure your the server’s certificate has:

  • A Key Usage extension that includes Digital Signature

  • An Extended Key Usage extension that includes Server Authentication


It’s always good to split these problems apart. Rather than try to debug your client and server simultaneously, first debug the one and then the other.

To debug your client, connect to a known server whose identity has a self-signed certificate. I typically use badssl.com for this sort of thing. For example, this program:

import Foundation
import Network

func main() {
    print("will start")
    let tls = NWProtocolTLS.Options()
    sec_protocol_options_set_verify_block(tls.securityProtocolOptions, { _, _, completionHandler in
        print("did disable security")
        completionHandler(true)
    }, .main)
    let parameters = NWParameters(tls: tls)
    let connection = NWConnection(host: "self-signed.badssl.com", port: 443, using: parameters)
    connection.stateUpdateHandler = { newState in
        print("did change state, new: \(newState)")
    }
    connection.start(queue: .main)
    print("did start")
    withExtendedLifetime(connection) {
        dispatchMain()
    }
}

main()

prints:

will start
did start
did change state, new: preparing
did disable security
did change state, new: ready

You can do the same thing with your server, using the openssl command-line tool to connect to it:

% openssl s_client -connect fluffycom:443

Note The s_client subcommand disables TLS server trust evaluation by default.

Once you have this basic setup working for both the client and server, you can then combine them and see what happens.

Share and Enjoy

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

Thanks for your advice! but, new two problems occurred.

  1. When exchanging values ​​using the NWconnection send and receive methods, the first 4 bytes are missing.
  • I'm not sure how to fundamentally solve the problem.
  • Adding 4 extra bytes to the beginning is temporarily postponing the problem...
  1. When the Server responds to the Client after receiving a value with receive, it is not returning the value.
  • I implemented it thinking that it would be possible to return the value as is by sending it within receive. However, the response data is nil, so I think the response method is incorrect.
public class NetworkSession {
    ...

    internal func send(data: Data) {
        connection?.send(content: data, completion: .contentProcessed({ [weak self] error in
            if let error {
                self?.logger.log("Error sending - \(error)")
            }
        }))
    }

    internal func receive() {}
}
public class RequestResponseSession: NetworkSession {
    public enum Error: Swift.Error {
        case requestTimeout
        case connectionReset
        case encoding(Swift.Error)
        case unableToDecode
        case unknown
    }

    private struct PendingRequest {
        var id: UInt32
        var completion: Future<Bool, Swift.Error>.Promise
    }

    private enum Command: String, Codable {
        case request
        case acknowledge
    }

    private struct RequestData: Codable {
        var command: Command
        var requestIdentifier: UInt32
        var payload: KeyCodedPayload<UInt8>?
    }

    public private(set) lazy var messagePublisher = messagesSubject.eraseToAnyPublisher()
    public let timeout: DispatchTimeInterval = .seconds(10)
    public var disconnectOnFailure: Bool = true
    private let dispatchQueue = DispatchQueue(label: "RequestResponseSession.dispatchQueue")
    private let messagesSubject = PassthroughSubject<Codable, Never>()
    private var pendingRequests = [UInt32: PendingRequest]()
    private let keyCoder = KeyCoder()
    private let encoder = JSONEncoder()
    private let decoder = JSONDecoder()
    ...

    private func request<Message: Codable>(id: UInt32, message: Message, completion: @escaping Future<Bool, Swift.Error>.Promise) {
        dispatchQueue.async { [weak self] in
            guard let self else { return }

            guard self.state == .connected else {
                completion(.failure(NetworkSession.Error.notConnected))
                return
            }

            self.pendingRequests[id] = PendingRequest(id: id, completion: completion)

            do {
                let payload = try self.keyCoder.encode(value: message)
                let requestData = RequestData(command: .request, requestIdentifier: id, payload: payload)
                var encodedRequestData = try self.encoder.encode(requestData)
                /// ↓ Problem 1, temporary solution
                let addData = try self.encoder.encode("aa")
                encodedRequestData = addData + encodedRequestData

                self.send(data: encodedRequestData)
            } catch {
                completion(.failure(Error.encoding(error)))
            }
        }
    }

    override func receive() {
        guard let connection else { return }

        connection.receiveMessage { [weak self] data, context, isComplete, error in
            guard let self else { return }

            if let error { return }

            if let data {
                self.dispatchQueue.async {
                    if let receiveData = self.decode(data: data), receiveData.command != .acknowledge {
                        /// ↓ Problem2, Server is recieve data and send to Client
                        do {
                            var data = try self.encoder.encode(RequestData(command: .acknowledge, requestIdentifier: receiveData.requestIdentifier, payload: nil))
                            self.send(data: data)
                        } catch {
                            print("Error encoding acknowledgment - \(error)")
                        }
                    }
                }
            }

            if let context, context.isFinal {
                self.disconnect()
            } else {
                self.receive()
            }
        }
    }

    private func decode(data: Data) -> RequestData? {
        do {
            let requestData = try decoder.decode(RequestData.self, from: data)

            switch requestData.command {
                case .request:
                    guard let payload = requestData.payload else { break }
                    let message = try keyCoder.decode(for: payload.codingKey, data: payload.data)
                    messagesSubject.send(message)
                case .acknowledge:
                    dispatchQueue.async { [weak self] in
                        guard let request = self?.pendingRequests[requestData.requestIdentifier] else { return }
                        request.completion(.success(true))
                    }
            }
            return requestData
        } catch {
            print("Error decoding - \(error)")
        }
        return nil
    }
}
struct KeyCoder: KeyCodable {
    var keymap: [UInt8: Codable.Type] {
        [
            0: Invite.self,
            1: User.self,
            2: CallAction.self,
            3: TextMessage.self,
            4: Heartbeat.self,
            5: Directory.self
        ]
    }
}
TLS communication error between iPhone and iPad
 
 
Q