cryptotokenkit
CryptoTokenKit
Access security tokens and the cryptographic assets they store using the CryptoTokenKit framework. Covers token driver extensions, smart card communication, token sessions, keychain integration, and certificate-based authentication. Targets Swift 6.3.
Platform availability: CryptoTokenKit is primarily a macOS framework.
Smart card reader access (TKSmartCard, TKSmartCardSlotManager) requires
macOS. Token extension APIs (TKTokenDriver, TKToken, TKTokenSession)
are macOS-only. Client-side token watching (TKTokenWatcher) and keychain
queries filtered by kSecAttrTokenID are available on iOS 14+/macOS 11+.
NFC smart card slot sessions are available on iOS 16.4+.
Contents
- Architecture Overview
- Token Extensions
- Token Sessions
- Smart Card Communication
- Keychain Integration
- Certificate Authentication
- Token Watching
- Error Handling
- Common Mistakes
- Review Checklist
- References
Architecture Overview
CryptoTokenKit bridges hardware security tokens (smart cards, USB tokens) with macOS authentication and keychain services. The framework has two main usage modes:
Token driver extensions (macOS only) -- App extensions that make a hardware token's cryptographic items available to the system. The driver handles token lifecycle, session management, and cryptographic operations.
Client-side token access (macOS + iOS) -- Apps query the keychain for items backed by tokens. CryptoTokenKit automatically exposes token items as standard keychain entries when a token is present.
Key Types
| Type | Role | Platform |
|---|---|---|
TKTokenDriver |
Base class for token driver extensions | macOS |
TKToken |
Represents a hardware cryptographic token | macOS |
TKTokenSession |
Manages authentication state for a token | macOS |
TKSmartCardTokenDriver |
Entry point for smart card extensions | macOS |
TKSmartCard |
Low-level smart card communication | macOS |
TKSmartCardSlotManager |
Discovers and manages card reader slots | macOS |
TKTokenWatcher |
Observes token insertion and removal | macOS, iOS 14+ |
TKTokenKeychainKey |
A key stored on a token | macOS |
TKTokenKeychainCertificate |
A certificate stored on a token | macOS |
Token Extensions
A token driver is a macOS app extension that makes a hardware token's cryptographic capabilities available to the system. The host app exists only as a delivery mechanism for the extension.
A smart card token extension has three core classes:
- TokenDriver (subclass of
TKSmartCardTokenDriver) -- entry point - Token (subclass of
TKSmartCardToken) -- represents the token - TokenSession (subclass of
TKSmartCardTokenSession) -- handles operations
Driver Class
import CryptoTokenKit
final class TokenDriver: TKSmartCardTokenDriver, TKSmartCardTokenDriverDelegate {
func tokenDriver(
_ driver: TKSmartCardTokenDriver,
createTokenFor smartCard: TKSmartCard,
aid: Data?
) throws -> TKSmartCardToken {
return try Token(
smartCard: smartCard,
aid: aid,
instanceID: "com.example.token:\(smartCard.slot.name)",
tokenDriver: driver
)
}
}
Token Class
The token reads certificates and keys from hardware and populates its keychain contents:
final class Token: TKSmartCardToken, TKTokenDelegate {
init(
smartCard: TKSmartCard, aid: Data?,
instanceID: String, tokenDriver: TKSmartCardTokenDriver
) throws {
try super.init(
smartCard: smartCard, aid: aid,
instanceID: instanceID, tokenDriver: tokenDriver
)
self.delegate = self
let certData = try readCertificate(from: smartCard)
guard let cert = SecCertificateCreateWithData(nil, certData as CFData) else {
throw TKError(.corruptedData)
}
let certItem = TKTokenKeychainCertificate(certificate: cert, objectID: "cert-auth")
let keyItem = TKTokenKeychainKey(certificate: cert, objectID: "key-auth")
keyItem?.canSign = true
keyItem?.canDecrypt = false
keyItem?.isSuitableForLogin = true
self.keychainContents?.fill(with: [certItem!, keyItem!])
}
func createSession(_ token: TKToken) throws -> TKTokenSession {
TokenSession(token: token)
}
}
Info.plist and Registration
The extension's Info.plist must name the driver class:
NSExtension
NSExtensionAttributes
com.apple.ctk.driver-class = $(PRODUCT_MODULE_NAME).TokenDriver
NSExtensionPointIdentifier = com.apple.ctk-tokens
Register the extension once by launching the host app as _securityagent:
sudo -u _securityagent /Applications/TokenHost.app/Contents/MacOS/TokenHost
Token Sessions
TKTokenSession manages authentication state and performs cryptographic
operations via its delegate.
final class TokenSession: TKSmartCardTokenSession, TKTokenSessionDelegate {
func tokenSession(
_ session: TKTokenSession,
supports operation: TKTokenOperation,
keyObjectID: TKToken.ObjectID,
algorithm: TKTokenKeyAlgorithm
) -> Bool {
switch operation {
case .signData:
return algorithm.isAlgorithm(.rsaSignatureDigestPKCS1v15SHA256)
|| algorithm.isAlgorithm(.ecdsaSignatureDigestX962SHA256)
case .decryptData:
return algorithm.isAlgorithm(.rsaEncryptionOAEPSHA256)
case .performKeyExchange:
return algorithm.isAlgorithm(.ecdhKeyExchangeStandard)
default:
return false
}
}
func tokenSession(
_ session: TKTokenSession,
sign dataToSign: Data,
keyObjectID: TKToken.ObjectID,
algorithm: TKTokenKeyAlgorithm
) throws -> Data {
let smartCard = try getSmartCard()
return try smartCard.withSession {
try performCardSign(smartCard: smartCard, data: dataToSign, keyID: keyObjectID)
}
}
func tokenSession(
_ session: TKTokenSession,
decrypt ciphertext: Data,
keyObjectID: TKToken.ObjectID,
algorithm: TKTokenKeyAlgorithm
) throws -> Data {
let smartCard = try getSmartCard()
return try smartCard.withSession {
try performCardDecrypt(smartCard: smartCard, data: ciphertext, keyID: keyObjectID)
}
}
}
PIN Authentication
Return a TKTokenAuthOperation from beginAuthFor: to prompt the user
for PIN entry before cryptographic operations:
func tokenSession(
_ session: TKTokenSession,
beginAuthFor operation: TKTokenOperation,
constraint: Any
) throws -> TKTokenAuthOperation {
let pinAuth = TKTokenSmartCardPINAuthOperation()
pinAuth.pinFormat.charset = .numeric
pinAuth.pinFormat.minPINLength = 4
pinAuth.pinFormat.maxPINLength = 8
pinAuth.smartCard = (session as? TKSmartCardTokenSession)?.smartCard
pinAuth.apduTemplate = buildVerifyAPDU()
pinAuth.pinByteOffset = 5
return pinAuth
}
Smart Card Communication
TKSmartCard provides low-level APDU communication with smart cards
connected via readers (macOS-only).
Discovering Card Readers
import CryptoTokenKit
func discoverSmartCards() {
guard let slotManager = TKSmartCardSlotManager.default else {
print("Smart card services unavailable")
return
}
for slotName in slotManager.slotNames {
slotManager.getSlot(withName: slotName) { slot in
guard let slot else { return }
if slot.state == .validCard, let card = slot.makeSmartCard() {
communicateWith(card: card)
}
}
}
}
Sending APDU Commands
Use send(ins:p1:p2:data:le:) for structured APDU communication.
Always wrap calls in withSession:
func selectApplication(card: TKSmartCard, aid: Data) throws {
try card.withSession {
let (sw, response) = try card.send(
ins: 0xA4, p1: 0x04, p2: 0x00, data: aid, le: nil
)
guard sw == 0x9000 else {
throw TKError(.communicationError)
}
}
}
For raw APDU bytes or non-standard formats, use transmit(_:reply:) with
manual beginSession/endSession lifecycle management.
NFC Smart Card Sessions (iOS 16.4+)
On supported iOS devices, create NFC smart card sessions to communicate with contactless smart cards:
func readNFCSmartCard() {
guard let slotManager = TKSmartCardSlotManager.default,
slotManager.isNFCSupported() else { return }
slotManager.createNFCSlot(message: "Hold card near iPhone") { session, error in
guard let session else { return }
defer { session.end() }
guard let slotName = session.slotName,
let slot = slotManager.slotNamed(slotName),
let card = slot.makeSmartCard() else { return }
// Communicate with the NFC card using card.send(...)
}
}
Keychain Integration
When a token is present, CryptoTokenKit exposes its items as standard
keychain entries. Query them using the kSecAttrTokenID attribute:
import Security
func findTokenKey(tokenID: String) throws -> SecKey {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrTokenID as String: tokenID,
kSecReturnRef as String: true
]
var result: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let key = result else {
throw TKError(.objectNotFound)
}
return key as! SecKey
}
Use kSecReturnPersistentRef instead of kSecReturnRef to obtain a
persistent reference that survives across app launches. The reference
becomes invalid when the token is removed -- handle errSecItemNotFound
by prompting the user to reinsert the token.
Query certificates the same way with kSecClass: kSecClassCertificate.
Certificate Authentication
Token Key Requirements
For user login, the token must contain at least one key capable of signing with: EC signature digest X962, RSA signature digest PSS, or RSA signature digest PKCS1v15.
For keychain unlock, the token needs:
- 256-bit EC key (
kSecAttrKeyTypeECSECPrimeRandom) supportingecdhKeyExchangeStandard, or - 2048/3072/4096-bit RSA key (
kSecAttrKeyTypeRSA) supportingrsaEncryptionOAEPSHA256decryption
Smart Card Authentication Preferences (macOS)
Configure in the com.apple.security.smartcard domain (MDM or systemwide):
| Key | Default | Description |
|---|---|---|
allowSmartCard |
true |
Enable smart card authentication |
checkCertificateTrust |
0 |
Certificate trust level (0-3) |
oneCardPerUser |
false |
Pair a single smart card to an account |
enforceSmartCard |
false |
Require smart card for login |
Trust levels: 0 = trust all, 1 = validity + issuer, 2 = + soft
revocation, 3 = + hard revocation.
Token Watching
TKTokenWatcher monitors token insertion and removal. Available on both
macOS and iOS 14+.
import CryptoTokenKit
final class TokenMonitor {
private let watcher = TKTokenWatcher()
func startMonitoring() {
for tokenID in watcher.tokenIDs {
print("Token present: \(tokenID)")
if let info = watcher.tokenInfo(forTokenID: tokenID) {
print(" Driver: \(info.driverName ?? "unknown")")
print(" Slot: \(info.slotName ?? "unknown")")
}
}
watcher.setInsertionHandler { [weak self] tokenID in
print("Token inserted: \(tokenID)")
self?.watcher.addRemovalHandler({ removedTokenID in
print("Token removed: \(removedTokenID)")
}, forTokenID: tokenID)
}
}
}
Error Handling
CryptoTokenKit operations throw TKError. Key error codes:
| Code | Meaning |
|---|---|
.notImplemented |
Operation not supported by this token |
.communicationError |
Communication with token failed |
.corruptedData |
Data from token is corrupted |
.canceledByUser |
User canceled the operation |
.authenticationFailed |
PIN or password incorrect |
.objectNotFound |
Requested key or certificate not found |
.tokenNotFound |
Token is no longer present |
.authenticationNeeded |
Authentication required before operation |
Common Mistakes
DON'T: Query token keychain items without checking token presence
// WRONG -- query may fail if token was removed
let key = try findTokenKey(tokenID: savedTokenID)
// CORRECT -- verify the token is still present first
let watcher = TKTokenWatcher()
guard watcher.tokenIDs.contains(savedTokenID) else {
promptUserToInsertToken()
return
}
let key = try findTokenKey(tokenID: savedTokenID)
DON'T: Assume smart card APIs work on iOS
// WRONG -- TKSmartCardSlotManager.default is nil on iOS
let manager = TKSmartCardSlotManager.default! // Crashes on iOS
// CORRECT -- guard availability
guard let manager = TKSmartCardSlotManager.default else {
print("Smart card services unavailable on this platform")
return
}
DON'T: Skip session management for card communication
// WRONG -- sending commands without a session
card.transmit(apdu) { response, error in /* may fail */ }
// CORRECT -- use withSession or beginSession/endSession
try card.withSession {
let (sw, response) = try card.send(
ins: 0xCA, p1: 0x00, p2: 0x6E, data: nil, le: 0
)
}
DON'T: Ignore status words in APDU responses
// WRONG -- assuming success
let (_, response) = try card.send(ins: 0xA4, p1: 0x04, p2: 0x00, data: aid, le: nil)
// CORRECT -- check status word
let (sw, response) = try card.send(ins: 0xA4, p1: 0x04, p2: 0x00, data: aid, le: nil)
guard sw == 0x9000 else {
throw SmartCardError.commandFailed(statusWord: sw)
}
DON'T: Hard-code blanket algorithm support
The supports delegate method must reflect what the hardware actually
implements. Returning true unconditionally causes runtime failures when
the system attempts unsupported operations.
Review Checklist
- Platform availability verified (
TKSmartCardmacOS-only,TKTokenWatcheriOS 14+) - Token extension target uses
NSExtensionPointIdentifier=com.apple.ctk-tokens -
com.apple.ctk.driver-classset to the correct driver class in Info.plist - Extension registered via
_securityagentlaunch during installation -
TKTokenSessionDelegatechecks specific algorithms, not blankettrue - Smart card sessions opened and closed (
withSessionorbeginSession/endSession) - APDU status words checked after every
sendcall - Token presence verified via
TKTokenWatcherbefore keychain queries -
TKErrorcases handled with appropriate user feedback - Keychain contents populated with correct
objectIDvalues -
TKTokenKeychainKeycapabilities (canSign,canDecrypt) match hardware - Certificate trust level configured appropriately for deployment environment
-
errSecItemNotFoundhandled for persistent references when token is removed
References
- Extended patterns (PIV commands, TLV parsing, generic token drivers, APDU helpers, secure PIN): references/cryptotokenkit-patterns.md
- CryptoTokenKit framework
- TKTokenDriver
- TKToken
- TKTokenSession
- TKSmartCard
- TKSmartCardSlotManager
- TKTokenWatcher
- Authenticating Users with a Cryptographic Token
- Using Cryptographic Assets Stored on a Smart Card
- Configuring Smart Card Authentication