Keychain client https certificates and URLSession with Swift 5
We issue client identity certificates to our Mac fleet with MDM; we also want to use those identifies with our own client-side tools and backends.
I didn’t find a good end-to-end example, just snippets of partial answers on Stack Overflow or the Apple Developer forums, so I hope this code is useful for someone else.
KeychainCertificateDelegate
is a URLSession
delegate that finds the correct identity by handling urlSession:didReceive:completionHandler
:
KeychainCertificateDelegate
1class KeychainCertificateDelegate: NSObject, URLSessionDelegate {
2 func urlSession(_: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
3 if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodClientCertificate {
4 // Get the DNs the server will accept
5 guard let expectedDNs = challenge.protectionSpace.distinguishedNames else {
6 completionHandler(.cancelAuthenticationChallenge, nil)
7 return
8 }
9
10 // Ask Keychain to search, based on the DNs the server presented on its cert
11 var identityRefs: CFTypeRef? = nil
12 let err = SecItemCopyMatching([
13 kSecClass: kSecClassIdentity,
14 kSecMatchLimit: kSecMatchLimitAll,
15 kSecMatchIssuers: expectedDNs,
16 kSecReturnRef: true,
17 ] as NSDictionary, &identityRefs)
18
19 // can't get keychain certs, get out
20 if err != errSecSuccess {
21 completionHandler(.cancelAuthenticationChallenge, nil)
22 return
23 }
24
25 // just use the first identity
26 guard let identities = identityRefs as? [SecIdentity],
27 let identity = identities.first
28 else {
29 completionHandler(.cancelAuthenticationChallenge, nil)
30 return
31 }
32
33 // DEBUGGING -- obtain the cert details to print it out
34 // you can just delete this entire block
35 var certificateRef: SecCertificate?
36 let status = SecIdentityCopyCertificate(identity, &certificateRef)
37 guard status == errSecSuccess, let certificateRef
38 else {
39 completionHandler(.cancelAuthenticationChallenge, nil)
40 return
41 }
42 debugPrint(certificateRef)
43 // END DEBUG
44
45 let credential = URLCredential(identity: identity, certificates: nil, persistence: .forSession)
46 completionHandler(.useCredential, credential)
47 return
48
49 } else {
50 // Handle any other kinds of authentication challenge
51 completionHandler(.performDefaultHandling, nil)
52 }
53 }
54}
KeychainCertificateDelegate obtains the list of valid issuers from the server, using the challenge.protectedSpace.distinguishedNames
attribute. Then, we ask Keychain for a matching SecIdentity
using SecItemCopyMatching()
. Finally, we wrap that identity in a URLCredential
and call the completion handler.
Using this delegate is relatively simple from a command line tool - just create a new instance of KeychainCertificateDelegate
and set it as your delegate (lines 6-8) when creating a new URLSession
, and then call that new session for your requests.
1main
2struct HelloWorld {
3 static func main() async throws {
4 let url = URL(string: "https://some-server.goes.here")!
5
6 let config = URLSessionConfiguration.default
7 let keyDelegate = KeychainCertificateDelegate()
8 let session = URLSession(configuration: config, delegate: keyDelegate, delegateQueue: nil)
9
10 let (data, response) = try await session.data(from: url)
11
12 guard let httpResponse = response as? HTTPURLResponse,
13 (200 ... 299).contains(httpResponse.statusCode)
14 else {
15 throw URLError(.badServerResponse)
16 }
17
18 print(String(data: data, encoding: .utf8)!)
19 }
20}
Hope this helps someone else!