Перейти к содержимому
ecdsa.com
← Все статьи

Аппаратные ключи: Secure Enclave, StrongBox, TPM, PIV

Мы создали ключи в Secure Enclave этого Mac, в том числе постквантовые, и в программном TPM с аттестацией, а также разобрали правила аттестации Android и Yubico. Обещание у всех платформ одно, узкое, но ценное: ключ невозможно скопировать.

Коротко. Аппаратный ключ генерируется внутри отдельного чипа или изолированного процессора и никогда не покидает его в открытом виде; ваш код хранит только дескриптор ключа и просит оборудование выполнить подпись. Надёжно это гарантирует одно: закрытый ключ нельзя скопировать с устройства, поэтому украденный образ диска, резервная копия или дамп памяти бесполезны. Но это не мешает вредоносной программе на том же устройстве попросить оборудование что-нибудь подписать, если только вы не требуете присутствия пользователя или биометрии при каждом использовании. Поддерживаемых алгоритмов немного (P-256 везде, RSA на некоторых платформах, постквантовые ML-DSA и ML-KEM в Secure Enclave от Apple начиная с ОС версии 26). Аттестация позволяет серверу проверить, что ключ действительно находится в таком оборудовании, по цепочке сертификатов до производителя: до корневых сертификатов Google для Android, Yubico — для YubiKey, производителя TPM — для TPM. Потерять устройство — значит потерять ключ, поэтому с самого начала закладывайте в архитектуру регистрацию нескольких ключей и их ротацию.

Платформы

Где хранится ключТипичные алгоритмыАттестацияПроверка пользователя
Apple Secure EnclaveОтдельный сопроцессор в чипах Apple siliconP-256; ML-KEM-768/1024 и ML-DSA-65/87 (CryptoKit, ОС 26+)App Attest в iOS; для произвольных ключей нетTouch ID, Face ID, код-пароль через управление доступом
Android Keystore, TEEДоверенная среда исполнения основной системы на кристалле (SoC)RSA, EC, AES, HMACАттестация ключей до корневых сертификатов GoogleУчётные данные блокировки экрана или биометрия: при каждом использовании или на заданное время
Android StrongBoxВстроенный защищённый элемент (secure element) или интегрированный защищённый анклав (Android 9+)RSA 2048, P-256, AES, HMAC-SHA256, 3DESТо же, с уровнем безопасности StrongBoxТо же
TPM 2.0Отдельный чип, TPM в прошивке или виртуальный TPMRSA 2048+, P-256, P-384 (зависит от производителя)AK, заверенный ключом EK; сертификат EK от производителяЗначения авторизации и политики
YubiKey PIVАпплет смарт-карты на токенеRSA 1024/2048, P-256, P-384; RSA 3072/4096, Ed25519 и X25519 начиная с прошивки 5.7Сертификат аттестации из слота f9 с цепочкой до YubicoПолитика PIN и политика касания для каждого ключа
Облачный HSMHSM, которыми управляет провайдерСм. статью о подписи с помощью облачных KMSЗаявления об аттестации Google Cloud HSMIAM, а не люди

Ключи доступа — самое распространённое применение всего этого: платформенный аутентификатор — это одна из первых четырёх строк таблицы плюс интерфейс WebAuthn (ключи доступа для разработчиков).

Apple Secure Enclave: проверка на этом Mac

Secure Enclave — изолированный процессор Apple для работы с ключами. В CryptoKit ключ создаётся одной строкой. Мы запускали этот код на Apple M4 Max с macOS 27.0.1 и Swift 6.4 как обычный инструмент командной строки, без особых прав (entitlements):

main.swift: ключ P-256 в Secure Enclave рядом с программным ключом
// 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)")
Вывод (macOS 27.0.1, Apple M4 Max; открытый ключ в формате PEM опущен)
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)

Именно dataRepresentation размером 284 байта приложение сохраняет (в связке ключей или в файле), чтобы позже снова воспользоваться ключом; Secure Enclave зашифровал этот блок, и использовать его может только анклав этого устройства. Запросить закрытый скаляр нельзя вообще: код, который пытается это сделать, не компилируется.

swiftc на файле, который читает key.rawRepresentation у ключа из Secure Enclave
export.swift:5:11: error: value of type 'SecureEnclave.P256.Signing.PrivateKey' has no member 'rawRepresentation'

В руководстве Apple Защита ключей с помощью Secure Enclave, написанном для API SecKey из фреймворка Security, по-прежнему сказано, что Secure Enclave «работает только с ключами на эллиптической кривой NIST P-256». CryptoKit пошёл дальше: SecureEnclave.MLDSA65, MLDSA87, MLKEM768 и MLKEM1024 доступны начиная с iOS и macOS 26. На этом Mac:

pq.swift: постквантовые ключи в Secure Enclave
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 Keystore: TEE и StrongBox

В Android ключи, сгенерированные в Android Keystore, остаются «неэкспортируемыми»: «ключевой материал никогда не попадает в процесс приложения», а если защищённое оборудование устройства поддерживает алгоритм и параметры ключа, «его ключевой материал никогда не раскрывается за пределами защищённого оборудования». Уровней оборудования два:

  • TEE (KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT): изолированная среда на основном процессоре.
  • StrongBox (Android 9 и новее, setIsStrongBoxBacked(true)): «встроенные защищённые элементы (SE) или интегрированные защищённые анклавы (iSE), которые обеспечивают более сильную изоляцию и защиту от физического вмешательства по сравнению с TEE», с собственным процессором, хранилищем, генератором случайных чисел и защищённым таймером. StrongBox поддерживает только «RSA 2048, AES 128 и 256, ECDSA, ECDH P-256, HMAC-SHA256» и Triple 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. Корневой сертификат ECDSA P-384 «Key Attestation CA1» «начнёт подписывать цепочки сертификатов аттестации 1 февраля 2026 года» наряду с давно используемым корневым сертификатом RSA; принимайте оба.
  • Сверяйте каждый сертификат со списком отзыва Google по адресу android.googleapis.com/attestation/status.
  • «Доверять можно только первому вхождению расширения в цепочке»; последующие мог добавить злоумышленник, дописавший цепочку.
  • Требуйте, чтобы attestationSecurityLevel был равен TrustedEnvironment или StrongBox; цепочка до любого другого корня или программная аттестация ничего не говорят об оборудовании.

Вместо самописного кода проверки Google рекомендует свою библиотеку проверки на Kotlin. На этом основана, например, подпись C2PA в камере Pixel: ключи для подписи claim «генерируются и хранятся с помощью Android StrongBox в чипе безопасности Titan M2», а сертификаты выдаются только после аттестации ключа (C2PA Content Credentials).

TPM 2.0: ключи, политики и аттестация на программном TPM

TPM — аналог всего этого для ПК и серверов. Чтобы показать его механику без оборудования, мы запустили в лаборатории swtpm 0.10.2 (программный TPM, который по своему устройству ничего не защищает) вместе с tpm2-tools 5.8 в debian:forky-20261005-slim. Ключ подписи создаётся под первичным ключом и получает атрибуты, которые привязывают его к этому TPM:

lab.sh (фрагмент): неэкспортируемый ключ подписи P-256, подпись, проверка в OpenSSL
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 — ключ, зашифрованный родительским ключом; его можно спокойно хранить на диске. Подпись — обычная подпись ECDSA в кодировке DER, и OpenSSL проверяет её экспортированным открытым ключом.

Для аттестации используется отдельный ключ аттестации (attestation key, AK) с атрибутом restricted. tpm2_certify заставляет AK подписать заявление о другом ключе, в которое входит nonce (одноразовое число) проверяющей стороны:

lab.sh (фрагмент): заверение ключа подписи ограниченным ключом аттестации
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 обозначает заявление certify, extraData — наш nonce в шестнадцатеричном виде, а name — хеш открытой области (public area) заверенного ключа и его атрибутов: он совпадает с именем ключа подписи. Почему этому можно доверять? Потому что ограниченный (restricted) ключ отказывается подписывать данные, которые начинаются с магического числа TPM_GENERATED, если только TPM не создал эти данные сам:

Тот же ключ аттестации: обычные данные, затем поддельная аттестация (изменён один байт)
ordinary data (msg.txt): signed by the attestation key
ERROR: Esys_Sign(0x3E0) - tpm:parameter(3):invalid ticket

В реальном развёртывании цепочка продолжается: AK привязан к ключу подтверждения (endorsement key, EK) этого TPM, производитель TPM выпустил для EK сертификат, а проверяющая сторона проверяет всю цепочку; так работает формат заявления об аттестации tpm в WebAuthn. У программного TPM такого сертификата нет, и именно поэтому его аттестации нельзя доверять.

YubiKey PIV и аттестация

Апплет PIV в YubiKey хранит ключи в слотах (9a — аутентификация, 9c — подпись, 9d — управление ключами, 9e — аутентификация карты) с политикой PIN и политикой касания, которые задаются при генерации ключа и больше не меняются. Аттестация PIV показывает, «что определённый асимметричный ключ был сгенерирован на устройстве, а не импортирован»: YubiKey подписывает сертификат для ключа своим ключом аттестации из слота f9, а у этого ключа есть сертификат от центра сертификации (CA) Yubico для PIV. Команды для yubico-piv-tool 2.7.3 (токена в лаборатории у нас нет, поэтому проверялся только вывод --help):

yubico-piv-tool (не запускался на токене)
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 проводит ротацию корневого CA: «YubiKey с прошивкой версии 5.7.4 и новее будут подписываться нашим новым корневым CA», поэтому проверяющим сторонам нужны оба корневых сертификата. CA/Browser Forum принимает именно такое доказательство для ключей подписи кода (Authenticode), а OpenSSH использует те же токены, но через FIDO2 (ssh-keygen).

Чего аппаратные ключи не дают

  • Защита от кода, запущенного от имени пользователя. Вредоносная программа на устройстве может попросить оборудование что-нибудь подписать; она лишь не может унести ключ с собой. Для важных ключей требуйте присутствия пользователя (касание), проверки пользователя (PIN, биометрия) или подтверждения.
  • Доступность. Потерянное, стёртое или сломанное устройство забирает ключ с собой, а очистка TPM (её выполняют некоторые обновления прошивки и переустановки системы) делает недействительными все ключи под ним. Зарегистрируйте как минимум два аутентификатора, предусмотрите способ восстановления доступа и спланируйте ротацию.
  • Гибкость в выборе алгоритмов. Доступно только то, что реализовано в чипе. Многие платформы по-прежнему предлагают лишь P-256 и RSA; если протоколу нужен Ed25519 или secp256k1, проверьте поддержку, прежде чем строить архитектуру вокруг оборудования.
  • Доверие к тому, что подписывает оборудование. Защищённый ключ подписывает всё, что ему передаёт операционная система; экран с надписью «Подтвердить платёж» принадлежит той же, возможно скомпрометированной, ОС. Именно поэтому существуют аппаратные кошельки с собственным экраном.
  • Доказательство без аттестации. Если ваш сервер не проверяет цепочку аттестации, «ключ хранится в оборудовании» — всего лишь слова клиента.

Чек-лист для работы с аппаратными ключами

  1. Генерируйте ключи на устройстве; никогда не импортируйте ключ, который хотите считать аппаратным.
  2. Выберите нужный уровень: Secure Enclave или TEE для большинства приложений, StrongBox или защищённый элемент для особо ценных ключей, HSM для серверных ключей.
  3. Привязывайте использование ключа к пользователю (биометрия или PIN при каждом использовании), если подпись что-то авторизует.
  4. Проверяйте аттестацию на сервере: корневые сертификаты (и текущие, и новые после ротации), отзыв, nonce, уровень безопасности, свойства ключа.
  5. Храните только открытые ключи и непрозрачные дескрипторы; будьте готовы к тому, что ключи пропадут, и поддерживайте повторную регистрацию.

Источники