简短回答:硬件支持的密钥在独立的芯片或隔离的处理器内部生成,永远不会以明文形式离开那里;你的代码只持有一个句柄,需要签名时就请求硬件完成。它能很好地保证一件事:私钥无法被复制出设备,因此被盗的磁盘镜像、备份或内存转储都毫无用处。它不能阻止同一设备上的恶意软件请求硬件签名,除非你要求每次使用时都确认用户在场或进行生物识别。支持的算法很少(P-256 处处可用,部分平台支持 RSA,Apple 的 Secure Enclave 自 26 版系统起支持后量子的 ML-DSA 和 ML-KEM)。证明让服务器能够通过一条通往制造商的证书链,确认密钥确实存放在这样的硬件中:Android 对应 Google 的根证书,YubiKey 对应 Yubico 的根证书,TPM 对应 TPM 厂商的根证书。丢失设备就意味着丢失密钥,因此从设计之初就要支持注册多个密钥和密钥轮换。
各个平台
| 密钥存放位置 | 常见算法 | 证明 | 用户确认 | |
|---|---|---|---|---|
| Apple 的 Secure Enclave | Apple 芯片中的专用协处理器 | P-256;ML-KEM-768/1024 和 ML-DSA-65/87(CryptoKit,26 及以上系统版本) | iOS 上的 App Attest;任意密钥没有证明 | 通过访问控制要求 Touch ID、Face ID 或设备密码 |
| Android 密钥库,TEE | 主 SoC 的可信执行环境 | RSA、EC、AES、HMAC | 密钥证明,链到 Google 根证书 | 每次使用或在一段时间内要求锁屏凭证或生物识别 |
| Android StrongBox | 嵌入式安全元件或集成式安全隔区(Android 9+) | RSA 2048、P-256、AES、HMAC-SHA256、3DES | 同上,安全级别为 StrongBox | 同上 |
| TPM 2.0 | 独立芯片、固件 TPM 或虚拟 TPM | RSA 2048+、P-256、P-384(取决于厂商) | AK 由 EK 认证,EK 证书来自厂商 | 授权值和策略 |
| YubiKey PIV | 令牌上的智能卡应用 | RSA 1024/2048、P-256、P-384;固件 5.7 起另有 RSA 3072/4096、Ed25519 和 X25519 | 来自插槽 f9 的证明证书,链到 Yubico | 每个密钥的 PIN 策略和触摸策略 |
| 云 HSM | 云服务商运营的 HSM | 见用云 KMS 签名 | Google Cloud HSM 证明声明 | IAM,而不是人 |
通行密钥(passkey)是上述一切最常见的用途:平台认证器就是前四行中的某一种硬件加上 WebAuthn 接口(面向开发者的通行密钥指南)。
在这台 Mac 上实测 Apple 的 Secure Enclave
Secure Enclave(安全隔区)是 Apple 的隔离式密钥处理器。借助 CryptoKit,创建密钥只需一行代码。我们在一台运行 macOS 27.0.1 和 Swift 6.4 的 Apple M4 Max 上运行了下面的代码,它只是一个普通的命令行工具,没有任何特殊权限:
// Secure Enclave from a command-line tool: create a P-256 signing key, sign, export the public key,
// and look at what the "private key" object actually contains.
import CryptoKit
import Foundation
print("SecureEnclave.isAvailable: \(SecureEnclave.isAvailable)")
let key = try SecureEnclave.P256.Signing.PrivateKey()
let message = Data("release 1.4.2".utf8)
let signature = try key.signature(for: message)
print("public key (x9.63): \(key.publicKey.x963Representation.count) bytes")
print("signature (DER): \(signature.derRepresentation.count) bytes, raw r||s: \(signature.rawRepresentation.count) bytes")
print("verifies with the public key: \(key.publicKey.isValidSignature(signature, for: message))")
// dataRepresentation is an opaque blob: the key encrypted by the Secure Enclave for this device.
let blob = key.dataRepresentation
print("dataRepresentation: \(blob.count) bytes (opaque, usable only by this device's Secure Enclave)")
let restored = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: blob)
print("restored from the blob signs too: \(restored.publicKey.isValidSignature(try restored.signature(for: message), for: message))")
// A software key from the same API family, for comparison: its raw bytes are the private scalar.
let software = P256.Signing.PrivateKey()
print("software key rawRepresentation: \(software.rawRepresentation.count) bytes (the private scalar itself)")
SecureEnclave.isAvailable: true
public key (x9.63): 65 bytes
signature (DER): 71 bytes, raw r||s: 64 bytes
verifies with the public key: true
dataRepresentation: 284 bytes (opaque, usable only by this device's Secure Enclave)
restored from the blob signs too: true
software key rawRepresentation: 32 bytes (the private scalar itself)
284 字节的 dataRepresentation 是应用为了日后再次使用该密钥而保存的内容(存入钥匙串或文件);它由 Secure Enclave 封装,只有那台设备的 Secure Enclave 才能使用。根本没有办法获取私钥标量:试图这样做的代码无法通过编译。
export.swift:5:11: error: value of type 'SecureEnclave.P256.Signing.PrivateKey' has no member 'rawRepresentation'
Apple 的指南用 Secure Enclave 保护密钥是为 Security 框架的 SecKey API 编写的,其中至今仍写着它“仅适用于 NIST P-256 椭圆曲线密钥”。CryptoKit 走得更远:从 iOS 和 macOS 26 起,SecureEnclave.MLDSA65、MLDSA87、MLKEM768 和 MLKEM1024 均可使用。在这台 Mac 上:
let mldsa = try SecureEnclave.MLDSA65.PrivateKey()
let signature = try mldsa.signature(for: message)
print("ML-DSA-65 in the Secure Enclave: public key \(mldsa.publicKey.rawRepresentation.count) bytes, signature \(signature.count) bytes")
print("verifies: \(mldsa.publicKey.isValidSignature(signature, for: message))")
let mlkem = try SecureEnclave.MLKEM768.PrivateKey()
let encapsulated = try mlkem.publicKey.encapsulate()
let shared = try mlkem.decapsulate(encapsulated.encapsulated)
print("ML-KEM-768 in the Secure Enclave: ciphertext \(encapsulated.encapsulated.count) bytes, shared secrets match: \(shared == encapsulated.sharedSecret)")
ML-DSA-65 in the Secure Enclave: public key 1952 bytes, signature 3309 bytes
verifies: true
ML-KEM-768 in the Secure Enclave: ciphertext 1088 bytes, shared secrets match: true
这些大小与 FIPS 204 和 FIPS 203 中的规定值一致。对于任意密钥,Apple 不提供可供服务器检查的证明;在 iOS 上,App Attest(DeviceCheck)证明的是应用本身,以及它为此专门创建的一个 Secure Enclave 密钥。
Android 密钥库:TEE 与 StrongBox
在 Android 上,Android 密钥库中生成的密钥保持“不可导出”:“密钥材料永远不会进入应用进程”,而且当设备的安全硬件支持该密钥的算法和参数时,“其密钥材料永远不会暴露在安全硬件之外。”硬件分为两个级别:
- TEE(
KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT):主处理器上的隔离环境。 - StrongBox(Android 9 及更高版本,
setIsStrongBoxBacked(true)):“嵌入式 SE 或集成式安全隔区(iSE),与 TEE 相比提供更强的隔离和防篡改能力”,拥有自己的 CPU、存储、随机数生成器和安全计时器。它只支持“RSA 2048、AES 128 和 256、ECDSA、ECDH P-256、HMAC-SHA256”以及三重 DES,Google 还提醒它“速度更慢、资源更受限”,并且“对大多数应用来说,StrongBox 并非必需。”请求不受支持的配置时会抛出StrongBoxUnavailableException。
用 KeyInfo.getSecurityLevel()(API 31+;更早的版本用 isInsideSecureHardware())检查密钥最终存放在哪里,并用 setUserAuthenticationParameters() 把密钥的使用与用户绑定:可以每次操作都要求验证,也可以在解锁后的一段时间窗口内放行。同一页面还提醒了一点:硬件“通常不会强制执行有效时间区间类的授权,因为它通常没有独立、安全的实时时钟。”
密钥证明负责向你的服务器证明这一点。设备会返回该密钥的证书链;第一张带有证明扩展(OID 1.3.6.1.4.1.11129.2.1.17)的证书描述了密钥本身、它的属性和安全级别。密钥证明文档中值得照搬到你的验证器里的规则如下:
- 在服务器上验证,切勿在设备本身上验证。
- 根证书必须是 Google 的证明根证书之一,发布在
android.googleapis.com/attestation/root。名为“Key Attestation CA1”的 ECDSA P-384 根证书“将从2026年2月1日起为证明证书链签名”,与长期使用的 RSA 根证书并存;两者都要接受。 - 用 Google 的吊销列表
android.googleapis.com/attestation/status检查每一张证书。 - “只有链中第一次出现的该扩展才可信”;之后出现的扩展可能来自延长证书链的攻击者。
- 要求
attestationSecurityLevel为TrustedEnvironment或StrongBox;链到任何其他根证书的证书链,或者软件证明,都说明不了硬件的任何情况。
与其自己编写验证器,Google 更推荐使用它的 Kotlin 验证库。Pixel 相机的 C2PA 签名就是建立在这之上的一个例子:声明签名密钥“使用 Titan M2 安全芯片中的 Android StrongBox 生成和存储”,并且只有在密钥证明通过之后才会颁发证书(C2PA 内容凭证)。
TPM 2.0:在软件 TPM 中演示密钥、策略和证明
在 PC 和服务器上,与之对应的是 TPM。为了在没有硬件的情况下展示它的机制,我们在实验环境的 debian:forky-20261005-slim 中运行了 swtpm 0.10.2(一个软件 TPM,从设计上就什么也保护不了)和 tpm2-tools 5.8。签名密钥在一个主密钥之下创建,其属性把它绑定到这个 TPM:
tpm2_createprimary -Q -C o -g sha256 -G ecc256 -c primary.ctx
tpm2_create -Q -C primary.ctx -G ecc256:ecdsa-sha256 -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign" -u sig.pub -r sig.priv
tpm2_load -Q -C primary.ctx -u sig.pub -r sig.priv -c sig.ctx
tpm2_readpublic -c sig.ctx -f pem -o sig.pem
tpm2_sign -Q -c sig.ctx -g sha256 -f plain -o msg.sig msg.txt
openssl dgst -sha256 -verify sig.pem -signature msg.sig msg.txt
attributes:
value: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign
sig.priv: 128 bytes (encrypted by the TPM, usable only under this parent)
Verified OK
fixedtpm 和 fixedparent 表示该密钥永远不能被复制到另一个 TPM 或另一个父密钥之下;sensitivedataorigin 表示密钥由 TPM 生成。sig.priv 是用父密钥加密后的密钥,可以安全地存放在磁盘上。签名是普通的 DER 编码 ECDSA 签名,由 OpenSSL 用导出的公钥验证。
证明使用一个单独的、带有 restricted 属性的证明密钥(AK)。tpm2_certify 让 AK 对一条关于另一个密钥的声明签名,声明中包含验证方提供的 nonce:
tpm2_create -Q -C primary.ctx -G ecc256:ecdsa-sha256:null -a "fixedtpm|fixedparent|sensitivedataorigin|userwithauth|restricted|sign" -u ak.pub -r ak.priv
tpm2_load -Q -C primary.ctx -u ak.pub -r ak.priv -c ak.ctx
tpm2_readpublic -Q -c ak.ctx -f pem -o ak.pem
printf 'server-nonce-7f3a' > nonce.bin
tpm2_certify -Q -c sig.ctx -C ak.ctx -g sha256 -q nonce.bin -o attest.bin -s attest.sig -f plain
openssl dgst -sha256 -verify ak.pem -signature attest.sig attest.bin
tpm2_print -t TPMS_ATTEST attest.bin
Verified OK
magic: ff544347
type: 8017
extraData: 7365727665722d6e6f6e63652d37663361
name: 000b2d7a257558828e1caf174b67daabff261f8c02077768266be3cdc58015de4bee
qualifiedName: 000b6d68f45fa311b20dc86b37e1659c1f06055e5641aa6a03e28cbb2caad6646fcd
name of the signing key: 000b2d7a257558828e1caf174b67daabff261f8c02077768266be3cdc58015de4bee
该结构以魔数 0xff544347(“TPM_GENERATED”)开头,类型 0x8017 表示认证声明,extraData 是我们以十六进制表示的 nonce,name 是被认证密钥的公开区域和属性的哈希:它与签名密钥一致。凭什么信任它?因为受限密钥会拒绝对以 TPM_GENERATED 魔数开头的数据签名,除非这些数据是 TPM 自己生成的:
ordinary data (msg.txt): signed by the attestation key
ERROR: Esys_Sign(0x3E0) - tpm:parameter(3):invalid ticket
在真实部署中,这条链还会继续延伸:AK 与 TPM 的背书密钥(EK)绑定,EK 的证书由 TPM 厂商颁发,验证方会检查这条证书链;WebAuthn 的 tpm 证明声明格式就是这样工作的。软件 TPM 没有这样的证书,这正是不得信任其证明的原因。
YubiKey PIV 与证明
YubiKey 的 PIV 应用把密钥存放在插槽中(9a 身份验证、9c 签名、9d 密钥管理、9e 卡身份验证),PIN 策略和触摸策略在生成密钥时就已固定。PIV 证明能够表明“某个非对称密钥是在设备上生成的,而不是导入的”:YubiKey 用插槽 f9 中的证明密钥为该密钥签发一张证书,而这个证明密钥本身带有一张由 Yubico PIV CA 签发的证书。以下命令来自 yubico-piv-tool 2.7.3(实验环境里没有硬件令牌,所以只核对了它的 --help):
yubico-piv-tool -a generate -s 9c -A ECCP256 --pin-policy=always --touch-policy=always -o 9c.pub
yubico-piv-tool -a attest -s 9c > 9c-attest.pem
yubico-piv-tool -a read-certificate -s f9 > f9-intermediate.pem
证明证书带有 Yubico 扩展:固件版本(1.3.6.1.4.1.41482.3.3)、序列号(…3.7)、PIN 和触摸策略(…3.8)、外形规格(…3.9)以及 FIPS 或 CSPN 状态(…3.10、…3.11)。验证方检查从插槽证书经 f9 证书到 Yubico 根证书的证书链,然后读取策略:“touch always”(始终需要触摸)表示每次签名时都有人实际在场。Yubico 正在轮换根证书:“固件版本 5.7.4 及更高版本的 YubiKey 将由我们的新根 CA 签名”,因此验证方需要同时接受两个根证书。CA/Browser Forum 对代码签名密钥认可的正是这类证明(Authenticode),而 OpenSSH 则通过 FIDO2(而不是 PIV)使用同样的硬件令牌(ssh-keygen)。
硬件密钥做不到的事
- 防范以用户身份运行的代码。设备上的恶意软件可以请求硬件签名,只是无法把密钥带走。对重要的密钥,要求用户在场(触摸)、用户验证(PIN、生物识别)或显式确认。
- 可用性。设备丢失、被擦除或损坏,密钥也随之消失;清除 TPM(某些固件更新和重装系统会这样做)会使其下的所有密钥失效。至少注册两个认证器,保留恢复途径,并规划轮换。
- 算法敏捷性。芯片实现了什么,你就只能用什么。许多平台仍然只提供 P-256 和 RSA;如果某个协议需要 Ed25519 或 secp256k1,先确认支持情况,再围绕硬件做设计。
- 对硬件所签内容的信任。安全的密钥会对操作系统送来的任何内容签名;显示“确认付款”的屏幕属于同一个可能已被攻破的操作系统。正因如此,才有了自带屏幕的硬件钱包。
- 没有证明时的证据。如果你的服务器不验证证明证书链,“密钥在硬件中”就只是客户端的一面之词。
使用硬件密钥的检查清单
- 在设备上生成密钥;凡是你想称为硬件支持的密钥,都不要导入。
- 选择你需要的级别:大多数应用用 Secure Enclave 或 TEE,高价值密钥用 StrongBox 或安全元件,服务器密钥用 HSM。
- 在签名用于授权某项操作的地方,把密钥的使用与用户绑定(每次使用都需要生物识别或 PIN)。
- 在服务器上验证证明:根证书(当前的和轮换后的)、吊销状态、nonce、安全级别、密钥属性。
- 只存储公钥和不透明句柄;要预料到密钥会消失,并支持重新注册。
来源
- Apple:用 Secure Enclave 保护密钥,CryptoKit
SecureEnclave.MLDSA65和SecureEnclave.MLKEM768 - Android:Android 密钥库系统、密钥证明、证明 schema
- TCG TPM 2.0 库规范(TPMS_ATTEST、受限密钥);tpm2-tools 5.8、swtpm 0.10.2
- Yubico:PIV 证明、YubiKey 与 PIV(各固件版本支持的算法)、yubico-piv-tool 2.7.3
- Google 安全博客:Pixel 的 C2PA 签名