CryptoTokenKit (“CTK”) provides a unique system for certificate-based authentication to macOS and iOS without giving an app access to the private key material. This makes CrytoTokenKit ideal for smart cards, apps, external systems, and other credential providers to enable certificate-based authentication with compromising the private keys.
Terms
Public Key/Private Key: RSA or ECC asymmetric keys for public key cryptography. Anything encrypted with one key can only be decrypted by the other key. Used for encrypting, signing and other public key cryptography operations.
Certificate: An X.509 digital certificate that typically contains a public key and is signed by a signing authority. The certificate provides information on how the keys should be used (for example, SSL encryption or email signing).
Identity: The certificate, public key (usually in the certificate) and private key.
Tokens: Tokens are physical devices built into the system, located on attached hardware (like a smart card), or accessible through a network connection. Tokens store cryptographic objects like keys and certificates. A token can be thought of as a keystore where you do not have access to the underlying private keys but can ask the token to perform operations such as signing and encrypting.
Client App: The app that is request a signing or encrypting operation.
Container App: The app that contains a CryptoTokenKit extension. The container app can register configurations with the container app but does not have direct communication with the CryptoTokenKit extension.
Key Store: Storage for key material, e.g. secure enclave, smart card, p12, or network device.
Architecture
One of the key features of CryptoTokenKit is to provide the ability for an app to use certificates provided by CTK without any code changes to an app that needs to do certificate-based authentication. Apps that need to do certificate-based authentication typically use the keychain API available in iOS and macOS. This gives the app the ability to use credentials stored in the encrypted keychain store in macOS and iOS using a standard interface. Apple has extended the keychain APIs to expose certificates and signing/encrypting operations to apps using the keychain API without exposing the key material.
CryptoTokenKit Container App
The container app is what the end user installs on the system and contains that CryptoTokenKit extension. The container app can also provides certificates to be used when requesting signing and encrypting operations.
CryptoTokenKit Extensions
An app that wants to be a provider of token operations (such as signing or encrypting) provides a CryptoTokenKit extension inside the app bundle. This extension is then detected by the OS on app installation. When an app requests token-related operations via the keychain API, the OS will ask for the extension for the tokens it provides and what operations are available. A reference will be provided by the OS to the client app to the token; the private key is not exposed to the client app.
CTK Extension Setup
When an installed app that contains a CryptoTokenKit extension is first launched, the extension is registered with the OS via the CryptoTokenKit daemon (ctkd). ctkd communicates with registered ctk extension over XPC calls. This does not make any identities available to the OS or calling apps. It just registers the extension for later use.
Request Available Identities
When a calling app needs to do a cryptographic operation using the keychain API, the first step is to request the available identities. This request is done using the SecItemCopyMatching API:
let query = [kSecClass: kSecClassIdentity,
kSecMatchLimit: kSecMatchLimitAll,
kSecReturnRef: kCFBooleanTrue as Any,
kSecReturnAttributes: kCFBooleanFalse as Any] as [CFString : Any]
let sanityCheck = SecItemCopyMatching(query as CFDictionary,&item)
This will return all a reference to all available identities available to the app. The identities may be in local keychain or by a CTK extension. If there is CTK extension available, the Client App must opt in to having CTK-provided identity references included in the return results by adding a keychain group of com.apple.token. This prevents unexpected behavior if an app was not expecting identity references in the search results.
Additionally, the user of the client app will be prompted to allow the identities to be included:
When the request for available identities is made, the Keychain API will return any available identities from the local keychain store as well as any identities available from ctkd. ctkd requests the available certificates from any registered CryptoTokenKit extensions. All results are combined and returns to the calling app. Note that the private keys are not returned; only a reference is returned to be used by future cryptographic operations.
Perform Signing Operation
Once the client app has a reference to an identity, it can use the reference in calls for cryptographic operations such as signing and encrypting:
// returns a private key ref, not the private key
var privateKey:SecKey?
SecIdentityCopyPrivateKey(identityRef, &privateKeyRef)
if let dataToSign = dataToSign, let privateKey = privateKey {
let signature = SecKeyCreateSignature(privateKey, .rsaSignatureMessagePKCS1v15SHA512, dataToSign as CFData, nil)
}
When a keychain API such as SecKeyCreateSignature is called, the identity reference and data (or hash) to be signed is sent via the keychain API. Since the private key reference was provided by a CryptoTokenKit extension, then the operation is sent to ctkd and then to the appropriate CryptoTokenKit extension. The extension would then sign the data and return the result:
func tokenSession(_ session: TKTokenSession, sign dataToSign: Data, keyObjectID: Any, algorithm: TKTokenKeyAlgorithm) throws -> Data {
var signature: Data?
//sign data and return signature
}
Note that the code above does not require access to the raw key material. It can perform the signing operation from the Secure Enclave, a network API, or a Smart Card.
Once the extension returns the signature, the signature is returned to the calling app, and the operation is complete.
Smart Card Tokens
CryptoTokeKit has 2 different types of objects: TKToken and TKSmartCardToken. TKSmartCardToken is a subclass of TKToken. This is a way of saying that TKSmartCardToken has all the features of TKToken but adds some additional functionality. Specifically, TKSmartCardToken allows for prompting the user for the PIN. In the example above for signing data, if a PIN was required for calling an external token, the extension throws a authenticationNeeded exception in the signing function:
func tokenSession(_ session: TKTokenSession, sign dataToSign: Data, keyObjectID: Any, algorithm: TKTokenKeyAlgorithm) throws -> Data {
var signature: Data?
//throw to get PIN
throw TKError(TKError.authenticationNeeded)
}
The effect of the TKError.authenticationNeeded is that the user will be prompted to enter a PIN. Neither the client app, the container app, or the extension provide the UI for the PIN. This is provided by the OS. Once the PIN has been entered, the finish() function is called with the PIN in the appropriate CTK extension and the extension validates the PIN and sets an instance variable to the current PIN:
class TokenAuthOperation: TKTokenSmartCardPINAuthOperation {
override func finish() throws {
guard let pin = self.pin, pin.count<=9 else {
throw NSError(domain: TKErrorDomain, code: TKError.Code.authenticationFailed.rawValue, userInfo: nil)
}
//validate PIN
}
}
Once the throw is handled, the signing function would be called again, and since the device was unlocked/verified, the signing operation can complete.