Hi there, we're looking to build a Bonjour service for our users so that they can share data between devices. Things are mostly going ok, but we would like to make sure the connection is secure.
Being good developers we took a look at the TicTacToe example from WWDC. This looks great! We'd love to secure our comms with the latest TLS via a Pre Shared Key (PSK) e.g. a Passcode in our case.
In the normal happy path, things work well, we can send and receive messages and all is well. However, when we enter the wrong passcode we don't receive any notification back on the client side. The server can detect the incorrect passcode, but the client is left hanging around.
The issue only appears to affect a Bonjour service or mode (not quite sure of the terminology here). If we explicitly specify a host (e.g. "localhost" and port (e.g. 12345) for connection/listening then we get the expected callbacks on both client/server that the PIN was incorrect.
However if we just setup a service and try to connect to it (in our case we use NWBrowser in our App, but below we create an endpoint manually), everything works fine for a good passcode, but for a bad passcode we don't receive any callback and have no way to know the passcode was no good and inform the user.
So, we'd love to be able to detect that incorrect passcode on the client side. What are we doing wrong.
Sample code below (mostly shamelessly ripped from some of @eskimos sample code in another issue) demonstrates the issue, change the ServiceMode / Passcodes inside main() to see the issue.
Hoping we can page Dr. @eskimo and Dr. @meaton - Could really do with your expertise here. Ta!
import CryptoKit
import Foundation
import Network
let ServerName = "My-Bonjour-Server"
let ServiceName = "_my_bonjour_service._tcp"
var listenerRef: NWListener?
var receiveConnectionRef: NWConnection?
var sendConnectionRef: NWConnection?
enum ServiceMode {
case explicitHostAndPort // This works all the time
case bonjourService // This doesn't work for an incorrect passcode
}
extension NWParameters { // Just ripped from the TicTacToe example
convenience init(passcode: String) {
self.init(tls: NWParameters.tlsOptions(passcode: passcode))
}
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: ServiceName.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(ServiceName)! as __DispatchData)
sec_protocol_options_append_tls_ciphersuite(tlsOptions.securityProtocolOptions,
tls_ciphersuite_t(rawValue: TLS_PSK_WITH_AES_128_GCM_SHA256)!)
return tlsOptions
}
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
}
}
func startListener(passcode: String, serviceMode: ServiceMode) {
let listener: NWListener
switch serviceMode {
case .explicitHostAndPort:
listener = try! NWListener(using: NWParameters(passcode: passcode), on: 12345)
case .bonjourService:
listener = try! NWListener(using: NWParameters(passcode: passcode))
listener.service = NWListener.Service(name: ServerName, type: ServiceName)
}
listenerRef = listener
listener.stateUpdateHandler = { state in
print("listener: state did change, new: \(state)")
}
listener.newConnectionHandler = { conn in
if let old = receiveConnectionRef {
print("listener: will cancel old connection")
old.cancel()
receiveConnectionRef = nil
}
receiveConnectionRef = conn
startReceive(on: conn)
conn.start(queue: .main)
}
listener.start(queue: .main)
}
func startReceive(on connection: NWConnection) {
connection.receive(minimumIncompleteLength: 1, maximumLength: 2048) { dataQ, _, _, errorQ in
if let data = dataQ, let str = String(data: data, encoding: .utf8) {
print("receiver: did receive: \"\(str)\"")
}
if let error = errorQ {
if case let .tls(oSStatus) = error, oSStatus == errSSLBadRecordMac {
print("receiver has detected an Incorrect PIN")
} else {
print("receiver: did fail, error: \(error)")
}
return
}
}
}
func startSender(passcode: String, serviceMode: ServiceMode) {
let connection: NWConnection
switch serviceMode {
case .explicitHostAndPort:
connection = NWConnection(host: "localhost", port: 12345, using: NWParameters(passcode: passcode))
case .bonjourService:
let endpoint = NWEndpoint.service(name: ServerName, type: ServiceName, domain: "local.", interface: nil)
connection = NWConnection(to: endpoint, using: NWParameters(passcode: passcode))
}
sendConnectionRef = connection
connection.stateUpdateHandler = { state in
if case let .waiting(error) = state {
if case let .tls(os) = error, os == errSSLPeerBadRecordMac { // Incorrect PIN
print("Sender has detected an Incorrect PIN")
}
} else {
print("sender: state did change, new: \(state)")
}
}
connection.send(content: "It goes to 11".data(using: .utf8), completion: .idempotent)
connection.start(queue: .main)
}
func main() {
let serviceMode: ServiceMode = .explicitHostAndPort // Set this to Bonjour to see the issue
// Change one of the Passcodes below to see the incorrect pin message(s) or lack thereof
startListener(passcode: "1234", serviceMode: serviceMode)
// Wait for server to spin up...
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
startSender(passcode: "1234", serviceMode: serviceMode)
}
dispatchMain()
}
main()
exit(EXIT_SUCCESS)
Are you able to see the same?
I don’t have time to run that test right now.
Still, this sounds bugworthy to me, and I recommend that you file it as such. Please post your bug number, just for the record.
Fortunately you’ve also found a workaround, namely, to resolve the service and then connect using .hostPort(host:port:)
. Annoyingly, resolving the service isn’t as easy as it should be, but you can find some code for it here.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"