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
}
}
}
}
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:
-
QA1948 HTTPS and Test Servers
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"