本文へスキップ
ecdsa.com
← すべての記事

ハードウェア保護の鍵:Secure Enclave、StrongBox、TPM、PIV

この Mac の Secure Enclave ではポスト量子の鍵も含めて、ソフトウェア TPM ではアテステーション付きで実際に鍵を作成し、Android と Yubico のアテステーションの規則を読み解きました。どれにも共通する約束は、範囲こそ狭いものの価値があります。鍵をコピーできない、ということです。

要点:ハードウェアで保護された鍵は、独立したチップや分離されたプロセッサーの内部で生成され、平文のままそこから出ることはありません。コードが持つのはハンドルだけで、署名はハードウェアに依頼します。これによって 1 つのことが確実に保証されます。秘密鍵をデバイスからコピーできないため、盗まれたディスクイメージ、バックアップ、メモリダンプは役に立ちません。一方で、同じデバイス上のマルウェアがハードウェアに署名を依頼することは、使用のたびにユーザーの存在確認か生体認証を必須にしない限り、防げません。対応するアルゴリズムは限られます(P-256 はどこでも、RSA は一部で使え、ポスト量子の ML-DSA と ML-KEM は Apple の Secure Enclave でバージョン 26 のリリース以降に使えます)。アテステーションを使うと、メーカーにつながる証明書チェーンによって、鍵が本当にそうしたハードウェア内にあることをサーバーで検証できます。Android なら Google のルート、YubiKey なら Yubico のルート、TPM なら TPM ベンダーのルートにつながります。デバイスを失えば鍵も失われるため、複数の鍵の登録と鍵のローテーションを最初から設計に組み込んでください。

各プラットフォーム

鍵の格納場所主なアルゴリズムアテステーションユーザーの確認手段
Apple の Secure EnclaveApple シリコンに内蔵された専用コプロセッサーP-256、ML-KEM-768/1024 と ML-DSA-65/87(CryptoKit、OS 26 以降)iOS では App Attest、任意の鍵にはなしアクセス制御による Touch ID、Face ID、パスコード
Android Keystore、TEEメイン SoC の信頼できる実行環境RSA、EC、AES、HMACGoogle のルートにつながる鍵のアテステーション画面ロックの認証情報または生体認証(使用ごと、または一定時間ごと)
Android StrongBox組み込みのセキュアエレメントまたは統合型セキュアエンクレーブ(Android 9 以降)RSA 2048、P-256、AES、HMAC-SHA256、3DES同上(セキュリティレベルは StrongBox)同上
TPM 2.0独立したチップ、ファームウェア TPM、または仮想 TPMRSA 2048 以上、P-256、P-384(ベンダーによる)EK が証明した AK、ベンダー発行の 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

パスキーは、これらすべての最も一般的な用途です。プラットフォーム認証器とは、上の表の最初の 4 行のいずれかに WebAuthn のインターフェースを備えたものです(開発者のためのパスキー)。

Apple の Secure Enclave をこの Mac で試す

Secure Enclave は、Apple の分離された鍵処理用プロセッサーです。CryptoKit を使えば、鍵は 1 行で作成できます。以下は、Apple M4 Max、macOS 27.0.1、Swift 6.4 の環境で、特別なエンタイトルメントのない通常のコマンドラインツールとして実行したものです。

main.swift:Secure Enclave の P-256 鍵とソフトウェア鍵の比較
// 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)

284 バイトの dataRepresentation は、後で鍵を再び使うためにアプリが(キーチェーンやファイルに)保存するデータです。Secure Enclave によってラップされており、使えるのはそのデバイスの Secure Enclave だけです。秘密スカラーを要求する手段はそもそも存在せず、要求しようとするコードはコンパイルできません。

Secure Enclave の鍵から key.rawRepresentation を読み取るファイルを swiftc でコンパイル
export.swift:5:11: error: value of type 'SecureEnclave.P256.Signing.PrivateKey' has no member 'rawRepresentation'

Security フレームワークの SecKey API 向けに書かれた Apple のガイド『Secure Enclave による鍵の保護』には、今も「NIST P-256 の楕円曲線鍵でのみ動作する」と書かれています。CryptoKit はさらに先へ進んでおり、iOS と macOS のバージョン 26 からは SecureEnclave.MLDSA65、MLDSA87、MLKEM768、MLKEM1024 を使えます。この 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 で生成した鍵は「エクスポート不可」のままです。「鍵マテリアルがアプリのプロセスに入ることはなく」、デバイスのセキュアハードウェアが鍵のアルゴリズムとパラメーターに対応している場合は「鍵マテリアルがセキュアハードウェアの外部に公開されることはありません」。ハードウェアのレベルは 2 つあります。

  • TEE(KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT):メインプロセッサー上の分離された環境です。
  • StrongBox(Android 9 以降、setIsStrongBoxBacked(true)):「TEE と比べて強力な分離と耐タンパー性を備えた、組み込み SE または統合型セキュアエンクレーブ(iSE)」で、独自の CPU、ストレージ、乱数生成器、セキュアタイマーを持ちます。対応するのは「RSA 2048、AES 128 と 256、ECDSA、ECDH P-256、HMAC-SHA256」とトリプル DES だけで、Google は StrongBox について「低速で、リソースの制約も大きい」と注意を促し、「ほとんどのアプリでは StrongBox は必要ありません」としています。対応していない要求を行うと StrongBoxUnavailableException がスローされます。

鍵が実際にどこに格納されたかは KeyInfo.getSecurityLevel()(API 31 以降。それより前のバージョンでは isInsideSecureHardware())で確認し、setUserAuthenticationParameters() で鍵の使用をユーザーに結び付けます。認証は操作ごとに求めることも、ロック解除後の一定時間だけ有効にすることもできます。同じページには注意点が 1 つあります。ハードウェアは「一般に独立した安全なリアルタイムクロックを持たないため、時間的な有効期間に関する認可を通常は適用しません」。

鍵のアテステーションによって、これをサーバーに証明できます。デバイスは鍵の証明書チェーンを返し、そのうちアテステーション拡張(OID 1.3.6.1.4.1.11129.2.1.17)を含む最初の証明書に、鍵とそのプロパティ、セキュリティレベルが記述されています。鍵のアテステーションのドキュメントにある規則のうち、自分の検証コードに取り入れるべきものは次のとおりです。

  • 検証はサーバーで行い、デバイス自体では決して行わないでください。
  • ルートは、android.googleapis.com/attestation/root で公開されている Google のアテステーションルートのいずれかでなければなりません。ECDSA P-384 のルート「Key Attestation CA1」が、従来からある RSA のルートと並んで「2026年2月1日からアテステーション証明書チェーンへの署名を開始」するので、両方を受け入れてください。
  • すべての証明書を、android.googleapis.com/attestation/status にある Google の失効リストと照合してください。
  • 「チェーン内で信頼できるのは、拡張が最初に現れたものだけ」です。それより後のものは、チェーンを延長した攻撃者によるものかもしれません。
  • attestationSecurityLevel が TrustedEnvironment または StrongBox であることを必須にしてください。それ以外のルートにつながるチェーンや、ソフトウェアによるアテステーションは、ハードウェアについて何も示しません。

Google は、独自の検証コードではなく、同社の Kotlin 製検証ライブラリを使うことを推奨しています。Pixel のカメラによる C2PA 署名は、この仕組みの上に構築された例です。クレームの署名鍵は「Titan M2 セキュリティチップ内の Android StrongBox を使って生成・保管」され、証明書は鍵のアテステーションを経た後でなければ発行されません(C2PA コンテンツ認証情報)。

TPM 2.0:ソフトウェア TPM で見る鍵、ポリシー、アテステーション

TPM は、PC やサーバーにおける同様の仕組みです。ハードウェアなしでその仕組みを示すため、実験では debian:forky-20261005-slim 上で swtpm 0.10.2(ソフトウェア TPM なので、構造上何も保護しません)と tpm2-tools 5.8 を使いました。署名鍵は、この 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 は親の下で暗号化された鍵で、ディスクに保存しても安全です。署名は DER 形式の通常の ECDSA 署名で、エクスポートした公開鍵を使って OpenSSL で検証しています。

アテステーションには、restricted 属性を持つ別のアテステーション鍵(AK)を使います。tpm2_certify は、検証者の nonce を含む、別の鍵についてのステートメントに AK で署名させます。

lab.sh(抜粋):restricted 属性のアテステーション鍵で署名鍵を証明
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 の 16 進表記で、name は証明された鍵の公開領域と属性のハッシュ値であり、署名鍵と一致しています。なぜこれを信頼できるのでしょうか。restricted 属性の鍵は、TPM_GENERATED のマジック値で始まるデータについては、TPM 自身が生成したものでない限り署名を拒否するからです。

同じアテステーション鍵で、通常のデータ、次に偽造したアテステーション(1 バイト変更)に署名
ordinary data (msg.txt): signed by the attestation key
ERROR: Esys_Sign(0x3E0) - tpm:parameter(3):invalid ticket

実際の運用環境では、チェーンがさらに続きます。AK は TPM のエンドースメント鍵に結び付けられ、その証明書は 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(トークンに対しては未実行)
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 のルートに至るチェーンを確認し、そのうえでポリシーを読み取ります。タッチポリシー「always」は、すべての署名で人が物理的にその場にいたことを意味します。Yubico はルートを切り替えつつあり、「ファームウェアバージョン 5.7.4 以降の YubiKey は新しいルート CA で署名される」ため、検証者には両方のルートが必要です。CA/Browser Forum は、コード署名鍵についてまさにこの種の証明を認めています(Authenticode)。一方、OpenSSH は同じトークンを PIV ではなく FIDO2 経由で使います(ssh-keygen)。

ハードウェア鍵では得られないもの

  • ユーザーとして動作するコードからの保護:デバイス上のマルウェアはハードウェアに署名を依頼できます。できないのは鍵を持ち出すことだけです。重要な鍵には、ユーザー存在確認(タッチ)、ユーザー検証(PIN、生体認証)、または確認操作を必須にしてください。
  • 可用性:デバイスを紛失したり、初期化したり、壊したりすると鍵も失われます。また、TPM をクリアすると(ファームウェアの更新や再インストールでクリアされる場合があります)、その下にある鍵は無効になります。認証器を少なくとも 2 つ登録し、リカバリ手段を確保し、ローテーションを計画してください。
  • アルゴリズムアジリティ:使えるのはチップが実装しているものだけです。多くのプラットフォームは今も P-256 と RSA しか提供していません。プロトコルで Ed25519 や secp256k1 が必要なら、ハードウェアを前提に設計する前に確認してください。
  • ハードウェアが署名する内容の信頼性:安全な鍵は、OS が渡したものに何でも署名します。「支払いを承認」と表示する画面も、乗っ取られているかもしれない同じ OS の一部です。独自の画面を持つハードウェアウォレットが存在するのはこのためです。
  • アテステーションなしの証明:サーバーがアテステーションのチェーンを検証しないなら、「鍵はハードウェア内にある」というのはクライアントの自己申告にすぎません。

ハードウェア鍵を使うときのチェックリスト

  1. 鍵はデバイス上で生成してください。ハードウェアで保護されていると呼びたい鍵を、インポートしてはいけません。
  2. 必要なレベルを選んでください。ほとんどのアプリには Secure Enclave か TEE、価値の高い鍵には StrongBox かセキュアエレメント、サーバーの鍵には HSM が適しています。
  3. 署名が何かを認可する場面では、鍵の使用をユーザーに結び付けてください(使用ごとに生体認証または PIN)。
  4. アテステーションはサーバーで検証してください。確認するのは、ルート(現行のものと切り替え後のもの)、失効、nonce、セキュリティレベル、鍵のプロパティです。
  5. 保存するのは公開鍵と不透明なハンドルだけにしてください。鍵は消えるものと想定し、再登録に対応してください。

出典