cryptotokenkit

cryptotokenkit

热门

使用 CryptoTokenKit 访问安全令牌与智能卡。适用于开发 TKTokenDriver 或 TKSmartCardTokenDriver 扩展、通过 TKSmartCard/TKSmartCardSlotManager 与智能卡通信、使用 iOS 26+ NFC 智能卡会话、注册智能卡、使用 kSecAttrTokenID 查询令牌关联的 Keychain 项、监听 TKTokenWatcher 状态,以及配置基于证书的智能卡身份验证。

967Star
49Fork
更新于 2026/7/31
SKILL.md
只读
名称
cryptotokenkit
描述

使用 CryptoTokenKit 访问安全令牌与智能卡。适用于开发 TKTokenDriver 或 TKSmartCardTokenDriver 扩展、通过 TKSmartCard/TKSmartCardSlotManager 与智能卡通信、使用 iOS 26+ NFC 智能卡会话、注册智能卡、使用 kSecAttrTokenID 查询令牌关联的 Keychain 项、监听 TKTokenWatcher 状态,以及配置基于证书的智能卡身份验证。

CryptoTokenKit

在 Swift 6.3 应用中使用 CryptoTokenKit 实现令牌驱动扩展(token driver extensions)、智能卡通信、令牌会话(token sessions)、基于令牌的 Keychain 集成以及基于证书的身份验证。

平台可用性: CryptoTokenKit 的类在 Apple 各个平台上均可用,但具体功能取决于扩展点(extension point)、Entitlement 权限、硬件设备和 OS 版本。用于系统登录/Keychain 解锁的智能卡 App 扩展流程仅支持 macOS。TKSmartCardSlotManager.default 是可选的(optional),除非已启用智能卡访问权限,否则会返回 nil。iOS/iPadOS 26+ 新增了 NFC 智能卡插槽和注册支持。

目录

架构概览

CryptoTokenKit 充当硬件安全令牌(智能卡、USB 令牌等)与系统身份验证及 Keychain 服务之间的桥梁。该框架主要有三种使用模式:

智能卡令牌扩展 —— macOS App 扩展,可将硬件令牌的密码学能力提供给系统登录和 Keychain 解锁使用。驱动程序负责处理令牌的生命周期、会话管理和密码学操作。

客户端令牌访问 —— 应用查询由令牌支持的 Keychain 项。当存在令牌时,CryptoTokenKit 会将令牌中的条目暴露为标准 Keychain 记录。

NFC 智能卡访问 —— iOS/iPadOS 26+ 应用可创建临时的 NFC 智能卡插槽,并通过 TKSmartCard 与贴近的非接触式智能卡进行通信。

边界路由: 自身令牌/智能卡会话、令牌关联的 Keychain 项以及基于证书的智能卡认证交由本框架;Passkey/WebAuthn 及账号登录请路由至 authentication;Secure Enclave、CryptoKit 密码学原语、Keychain 整体架构、证书固定(certificate pinning)及信任策略请路由至 swift-security

核心类型

类型 职责 平台支持
TKTokenDriver / TKToken / TKTokenSession 令牌驱动、令牌与会话基础原语 iOS 10+, macOS 10.12+
TKSmartCardTokenDriver 智能卡令牌扩展的入口点 iOS 10+, macOS 10.12+;macOS 扩展流程
TKSmartCard / TKSmartCardSlotManager 底层 APDU 通信与插槽发现 iOS 9+, macOS 10.10+;default 为可选值
TKTokenWatcher 监听令牌的插入与拔出 iOS 10+, macOS 10.12+
TKSmartCardSlotNFCSession 基于 NFC 的智能卡插槽会话 iOS/iPadOS 26+
TKSmartCardTokenRegistrationManager 注册 NFC 智能卡以供后续 Keychain 使用 iOS/iPadOS 26+

令牌扩展

在 macOS 上进行系统登录和 Keychain 解锁时,令牌驱动是一个 App 扩展,它负责向系统暴露硬件令牌的密码学能力。宿主 App(host app)仅作为交付该扩展的载体。

一个智能卡令牌扩展包含三个核心类:

  1. TokenDriverTKSmartCardTokenDriver 的子类)—— 入口点
  2. TokenTKSmartCardToken 的子类)—— 代表令牌实体
  3. TokenSessionTKSmartCardTokenSession 的子类)—— 处理实际的密码学操作

驱动类

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
        )
    }
}

令牌类

令牌类负责从硬件读取证书和密钥,并填充其 Keychain 内容:

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 与注册

扩展的 Info.plist 必须指定驱动类名称:

NSExtension
  NSExtensionAttributes
    com.apple.ctk.driver-class = $(PRODUCT_MODULE_NAME).TokenDriver
  NSExtensionPointIdentifier = com.apple.ctk-tokens

通过以 _securityagent 用户身份运行一次宿主 App 来完成扩展注册:

sudo -u _securityagent /Applications/TokenHost.app/Contents/MacOS/TokenHost

令牌会话

TKTokenSession 用于管理身份验证状态,并通过其 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 码身份验证

在执行密码学操作前,可从 beginAuthFor: 返回一个 TKTokenAuthOperation 来提示用户输入 PIN 码:

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
}

智能卡通信

TKSmartCard 提供了与智能卡进行底层 APDU 通信的能力。TKSmartCardSlotManager.default 是可选值,当返回 nil 时,应视为无可用硬件、缺失 Entitlement 访问权限或运行时环境不支持。

发现读卡器

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)
            }
        }
    }
}

发送 APDU 指令

使用 send(ins:p1:p2:data:le:) 进行结构化的 APDU 通信。务必将其包裹在 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)
        }
    }
}

对于原始 APDU 字节流或非标准格式,请使用 transmit(_:reply:),并配合手动管理的 beginSession/endSession 生命周期。

NFC 智能卡会话(iOS/iPadOS 26+)

在 iOS/iPadOS 26+ 上,调用 createNFCSlot(message:completion:) 与非接触式卡片通信前,请先检查 isNFCSupported()

@available(iOS 26.0, iPadOS 26.0, *)
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 {
            handleNFCError(error)
            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 集成

当插入令牌时,CryptoTokenKit 会将其条目暴露为标准的 Keychain 记录。可通过 kSecAttrTokenID 属性进行查询:

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
}

使用 kSecReturnPersistentRef 代替 kSecReturnRef 可以获取在应用重新启动后依然有效的持久化引用。注意:当令牌拔出时该引用会失效 —— 请妥善处理 errSecItemNotFound,并提示用户重新插入令牌。

查询证书时同理,只需设置 kSecClass: kSecClassCertificate

证书身份验证

令牌密钥要求

对于用户登录,令牌中必须包含至少一把具备以下签名能力的密钥:EC 签名摘要 X962、RSA 签名摘要 PSS 或 RSA 签名摘要 PKCS1v15。

对于 Keychain 解锁,令牌需要具备:

  • 支持 ecdhKeyExchangeStandard 的 256 位 EC 密钥(kSecAttrKeyTypeECSECPrimeRandom),或
  • 支持 rsaEncryptionOAEPSHA256 解密的 2048/3072/4096 位 RSA 密钥(kSecAttrKeyTypeRSA

智能卡身份验证偏好设置(macOS)

com.apple.security.smartcard 域名下配置(通过 MDM 或全局系统配置):

| 键名 | 默认值 | 描述