TPM 2.0に準拠したデバイス(又はエミュレータ)をコマンドライン上で操作するためのツール rust-tpm2-cli を開発していることを以前の記事で述べた.
この開発のためにTPM 2.0の仕様を色々と調べていたのだが,かなり奇妙な1鍵の取り回しをしていることが分かったので,ここに記録しておく.
⚠️ 記事中のTPMコマンドを試す場合,決して実TPMを使ってはいけない.必ずテスト専用のTPMエミュレータを立てて,TCTIを当該TPMエミュレータに設定すること.これらの言葉の意味が分からない場合,ツールの使用は中止することを推奨する.いずれにせよ何かが起きても私は責任を負わない.
Background
上記記事を読んでもらえば分かるが,少なくともTPMの文脈では最上位のレイヤーであるCLI(Command-line Interface)でさえ,コード規模は数万行になる.これは,TPM 2.0が複雑であるというよりかは(実際複雑ではあるのだが),搭載している機能がかなり多いことに因るものである.
手抜きのため rust-tpm2-cli に於いては,もちろんTCGの仕様書は参照するが,Claude Opusをぶん回しながら開発することとした.あくまで最上位のレイヤーであるので,FAPI/ESAPIのレイヤーさえ正しく把握していれば,そう変なバグの混入は起こらないだろうという甘い期待である.
とはいえ,私的活動(数学研究,OSS開発)でも業務(TEE研究開発)でも,複数のAIモデルを使っていて散々思い知らされたように,彼らは(ソフトウェア業界全体の文脈で)低レイヤーのニッチな領域では素晴らしいハルシネーションを披露してくれる.そのため,機能実装はLLMに全面的に依存しつつも,品質保証のために分厚いtest suiteを用意してバグ検出精度を高めたかった.もちろん,プログラム抽出・形式検証でもしているのでない限りは,AIを使おうが使うまいが,test suiteは用意すべきであるが.
当たり前だがテストを実施するためにはTPMが必要である.実TPMをテストに使うとシステムがぶっ壊れるおそれがあるので,各テスト毎にTPMエミュレータを作成する設計とした.TPMはステートフルなデバイスなので,単一のTPMを全てのテストで使い回すのは望ましくないからである.またこうすることによりテストを並列実行できるというメリットもある.
TPMエミュレータには swtpm を使った.当初はエミュレータとのローカル通信にはTCPを使っていたのだが,並列実行されるテストへのポート割り当て管理が著しく面倒(ポート枯渇やTOCTOUなど)であったため,途中でUnix Domain Socketに切り替えるなど細かい工夫を色々とやっていた.
統合テスト(及びサンプルコード)の一項目としてTPM Attestationがある.TPMに署名用秘密鍵を発行させ,PCR selectorとnonceをもとにQuoteを発行させ,AK公開鍵を用いて検証し,……といった一連の流れをテストするわけである.とりあえずQuoteの署名検証までは作った.しかしその後を実装しようとして気付いた.そもそもTPM 2.0におけるトラストチェーンの全貌を把握していないことに2.
Attestation
概要
Attestation(構成証明)とは,システム(Attester)が自らのシステム構成情報を証明(attest)し,かつそれを検証(verify)できるようにするプロトコルをいう3.
例えば,Confidential Computingの文脈であれば,Confidential Compute Systemが真に隔離環境(TEE)内で動作していること,TCB(Trusted Compute Base)バージョン番号,SVN(Security Version Number),各種脆弱性の対応状況(mitigation status),TEE内で動作するコードの測定値(ハッシュ値)等の事項を,リモートピアに対して証明することが不可欠である.リポートピアはこれら証明事項をもとに当該システムが信頼に足る存在であるかを査定(appraise)する.
Attestation Report
Attestationの際に,証明者(Attester)が検証者(Verifier)に対して提示する証拠(Evidence)に相当するものをAttestation Report(構成証明レポート)と呼ぶ.Attestation Reportは真正性を暗号的に検証可能な形で証明事項が記載されたデータとして実現される.
当然,次のようなトラストチェーンを考えるのが自然であろう:
すなわち,信頼の基点となるルートCAまで署名によって辿れるような署名用秘密鍵を用意し,この鍵を用いて証明事項に署名したものをAttestation Reportとし,Verifierは証明書をEndorser(チップベンダ等)から取ってきて検証すればよい.
Attestation Key
Attestation Reportの署名用秘密鍵(Attestation Keyと呼ばれる)は,無闇矢鱈とどこにでも顔を出すのではなく,(Verifierにとって)信頼できる領域の中に留まるように設計されなけばならない.
AMD SEV-SNPにおいてAttestation Keyに相当する鍵はVEK(Versioned Endorsement Key)である.VEK証明書はAMD Key Distribution Service(VCEKの場合)又はクラウドベンダ(VLEKの場合)により配布される.一方でVEK秘密鍵はAMD Secure Processor(ASP)というコプロセッサの中に留まる.
TPM 2.0におけるAttestation
前の記事でも述べた通り,AMD SEV-SNPやIntel TDXなどの主要なTEEと同じく,TPM 2.0もまたAttestation機能を備えている.もちろん脅威モデルや証明事項は著しく異なるのであるが.
TPM Quote
TPMではQuoteと呼ばれるデータがAttestation Reportの役割を担う.TPM QuoteはTPMが保持するPCRの値をattestするための証拠として用いられる.
Platform Configuration Register
PCR(Platform Configuration Register)はハッシュチェーンを保持するためのTPM内のレジスタであり,典型的には24個のPCRが存在する.メジャードブートにおける測定値や整合性ログなどのシステム構成情報を,それぞれ割り当てられたPCRにextend(ハッシュチェーンに追加すること)することで記録する4.
$$ \mathrm{PCR}[i] \leftarrow \mathrm{Hash}(\mathrm{PCR}[i] \parallel \mathrm{Hash}(\mathrm{data}) ).$$
各PCRに測定・記録されるデータは以下の通り:
| PCR | What's measured |
|---|---|
| 0 | BIOS |
| 1 | BIOS configs |
| 2 | Option ROMs |
| 3 | Option ROM configs |
| 4 | MBR (Master Boot Record) |
| ..7 | Boot time measurements |
| 8..15 | Reserved by Static OS Linux Kernel uses PCR 10 for extending IMA eventlogs |
| 16 | Debug |
| 23 | Application |
例えば,PCR0–7をバインドしたTPM Quoteを発行すれば,正しくメジャードブートされたことを,Verifierに対して証明できる.もちろん,そのためにはPCR0–7は不変でなければならないので,PCR0–7はユーザランドからは自由にextend/resetできないようアクセス制御されている.対照的に,PCR16やPCR23はユーザランドから自由に触ることができる.
Attestation KeyとEndorsement Key
TPM 2.0においては,Endorsement Key(EK)と呼ばれる秘密鍵を2つ(RSA及びECC),外部から取り出せない状態で保持している.とはいえ,EKはチップに固有かつ一意的に割り当てられた鍵なので,これ自体をAttestation Reportの署名に使うのは,あまり行儀が良くない.代わりに,EKをもとにしてAttestation Key(AK)を生成・保持し,AKを用いてTPM Quote(Attestation Reportに相当)を署名する.
AKはEKと同じTPM内にて生成されたことが暗号的に証明できる.AKは次の属性を持つことが仕様上MUSTである5.
| Attribute | AK | Note |
|---|---|---|
restricted |
1 |
TPMが生成したデータに対してのみ使用可能 |
signing |
1 |
署名用途は可 |
decrypt |
0 |
復号用途は不可 |
fixedtpm |
1 |
TPMからの取り出し・移植(migration)及び複製(duplication)は禁止 |
属性 restected: 1 は分かりやすい.もしAKが任意の情報源に対して署名を打てるとしたら,偽Quoteをいくらでも発行できてしまう.属性 fixedtpm: 1 も同様である.もしAKをマイグレーションできるなら「そのTPMデバイスに固有の鍵による署名」であることが保証できなくなる.属性 decrypt: 0 は別にそうでなければならない強い理由はなさそうだが,AKはあくまでAttestationのための署名鍵としての役割なので,用途を縛っておけば攻撃面が減るなどメリットはあるだろう.
AKとEKの紐付きが「暗号的に証明できる」というのが曲者である.AKからEKまで署名で辿れるのだろうと考えるのが自然だが,実はAKは署名されておらず,EKで署名されたAK証明書が存在するわけではない.
AKをEKで署名しないのは何故か
公開鍵署名の安全性を前提とすれば,EK秘密鍵による署名を公開したところで,EK秘密鍵が侵害されるわけではない.AMD SEV-SNPがやっているように,EKでAKに署名をして証明書を発行すれば十分である.
EKによる署名がなされないことには暗号学的な安全性以外の理由がある.それはデバイスの一意的な識別を不可能にすることである.以下は TCG EK Credential Profile for TPM Family 2.0 (Version 2.7) 6 からの引用である:
The TPMT_PUBLIC structure includes the base attributes restricted, sign and decrypt that determine the cryptographic operation a key may perform on an object. The TPM 2.0 Library Specification [1] does not impose any restrictions regarding the attributes of the Endorsement Key. As any other key the Endorsement Key can be created as a decryption or signing key.
However, the EK and its credential may be considered privacy-sensitive if the private part of the EK is used in a cryptographic protocol. In this case, the public EK or the EK Certificate could represent a privacy-sensitive cryptographic identifier for a particular platform. In privacy-sensitive environments, the EK should not be a signing key and restricted to specific operations (this is described in more detail in section 2.3 Privacy Protection).
例えば,TPMを備えたクラウド計算環境に対して,ユーザがAttestationを実施するという状況では,TPMが一意識別されたところで大して問題ではないだろう.実際,AMD SEV-SNPのAttestation ReportにはCHIP_ID(CPUID)が含まれることから,Verifierにはデバイスの一意的な識別を可能とする情報が渡ることになる7.これはクライアントがサーバを検証するという構図である.
一方,TPMをDRM(Digital Rights Management)やオンラインゲームのチート対策などのために用いる場合,サーバがクライアントを検証する形となる.このケースでは,TPMはクライアントのパーソナルデバイス内にあるので,EKによる署名はサービス横断的なデバイス識別子として機能し,プライバシー上の問題となりうる.
| Platform Key | Signing | Identity | Privacy |
|---|---|---|---|
| TPM 2.0 EK | SHALL NOT | per-TPM | Credential Protection |
| AMD SEV-SNP VCEK | CAN | per-CPU | None |
| AMD SEV-SNP VLEK | CAN | per-CSP | CSP単位k-匿名化 |
Credential Activation
AKがEKで署名されているわけではないことを前節で述べた.それではどうやってEKとAKを紐付けるのであろうか?これを署名を使わずに実現するための機構がCredential Activationである589.超雑に言えばAKをEndorseする第三者との間でchallenge-responseしてZKPするだけである.
EKのデフォルト属性
前節で述べた通り,EKを署名に使うことは TPM 2.0 Library Specification では禁止されていない.しかしプライバシー配慮から,EK Credential ProfileのデフォルトテンプレートではEKの属性を restricted: 1, decrypt: 1, sign: 0, fixedtpm: 1 と定めている.すなわち,デフォルトに準拠する限り,EKは復号専用鍵であり,署名には使えない.
そこで,EKの復号鍵としての能力を利用して,署名に基づく証明書チェーンの代わりに,暗号化に基づくchallenge-responseを用いることにより,「AKが同一TPM内に存在する」ことを暗号的に証明する.
プロトコルの参加者
プロトコルには以下のエージェントが参加する:
- Attester: TPMを搭載したデバイス.EKとAKを保持する
- Privacy CA (Attestation CA): AK証明書を発行する認証局.EK証明書の検証と,AK–EKの紐付け検証を担う
プロトコルフロー
プロトコルの骨格は次の通りである:
- Privacy CAがEK公開鍵でsecret(nonce)を暗号化し,同時にAKの暗号的識別子(Name)にバインドする(
MakeCredential) - Attester側のTPMがEK秘密鍵で復号し,かつAK Nameが一致することを内部検証する(
ActivateCredential) - 復号されたsecretをPrivacy CAに返送し,Privacy CAが一致を確認する
ActivateCredential が成功するためには,「EK秘密鍵を持つこと」と「当該AKが同一TPM内に存在すること」の2条件を同時に満たさなければならない.この2つの条件が暗号的に結合されているため,片方だけでは突破できない.
AK Name: AKオブジェクトの暗号的識別子
TPM 2.0におけるNameとは,TPM 2.0 Library Specification Part 1 8で定義される,オブジェクトの暗号的な一意識別子である.とくに,当該プロトコルにおけるAK Nameは,AKの暗号的な一意識別子であって,次のように計算される:
$$\mathrm{Name} = \mathrm{nameAlg_{ID}} \parallel H_{\mathrm{nameAlg}}(\mathrm{TPMT\_PUBLIC})$$
ここで $\mathrm{TPMT\_PUBLIC}$ は TPM 2.0 Library Specification Part 2 10で定義される,オブジェクトの公開領域全体のマーシャル済みバイト列であり,restricted, sign, fixedtpm などのオブジェクト属性,署名スキーム,Authポリシーなどを含む.
AdversaryがAKの属性を改変(例えば restricted を除去して任意データへの署名を許可)した場合,AK Nameが変化するため,Credential Activationは失敗する.
MakeCredential: Nameバインディング
TPM2_MakeCredential は TPM 2.0 Library Specification Part 3 11で定義されるコマンドである.入力は全て公開情報(EK公開鍵,AK Name,secret)であるため,TPM上で実行する必要はなく,Privacy CAがソフトウェアで同等の処理を実行して構わない.
処理の概要を疑似コードで示す:
def MakeCredential(EK_pub, secret, AK_Name):
nameAlg = EK_pub.nameAlg # e.g., SHA-256
# 1. ランダムなseedを生成し,EK公開鍵で暗号化
seed = random_bytes(digest_size(nameAlg))
encryptedSecret = RSA_OAEP(EK_pub, seed, label="IDENTITY\0")
# 2. seedとAK Nameから対称暗号鍵を導出
# AK NameがKDFの入力に入る = Nameバインディング
symKey = KDFa(nameAlg, seed, "STORAGE", AK_Name, "", 128)
hmacKey = KDFa(nameAlg, seed, "INTEGRITY", "", "", 256)
# 3. secretを対称暗号化し,HMACで認証
encIdentity = AES_CFB(symKey, secret)
integrityHMAC = HMAC(hmacKey, encIdentity || AK_Name)
credentialBlob = integrityHMAC || encIdentity
return (credentialBlob, encryptedSecret)
ここで KDFa はNIST SP 800-10812において,PRF(疑似乱数関数)としてHMACを用いる,カウンタモードの鍵導出関数(Key Delivarion Function)である8.
手順2において対称暗号鍵 symKey の導出に AK_Name が入力として使われることが重要である.これにより,credentialBlobは特定のAK Nameに対してのみ復号可能となる.別のAKに対して同じcredentialBlobを流用しても,Nameが異なるため導出される鍵も変わり,復号は失敗する.
ActivateCredential: TPM内部での復号と検証
TPM2_ActivateCredential は TPM 2.0 Library Specification Part 3 11で定義されるコマンドである.EK秘密鍵を使うため,必ずTPM内部で実行される.
def ActivateCredential(AK_handle, EK_handle, credentialBlob, encryptedSecret):
# 1. EK秘密鍵でseedを復号
seed = RSA_OAEP_Decrypt(EK_privkey, encryptedSecret, label="IDENTITY\0")
# 2. AKのNameをTPM内部で再計算
# ※ 外部から供給されたNameは信頼しない
AK_Name = AK_handle.nameAlg || Hash(AK_handle.TPMT_PUBLIC)
# 3. 鍵導出(MakeCredentialと同じ計算)
symKey = KDFa(nameAlg, seed, "STORAGE", AK_Name, "", 128)
hmacKey = KDFa(nameAlg, seed, "INTEGRITY", "", "", 256)
# 4. HMAC検証 → 失敗すれば TPM_RC_INTEGRITY
verify_HMAC(hmacKey, encIdentity || AK_Name)
# 5. 対称復号 → secretを返す
secret = AES_CFB_Decrypt(symKey, encIdentity)
return secret
TPMは AK_handle として渡されたオブジェクトの公開領域から自分でNameを計算する.MakeCredential 時にPrivacy CAが指定したNameと,ActivateCredential 時にTPMが内部計算したNameとが一致しなければ,KDFaから導出される symKey が異なり,HMAC検証が TPM_RC_INTEGRITY で失敗する.
なぜこれが紐付け証明になるのか
このプロトコルが「EKとAKが同一TPM内に存在する」ことの証明として機能するのは何故であろうか?
ActivateCredential が成功してsecretが正しく返送される条件は,以下の2つが同時に成り立つとき,かつそのときに限る:
-
EK秘密鍵の所有:
encryptedSecretはEK公開鍵でRSA-OAEP暗号化されたseedであり,これを復号できるのはEK秘密鍵の所有者のみ.EKはfixedtpm: 1なのでTPM外に出ることはなく,復号能力は当該TPMに限定される -
AKの同一TPM上での存在:
symKeyの導出にAK Nameが組み込まれており,かつTPMはNameを外部入力ではなく内部計算で求める.したがって,MakeCredential時に指定されたNameと同一のNameを持つAKが,EK秘密鍵を持つTPM上にロードされていなければならない- ここでTPMにおいて
ActivateCredentialが正しく実装されていることへの信頼が前提されていることに注意
- ここでTPMにおいて
図1. T(Φ) if and only if Φ
署名ベースのトラストチェーンと比較すると,保証の性質が微妙に異なる.署名チェーンは「ある鍵が別の鍵を証明した」という事実を証明するが,Credential Activationは「2つの鍵が同一のハードウェアセキュリティ境界内に共存する」ことを証明する.後者はTPMのようなハードウェアRoot of Trustに固有の保証であり,ソフトウェアPKIでは実現困難な性質である.
このプロトコルがAKとEKの紐付けとして機能するためにはPricacy CAへの信頼が必須である.
CLIでの実行例
Credential Activation
rust-tpm2-cli で上記のプロトコルを実行するには次のようにする13:
# Attester: Generate EK
tpm2 createek -G rsa -c ek.ctx -u ek_pub.bin
# Attester: Generate AK
tpm2 createak -C file:ek.ctx -c ak.ctx -G rsa -g sha256 \
-u ak_pub.bin -r ak_priv.bin -n ak_name.bin
# Privacy CA: MakeCredential
openssl rand -out secret.bin 32
tpm2 makecredential -u ek_pub.bin -s secret.bin -n ak_name.bin -o cred_blob.bin
# Attester: ActivateCredential
tpm2 activatecredential -c file:ak.ctx -C file:ek.ctx -i cred_blob.bin -o certinfo.bin
# Privacy CA: Compare two secrets
diff secret.bin certinfo.bin
# => Issue the AK certificate
tpm2 makecredential は公開情報のみで動作するため,Privacy CA側では必ずしもTPMを必要としない.一方 tpm2 activatecredential はEK秘密鍵による復号を伴うため,TPM上でしか実行できない.
TPM Attestation
TPM Quoteの発行及び検証は以下のようにやればよい14:
# Create and persist an Endorsement Key (EK)
tpm2 createek -c ek.ctx -G ecc -u ek.pub
tpm2 evictcontrol 0x81010001 -C o -c file:ek.ctx
# Create and persist an Attestation Key (AK)
tpm2 createak -C file:ek.ctx -c ak.ctx -G ecc -u ak.pub
tpm2 evictcontrol 0x81000002 -C o -c file:ak.ctx
# Generate a nonce for freshness
tpm2 getrandom 32 -o nonce.bin
# Quote PCRs 0–7 signed by the AK
tpm2 quote -c hex:0x81000002 -l sha256:0,1,2,3,4,5,6,7 \
-q file:nonce.bin \
-m quote.bin -s sig.bin -o pcrs.bin
# Verify the quote
tpm2 checkquote -u hex:0x81000002 -m quote.bin -s sig.bin \
-l sha256:0,1,2,3,4,5,6,7 \
-q file:nonce.bin \
-f pcrs.bin
# Verify quote signature only
tpm2 verifysignature -c file:ak.ctx -g sha256 -m quote.bin -s sig.bin
ここでは検証に tpm2 コマンドを用いているが,やっていることはただの署名検証なので,TPM外で実行して構わない.
-
なお,これが奇妙だと感じたのは,私がAMD SEV-SNPからTEEに入ったConfidential Computingの民であり,それ以外の文脈でのトラストチェーンの作り方をよく知らないからであるかもしれない. ↩
-
TEEと組み合わせる形でのvTPMのトラストチェーンしか知らなかったため.TEE×TPMにおけるトラストチェーンの例は Azure Confidential VM guest attestation design detail を参照されたい.このアーキテクチャではAK証明書よりも手前の部分は知る必要がない.何故なら,vTPMの背後にあるのはTPMエミュレータであり,これはCPU Attestationによって縛られているからである.それに,TPMエミュレータはHardware Root of Trustとしては機能しえないので,AKの手前(つまりEK)を検証したところで無意味である. ↩
-
RFC 9334: Remote ATtestation procedureS (RATS) Architecture ↩
-
AMD SEV-SNPにおいても
MASK_CHIP_IDを使うことでCHIP_IDを隠すことは可能である.ただし,VCEK署名鍵に対応するVCEK証明書をAMD KDSから取ってくるためには,CHIP_IDの情報が必須である.さらにはVCEK証明書の中には拡張属性としてCHIP_ID及びTCBバージョンの情報が含まれている.したがって,プロセッサ固有のVCEK(Versioned Chip Endorsement Key)を署名鍵として利用する限り,Attestation Reportの検証が不可能となってしまう.そこで代わりに用いるのがVLEK(Versioned Loaded Endorsement Key)である15.クラウドサービス事業者(CSP)は,KDSに対して,CHIP_IDとTCBバージョンをもとに,VLEK hashstickの発行を依頼する.KDSはCSPに割り当てられたシード値をもとにVLEK hashstickを計算するとともに,チップ固有のシークレットから導出されたトランスポート鍵でラップしてCPSに返送する.CSPはホストマシン上でSNP_VLEK_LOADを用いてwrapped VLEK hashstickをASPにロードする.ASPはこれをunwrapし,以降はVCEKの代わりにVLEKを署名に用いる. ↩ -
Trusted Platform Module 2.0 Library Part 1: Architecture ↩ ↩2 ↩3
-
NIST SP 800-108 Rev. 1: Recommendation for Key Derivation Using Pseudorandom Functions ↩
-
rust-tpm2-cli/tests/ek_ak.rsの
makecredential_and_activatecredential_roundtrip()テストのShellへの翻訳 ↩ -
rust-tpm2-cli/README.mdのサンプルコードより転載 ↩