Transparent Apple Watch Auth (with CloudFlare Client Certificates)
Introduction
I recently set up a Cloudflare Tunnel and wanted to find a way to protect that resource. I primary use an Apple Watch app to communicate with this service.
Some form of auth without needing to do some complicated or cumbersome process on the Apple watch was my primary goal. In talking with a friend, I discovered a very easy way to set up client certificates with Cloudflare as the means of authentication. Furthermore, the certificate can be embedded directly in the watch app for maximum convenience.
Cloudflare Setup
As a prerequsite, ensure that your service’s domain is being proxied through Cloudflare.
Next, generate a client certificate on the Cloudflare dashboard (SSL > Client Certificates) . From here, we can download the certificate and the secret. Make sure to enable mTLS
if needed, as outlined in these docs.
After generating a certificate, navigate to the Web Application Firewall (WAF) portal under Security > WAF
. You’ll want to add a new rule that blocks when not cf.tls_client_auth.cert_verified
for your subdomain.
(http.host in {"subdomain.example.com"} and not cf.tls_client_auth.cert_verified)
If all is setup correctly, navigating to your service will be blocked. To access the service, you’ll now need to offer the client certificate to Cloudflare.
App/Device Setup
Embedding into an Apple Watch app
Using the downloaded certificate and key from Cloudflare, I generated a combined p12
file secured by a password (keep this for later!) using the openssl
command below.
openssl pkcs12 -export -out cert.p12 -in cloudflare.pem -inkey cloudflare.key
Then, taking inspiration from Cloudflare’s ‘Client certificates Configure your Mobile app or IoT Device’ and a great Swift example project by @MarcoEidinger, I embedded the certificate into this Apple Watch app. The interesting bits are:
Reading in the certificate name (bundled into the application) and the password. These are exposed as an extension to Bundle
(src).
extension Bundle {
var userCertificate: UserCertificate? {
guard let filePath = Bundle.main.path(forResource: VARIABLE_INFO_FILE_PATH, ofType: "plist"),
let plist = NSDictionary(contentsOfFile: filePath),
let certName = plist.object(forKey: CERTIFICATE_NAME) as? String,
let certPassword = plist.object(forKey: CERTIFICATE_PASSWORD) as? String
else {
fatalError("Missing client certificate password in '\(VARIABLE_INFO_FILE_PATH)'")
}
guard let path = Bundle.main.path(forResource: certName, ofType: "p12"),
let p12Data = try? Data(contentsOf: URL(fileURLWithPath: path))
else {
fatalError("Missing client certificate.")
}
return (p12Data, certPassword)
}
}
When sending an HTTPS request, a client certificate delegate is conditionally added depending on the request being sent (src).
...
let session = URLSession(configuration: .default, delegate: certMode == CertMode.ClientCert ? URLSessionClientCertificateHandling() : nil, delegateQueue: nil)
let task = session.dataTask(with: request) {(data, response, error) in
if let httpResponse = response as? HTTPURLResponse {
print(httpResponse.statusCode)
if queryType == QueryType.StatusCode {
res.wrappedValue = String(httpResponse.statusCode)
return
}
if httpResponse.statusCode > 299 {
res.wrappedValue = "ERR (\(httpResponse.statusCode))"
return
}
let stringResponse = String(data: data!, encoding: String.Encoding.utf8)
res.wrappedValue = stringResponse ?? "ERR"
} else {
res.wrappedValue = "ERR"
}
}
task.resume()
}
Browser setup
Manually installing the certifcate is as easy as importing into an application’s certificate store. After importing in Firefox and navigating to a WAF-protected webpage, you’ll be asked once if you’d like to identify yourself with that certificate. If you do so, the Cloudflare WAF will grant you access to the resource.
Have a comment? Let me know
This post helpful? Buy me a coffee!