HN 日本語サマリー

← 一覧へ戻る
セキュリティ

TPM内でTLSハンドシェイクに署名する

Signing TLS handshakes inside a TPM (bschaatsbergen.com)

31 pointsby bschaatsbergen39 コメント

要約

この記事は、AI企業によるウェブサイトのスクレイピングからサーバーを保護するためにAnubisというシステムが導入された経緯について説明しています。Anubisは、電子メールスパム削減のためのProof-of-Work(PoW)スキームに似た手法を用いており、大量のスクレイパーによるアクセスをコスト高くすることで、ウェブサイトのダウンタイムを防ぐことを目的としています。将来的には、ヘッドレスブラウザのフィンガープリントを特定し、正当なユーザーにはPoWチャレンジを表示しないようにすることを目指しています。

全文翻訳

TPM内でTLSハンドシェイクに署名する

私は機密性の高いVM(Confidential VM)でリモートアテステーションの作業を行っています。この記事はその一環です。アテステーションがどのような形になるにせよ、マシンはその後も保持して使用できるIDを必要とします。私の場合は、相互TLS(Mutual TLS)で他のサービスに認証する必要があります。

相互TLSを行うアプリケーションは、クライアント証明書で認証します。ディスク上では、それはほぼ常に2つのファイルになります。client.crtは公開するもので、公開鍵と、その鍵に対するCAの署名を含んでおり、秘密ではありません。client.keyはハンドシェイクに署名するために使用するもので、サーバーはあなたが示した証明書の公開鍵に対してその署名をチェックします。

したがって、IDは完全に2番目のファイルにあり、プロセスは起動時にそれをメモリに読み込みます。その瞬間から、鍵はヒープダンプ、コアファイル、スワップページ、VMメモリのハイパーバイザーのスナップショット、そしてそのプロセスでコード実行権限を得たものすべてからアクセス可能になります。それをコピーした者は、どこでもそのマシンのように振る舞うことができます。秘密情報ストア、内部API、証明書のサブジェクトをロールにマッピングするデータベースなどです。

ファイル内の鍵は、実際のマシンIDではありません。それはマシン上に保存されているベアラートークン(bearer credential)であり、それを読み取った者は誰でも、誰かが気づいて証明書を失効させるまで、どこでもそのマシンになります。

脅威モデルは限定的です。私が防御しようとしているのは、ゲスト内部への読み取りアクセス権を得た攻撃者です。ファイル読み取り、コアダンプ、ファイルシステムに到達するSSRF、不適切に保存されたバックアップなどです。私は彼らがマシンのIDの動作するコピーなしで立ち去ることを望みます。

私が防御していないのは、ボックス上にいる間にそのIDを使用する、永続的なコード実行権限を持つ者です。それは別の問題であり、TPMはそれを解決しません。

したがって、私が必要とする鍵には4つの特性があります。

  1. TLSハンドシェイクに署名すること。それがIDの使用方法だからです。
  2. マシン外に決して存在しないこと。そのため、メモリやディスクを読み取っても、攻撃者は何も持ち去れません。
  3. それに到達するために2番目の認証情報を必要としないこと。なぜなら、その認証情報が盗むべき新しいものになるからです。
  4. 通常のTLSスタックで動作すること。すでにTLSを使用している2つのサービス間で、カスタムプロトコルを望まないからです。

TPMはこれらすべてを満たします。鍵で署名し、鍵を渡さないので、プロセスは決して秘密を保持しません。私が使用しているマシンでは、それは仮想TPMであり、チップ自体については以前の投稿で書きました。機密性の高いVMであることは、公開リストの項目に正確に1つ役立ちます。ゲストメモリはホストに対して暗号化されており、下から取得されたスナップショットは暗号文になります。リストの他のすべてはゲスト内で発生し、メモリ暗号化は役に立ちません。

なぜ他のものではないのか

TPMが登場する前に、3つの代替案があり、それらはすべて妥当です。それぞれが少なくとも1つの特性を欠いています。

ファイルパーミッションは強化する価値がありますが、それでも2番目の特性を満たしません。SELinux、RLIMIT_CORE=0、mlockallはいくつかの経路を閉じますが、鍵を使用するプロセスはその鍵をアドレス空間に持っており、ファイルがすでに読み取られている場合のモードビットでは、ptrace、/proc/pid/mem、およびそのプロセスでのコード実行はすべて影響を受けません。

短命な証明書も2番目の特性、そして部分的に3番目の特性を欠いています。盗まれた鍵が有用な期間を短縮しますが、1つのことしかできないのであれば、この記事のどの部分よりも価値があります。コピーを防ぐことはできず、更新を認証するために何らかの認証情報が必要になります。

KMSが真の競合相手です。Cloud KMSやVaultのトランジットエンジンは、決して離れない鍵を保持するため、2番目の特性を満たします。そして、多くのチームにとって、それが正しい答えです。それが欠けているのは3番目の特性です。KMSはあなたを何らかの方法で認証する必要があるため、ボックス上には署名鍵をアンロックする認証情報が存在することになり、その認証情報が攻撃者が代わりにコピーするものになります。これは問題を解決するのではなく移動させるだけです。ただし、その認証情報自体がハードウェアにバインドされている場合は除きます。

それに加えて実用的なコストがあります。つまり、すべてのコールドハンドシェイクが、ダウンする可能性のあるサービスへのネットワークラウンドトリップに依存するようになります。すでにTLSを話している2つのサービス間の依存関係を、通信できるかどうかの決定パスに入れたくありません。

したがって、それは3番目の特性(鍵に到達するための2番目の認証情報なし)に落ち着きます。何かがルートである必要があります。マシンによって保持され、コピーアウトできず、何も提示せずに到達できるものです。TPMはそれであり、ここにある他のすべて(KMSトークン、短命なワークロード証明書、購入する必要のあるPKCS#11トークン)は、それと競合するのではなく、それにぶら下がることができます。

これのどれもTPMを適切に実行されたKMSよりも安全にしません。ローカルであり、到達するために認証情報を必要とせず、過去10年間に構築されたほとんどのマシンにはすでに存在しています。

TLSが鍵に求めるもの

TPMは鍵で署名し、それ以外は何も行いません。それもTLSが必要とするすべてです。

TLS 1.3では、ピアは証明書で送信した秘密鍵を、CertificateVerifyで定義されたメッセージに署名することで証明します。そのメッセージは、64バイトの0x20、文字列TLS 1.3, client CertificateVerify、ゼロバイト、そして両サイドが交換したすべてのハンドシェイクメッセージの実行ハッシュであるトランスクリプトハッシュです。すべてがハッシュされ、そのダイジェストが鍵が署名するものです。証明書で認証するサーバーは、clientの代わりにserverを使用して同じ構造に署名します。これにより、どちらかの署名が他方にリプレイされるのを防ぎます。

トランスクリプトが含まれていることから、2つのことが導き出されます。両方のピアからの新しいランダム性が含まれているため、署名は他の接続には使用できず、リプレイできません。そして、ハンドシェイクごとに1つのトランスクリプトがあるため、鍵は1つのダイジェストに対する1つの署名のためにのみ要求されます。

秘密鍵は決して何かを復号せず、セッション鍵を導出することもありません。それらは、一時的なDiffie-Hellman交換から出てきます。CertificateVerifyがワイヤー上に出ると、鍵はその接続の寿命のために完了です。

Goも同じものを要求します。tls.CertificateはPrivateKeyを保持し、そのフィールドはcrypto.Signerを満たす必要があります。

type Signer interface {
	Public() crypto.PublicKey
	Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error)
}

crypto/tlsはCertificateVerifyメッセージ自体を構築し、ハッシュし、ダイジェストでSignを呼び出します。鍵材料を要求することはありません。PKCS#11モジュールやクラウドKMS鍵は、同じインターフェースを通じてGoにプラグインされ、TPMも同様に適合します。

TPM2_Signも同じ形状です。ダイジェストを入力し、署名を出力し、鍵はそのままです。したがって、私が書いたライブラリは、TPMに転送するSignと、適切な鍵へのアタッチに関するいくつかの注意点です。

process memory
TPM
crypto/tls
hashes the transcript
key
TPM2_Sign
digests
signature

ダイジェストと署名のみが交差します。鍵は決して交差しないので、ヒープダンプ、コアファイル、またはスワップページには漏洩するものはありません。

クライアント

これがクライアント全体です。何も鍵を指していません。

leaf, err := x509.ParseCertificate(certDER)
if err != nil {
	return err
}

// 証明書が鍵を選択します。
key, err := tpmtls.OpenForCertificate(tpmtls.DefaultDevice, leaf)
if err != nil {
	return err
}
defer key.Close()

conn, err := tls.Dial("tcp", "store.example.com:443", &tls.Config{
	MinVersion:   tls.VersionTLS13,
	Certificates: []tls.Certificate{key.TLSCertificate(leaf.Raw)},
})

ハンドルや鍵パス、デフォルト以上のデバイス設定はありません。tls.ConfigもTPMに言及していません。ファイル内の鍵と同じ設定であり、http.Transport、tls.Listen、またはhttp.Serverでも同様に機能します。

OpenForCertificateは、私がgo-tpmとgo-tpm-toolsの上に書いた薄いレイヤーであるgo-tpm-tlsから来ています。証明書のみを受け取りますが、これについては検討を重ねたので説明します。

TPM内の鍵はハンドルに配置されます。これは0x81000004のような数値で、オブジェクトの場所を示します。最も明白なことは、その数値を設定に含めることでした。私は最初にそのように書きましたが、それは間違いでした。ハンドルは鍵をプロビジョニングしたプロセスによって選択されるため、あるマシンで機能する値が次のマシンでは失敗し、起動時ではなく最初のハンドシェイクで失敗します。これは発見するには悪いタイミングです。しかし、アプリケーションはすでに提示しようとしている証明書を持っており、その証明書内の公開鍵は、TPM内のどの鍵で署名するかを正確に示しています。したがって、ルックアップは永続的なハンドルを読み取り、公開鍵を比較し、一致するものを選びます。一致するものがない場合は、起動時にtpmtls.ErrNotFoundが得られます。

また、2つの寿命とも一致します。鍵はマシンの寿命の間TPM内に留まり、それに対する証明書がローテーションされるため、証明書はアプリケーションが新しく渡されるものであり、鍵はそれを見つけなければならないものです。ハンドルを知っている場合は、tpmtls.Openがそれを受け取ります。

そのスニペットの2つの小さな点。tpmtls.DefaultDeviceは/dev/tpmrm0、つまりカーネルリソースマネージャーです。生の/dev/tpm0よりもこちらを使用したいのは、各ファイルディスクリプタに独自のコンテキストを与えるため、TPMを共有するプロセスが互いのオブジェクトを追い出すことがないからです。このディスクリプタごとの詳細は、ベンチマークで再び重要になります。これを開くにはroot権限またはtssグループのメンバーシップが必要です。

そして、Goのデフォルトである1.2ではなく、MinVersion: tls.VersionTLS13をピン留めする必要があります。その一般的な理由はほとんど廃止されています。TLS 1.2では、サーバー証明書の鍵は、静的RSA鍵交換スイートの下でプリマスターシークレットを復号するように要求される可能性があり、TPM署名鍵はそれを実行できません。最新のGoはすでにこれらのスイートを無効にしていますが、TLS 1.2でのクライアント認証はどちらにしても署名であるため、これはもはや実際の危険ではありません。それでもピン留めしてください。なぜなら、TLS 1.3では署名が鍵に要求できる唯一のことであり、この記事の後半の数値はそれに基づいて測定されたからです。

コードが仮定すること

上記のコードは6つのことが真実であることを前提としており、そのうち1つだけがあなたの制御下にあります。他の5つはあなたのプロセスが開始される前に別のプロセスによって決定されており、あなたのコードは結果にアタッチするだけです。

TPM内に署名鍵がすでに存在すること。他の何かがそれをそこに配置しました。通常は、TPM内で鍵を生成し、公開部分をハードウェア証拠にバインドし、それを証明させたアテステーションエージェントです。go-tpm-tlsには鍵を作成する方法は意図的にありません。また、それを削除することもありません。Closeは鍵を解放し、ハンドルをそのままにしておきます。なぜなら、プロビジョニングしなかった鍵からデタッチしても、マシン上の他のすべての人にとってそれを破壊するわけではないからです。

鍵は制限されていないこと。制限された鍵(アテステーション鍵のこと)は、TPM自体がハッシュしたデータにのみ署名します。これは検証チケットで証明されますが、TLSトランスクリプトハッシュは呼び出し元から提供され、チケットはありません。そのため、TPMはTPM_RC_TICKETと応答します。エージェントがアテステーション鍵とTLS鍵をプロビジョニングした場合、2番目の鍵が必要です。

鍵はECDSA、P-256またはP-384上にあること。RSA鍵はロードして署名しますが、crypto/tlsが要求する方法ではありません。これについては後で説明します。

鍵には認証値がないこと。go-tpm-tlsはnilセッションでロードします。これはヌルセッションに解決されるため、パスワードまたはポリシーで保護された鍵はロードされません。これはセキュリティ上の選択ではなく、制限です。認証値は、ボックス上の攻撃者が取得する必要のあるもう1つのものになります。プロビジョニングで設定した場合、このパッケージはまだ機能しません。

鍵は永続的なハンドルに配置されていること。永続的なハンドルは0x81000000から0x81FFFFFFまで実行され、再起動後も存続し、後から来るプロセスからアクセスできます。一時的なハンドルも使用できますが、作成したTPM接続でのみ有効であり、署名ごとに約10倍のコストがかかります。プロビジョニングに発言権がある場合は、永続的なものを要求してください。

あなたのプロセスが/dev/tpmrm0を開けること。これは、再プロビジョニングなしで修正できる唯一のものです。root権限またはtssグループが必要です。

他の5つは継承するため、プロビジョニングしたプロセスを信頼するのではなく、実際に何を取得したかを確認してください。NonExportableはTPMから属性を読み取ります。

ok, err := key.NonExportable()
if err != nil {
	return err
}
if !ok {
	return fmt.Errorf("key at %#x can be duplicated out of the TPM", key.Handle())
}

これはfixedTPM属性であり、TPMが秘密鍵を別のTPMに複製することを拒否することを伝えています。作成時のsensitiveDataOriginとペアになると、TPMが鍵を生成したことを示し、鍵が他のどこにも存在しなかったことを知ることができます。NonExportableは最初のものだけを読み取ります。2番目はプロビジョニング方法のプロパティです。

シェルからはtpm2_getcapとtpm2_readpublicを使用できます。一度実行して、正常なケースがどのように見えるかを知っておくと良いでしょう。これはGCE SEV-SNPゲストでtpm2-tools 5.6を使用した場合です。

$ sudo tpm2_getcap handles-persistent
- 0x81000004

$ sudo tpm2_readpublic -c 0x81000004
name: 000ba090006025e535419d16b12e7bdd219d20077aa4bd0fc99bc41cc5ed643005b3
name-alg:
  value: sha256
attributes:
  value: fixedtpm|fixedparent|sensitivedataorigin|userwithauth|sign
  raw: 0x40072
type:
  value: ecc
curve-id:
  value: NIST p256
scheme:
  value: ecdsa
scheme-halg:
  value: sha256

fixedtpmとsensitivedataoriginが重要な2つです。sign(restrictedなし)が3番目です。出力には秘密部分がありません。なぜなら、それを要求する方法がないからです。

試すための鍵の作成

実際のマシンではアテステーションエージェントが鍵を作成しますが、上記のいずれかを実行するにはそれが必要なので、ここでは簡単なバージョンを示します。

まず警告です。これはTPMから出せない鍵を作成しますが、それだけです。マシンが起動したことと何もバインドされておらず、検証者はいかなる場所にもその出所の証拠を持っていません。そのため、それに対して発行された証明書は、「あるTPMがこれを保持していた」以上の何も証明しません。この記事のコードを試すにはこれで十分ですが、プロビジョニングではありません。実際のアテステーションエージェントは鍵を作成し、公開部分を引用にバインドし、その証拠に対して証明させます。それが鍵を信頼する価値のあるものにします。

スニペットでは、clientはgo-tpm-tools/clientです。

// この投稿のコードを試すための使い捨て鍵。
template := tpm2.Public{
	Type:    tpm2.AlgECC,
	NameAlg: tpm2.AlgSHA256,
	Attributes: tpm2.FlagSign | tpm2.FlagSensitiveDataOrigin |
		tpm2.FlagUserWithAuth | tpm2.FlagFixedTPM | tpm2.FlagFixedParent,
	ECCParameters: &tpm2.ECCParams{
		Sign:    &tpm2.SigScheme{Alg: tpm2.AlgECDSA, Hash: tpm2.AlgSHA256},
		CurveID: tpm2.CurveNISTP256,
	},
}

const handle = tpmtls.Handle(0x81000004)

rwc, err := tpm2.OpenTPM(tpmtls.DefaultDevice)
if err != nil {
	return err
}
deferrwc.Close()

// TPM内で鍵を作成し、ハンドルに永続化します。
created, err := client.NewCachedKey(rwc, tpm2.HandleOwner, template, handle)
if err != nil {
	return err
}
created.Close() // それを解放します。鍵はハンドルに残ります。

そのテンプレートは、6つの仮定のうち5つをコードで表現したものです。FlagSensitiveDataOriginはTPMが秘密鍵を生成することを示します。FlagFixedTPMとFlagFixedParentは、複製できないことを意味します。FlagSign(FlagRestrictedなし)は、与えられたダイジェストに署名することを示します。これはCertificateVerifyが必要とするものです。曲線はP-256です。NewCachedKeyは一時的なものではなく永続化します。認証値が提供されないため、鍵は空の値になり、go-tpm-tlsがヌルセッションでロードできるようになります。

それでも提示するものがいくつか必要です。key.CertificateRequestは、TPMによって署名されたCSRを生成し、テスト対象のCAに渡すことができます。

終わったら鍵を削除してください。永続的なスロットは少なく、残された鍵は再起動後も存続します。

tpm2.EvictControl(rwc, "", tpm2.HandleOwner, handle, handle)

オーナー認証は、私がこれを実行した機密性の高いVMでは空文字列です。これはすべての場所で真実ではありません。

反対側は知らない

こちらが検証側です。こちら側にはTPMはなく、何も知りません。

pool := x509.NewCertPool()
pool.AppendCertsFromPEM(caPEM)


srv := &http.Server{
	Addr: ":8443",
	TLSConfig: &tls.Config{
		MinVersion: tls.VersionTLS13,
		ClientAuth: tls.RequireAndVerifyClientCert,
		ClientCAs:  pool,
	},
}

サーバーは、信頼するCAにチェーンする証明書の公開鍵に対して署名をチェックします。署名が作成されたときに秘密部分がどこにあったかは、それに不可視です。これは、片方のサービスからでも、もう片方のサービスからでも、一度に1つのサービスずつロールアウトできることを意味します。

これのどれもクライアント固有ではありません。サーバー自身の鍵もcrypto.Signerなので、それをTPMに入れるのは同じスワップと呼び出しです。

key, err := tpmtls.OpenForCertificate(tpmtls.DefaultDevice, serverLeaf)
if err != nil {
	return err
}
defkey.Close()

srv.TLSConfig.Certificates = []tls.Certificate{key.TLSCertificate(serverLeaf.Raw)}

サーバーは、クライアントと同様に、フルハンドシェイクごとに1つのCertificateVerifyに署名します。そのため、コストに関する以下のすべてが適用され、スループットの天井はダイヤラよりもリスナーではるかに重要になります。クライアントは変更を必要としません。なぜなら、クライアント側からは依然として通常の証明書だからです。

それも限界です。TPMは所有権の主張の強さを変えますが、相手側が検証するものを変えるわけではありません。検証者に鍵がTPM内にあり、どのマシンのTPMにあるかを知ってほしい場合、それは証明書に含まれている必要があります。つまり、ハンドシェイクにはそれがないため、発行時のアテステーションから来る必要があります。

壊れる4つの方法

これを機能させる間に4つのエラーが発生しました。エラーのどれもTLSやTPMに言及していなかったため、追跡が面倒でした。そのため、表示されるメッセージでリストアップします。

salt length must be rsa.PSSSaltLengthAuto

RSA鍵を使用しています。TLS 1.3はRSA-PSSを、ダイジェストと同じ長さのソルトで使用することを要求しているため、crypto/tlsは正確にそれを要求します。TPMは独自のソルト長を選択し、オーバーライドできないため、go-tpm-toolsはピアが拒否する署名を生成するのではなく、要求を拒否します。

鍵は正常にロードされますが、ハンドシェイクの途中で署名時に失敗します。P-256またはP-384を使用すると、問題は解決します。

key at 0x81000004 cannot sign: restricted keys are not supported

制限された鍵にアタッチしました。ほぼ間違いなくアテステーション鍵です。これはTPM自体がハッシュしたデータにのみ署名し、検証チケットで証明しますが、TLSトランスクリプトハッシュは呼び出し元から提供され、チケットはありません。

go-tpm-tlsなしでもこれを再現できます。制限された鍵に32バイトのランダムデータを渡し、それに署名するように要求します。これはCertificateVerifyが行うことです。

$ sudo tpm2_sign -c restricted.ctx -d -g sha256 -s ecdsa -o sig.bin digest.bin
ERROR: Esys_Sign(0x3E0) - tpm:parameter(3):invalid ticket
ERROR: Unable to run tpm2_sign

制限されていない鍵に対する同じコマンドは、それらのバイトに署名します。TPM_RC_TICKETは、制限された鍵はサポートされていないと述べているときに、go-tpm-toolsが報告している拒否です。

これはロード時に失敗し、意図的です。チェックはアタッチ時に実行され、ハンドシェイク時ではありません。そのため、設定ミスした鍵は、1時間後に1つの接続ではなく、起動時に失敗します。

tpmtls: no persistent key matches

これはErrNotFoundであり、保持している証明書とTPMの内容が一致しないことを意味します。アテステーションエージェントがまだ鍵をプロビジョニングしていないか、証明書が対象としているものとは異なる鍵をプロビジョニングしたかのどちらかです。これは、再プロビジョニング後の古い証明書の外観です。errors.Isでテストし、ハンドルをリストして比較してください。

remote error: tls: error decrypting message、最初の読み取り時

これはTLSであり、TPMではありません。そして、私を捕らえたものです。

TLS 1.3では、クライアントは証明書、CertificateVerify、およびFinishedを単一のフライトで送信し、それらのいずれかが受け入れられたかどうかを聞くのを待ちません。tls.Dialはどちらの場合でも動作する接続を返します。サーバーが署名を拒否した場合、アラートを送信し、次の読み取り時にそのアラートを受け取ります。

したがって、相手側が受け入れないIDを持つワークロードは、接続したワークロードとまったく同じように見えます。ヘルスチェックでダイヤルする場合、TCPが機能し、サーバーの証明書が有効であることを確認しましたが、あなた自身のものについては何も確認していません。バイトを読み取ってください。

conn, err := tls.Dial("tcp", addr, cfg)
if err != nil {
	return err
}
defconn.Close()

// サーバーからの証明書に対する判断は、ダイヤルエラーではなく、次の読み取り時のアラートとして届きます。
// これがないと、プローブはどちらの場合でもパスします。
if _, err := conn.Write(probe); err != nil {
	return err
}
if _, err := conn.Read(make([]byte, 1)); err != nil {
	return err
}

net/httpを経由している場合、実際の要求は応答を読み取るため、これは無料で提供されます。落とし穴は、ダイヤルのみを行う準備完了プローブです。

コスト

TPMのコストは、リクエストごとではなく、フルハンドシェイクごとに1回かかります。

TPMはミリ秒単位で署名しますが、ソフトウェアはマイクロ秒単位で署名します。私はgo-tpm-tls-benchで、vTPMがハイパーバイザーソフトウェアで実装されているGoogle Cloud Confidential VMで実行した接続にそのコストがどれだけ影響するかを測定しました。以下は、AMD SEV-SNPとP-256鍵を備えたn2d-standard-2で、シングルスレッドで、同じ曲線のソフトウェア鍵に対して測定した結果です。

シナリオ鍵中央値95パーセンタイルスループット署名数
生の署名、100署名TPM、永続2.21 ms2.61 ms452 件/秒100
ソフトウェア0.06 ms0.08 ms16591 件/秒100
TPM、一時20.44 ms21.20 ms49 件/秒100
50接続、再開なしTPM3.35 ms3.83 ms295 件/秒50
ソフトウェア1.38 ms1.51 ms707 件/秒50
50接続、再開ありTPM0.94 ms1.59 ms856 件/秒1, 49 再開
ソフトウェア0.95 ms1.13 ms1011 件/秒1, 49 再開
1接続、50リクエストTPM0.02 ms0.03 ms40043 件/秒1
ソフトウェア0.02 ms0.02 ms39133 件/秒1

署名はコールドハンドシェイクの大部分を占めます。3.35msのうち2.21msです。TPMは一度に1つのコマンドを実行し、署名はそのためロックによってシリアル化されるため、マシンは新しい相互認証接続を1秒あたり数百件しか処理できません。この制限はマシンごとです。各マシンには独自のTPMがあるため、1つのTPMの後ろにレプリカを増やしても改善されません。

最後の2つのシナリオでは、2つの鍵はノイズの範囲内で一致します。署名数はその理由を説明しています。50回の新しい接続で50署名、50回の再開で1署名、再利用された接続での50回のリクエストで1署名です。TPMは速くなったわけではなく、単にハンドシェイクが少なかっただけです。

その上限はDoSベクトルでもあります。公開されているリスナーでは、フルハンドシェイクを強制することは、1秒あたり数百署名を消費する簡単な方法であり、ロックにはタイムアウトがないため、プロセス内のすべてのハンドシェイクがその背後にキューイングされます。公開リスナーにTPM鍵を配置しないでください。アウトバウンドクライアント、内部リスナー、またはハンドシェイクレートを制限するものに使用してください。

2つの結果は私を驚かせました。最初の結果は、最初の測定方法を誤ったために2番目の結果が見つかっただけです。

一時行は10倍、Intel TDXでは15倍です。カーネルリソースマネージャーはコマンド間で一時オブジェクトのコンテキストを保存するため、すべての署名は鍵をTPMにスワップバックするコストがかかりますが、永続オブジェクトはTPM自身のストレージにそのまま残ります。私はそれが重要になると予想しましたが、10倍になるとは思いませんでした。

予想外だったのは、コストがオブジェクトごとではなく、ファイルディスクリプタごとであることです。他に触れていない永続鍵は2.08msで署名しますが、同じ鍵は、同じファイルディスクリプタ上で無関係な一時オブジェクトがロードされている間は20.04msで署名します。同じ10倍のファクターで、一時的ではなかった鍵でもです。別のディスクリプタでは、同じ鍵は2.10msかかります。したがって、永続鍵は必要ですが十分ではなく、あなたが制御できるのは、署名を行うディスクリプタにプロセスがロードする他のものですが、それは何もありません。これも私がOpenを使用する理由であり、Newではない理由です。鍵はそのディスクリプタを所有し、プロセス内の他のものは一時オブジェクトをそれに配置できません。

曲線選択のコストはコールドハンドシェイクでのみ発生し、SEV-SNPでは予想よりも低いです。このマシンでは、P-384はP-256の2.21msに対して2.37msで署名します。これは、295接続/秒に対して226コールドハンドシェイク/秒です。このギャップを有効数字2桁で引用することはありません。同じインスタンスタイプでの以前の実行では、7%ではなく16%でした。そのため、このプラットフォームではプレミアムは小さく、ノイズが多いです。Intel TDXでは、同じ比較は65%でした。再開はどちらの場合でも違いを消去します。これは、CNSA 1.0のようなポリシーがP-384を使用する場合に重要です。

上記のP-256テーブルは、1つのn2d-standard-2での単一の実行であり、TDXの数値はc3-standard-4から取得されています。どちらもハイパーバイザー実装のvTPMを備えており、これのどれもディスクリートTPMハードウェアについては何も述べていません。

証明すること

このように署名されたハンドシェイクは、接続が鍵を保持するマシンのTPMから来たこと、そしてトランスクリプトが両サイドからの新しいランダム性を持っているため、リプレイではなく現在行われたことを証明します。証明しないことが3つあります。

それはそのマシンで実行されているコードについては何も言いません。あなたのプロセス内の攻撃者は、それが実行されている間はいつでも署名できます。ボックス上の他のものも同様です。なぜなら、鍵には認証値がなく、それへのアクセス制御は/dev/tpmrm0を開けるかどうかだけだからです。これはrootまたはtssグループを意味します。鍵はマシンにバインドされており、ワークロードにはバインドされていません。それを1つのプロセスに絞ることは別の問題であり、TPMはそれを解決しません。

また、その攻撃者からトラフィックへのアクセスも防ぎません。セッション鍵は通常のメモリにあります。TPMに残るのは長期的なIDであり、接続上のバイトではありません。

そして、vTPM自体がどこに存在するかによります。これはプラットフォームの選択であり、機密コンピューティングがあなたのために解決するものではありません。SEV-SNPはゲストメモリをホストに対して暗号化します。誰がTPMを実装したかについては何も言いません。私がこれらの数値を測定したマシン(Google)では、vTPMはハイパーバイザーソフトウェアであるため、鍵はゲスト内のすべてからアクセスできませんが、プラットフォームからはアクセス可能です。これはdmesgで見ることができます。SEV: SNP running at VMPL0.は、カーネルが最も特権的なレベル自体を保持していることを意味します。そのため、その下にvTPMを提供するものは何も実行されていません。

別の構成では、VMPL0に小さな信頼されたコンポーネントを配置し、ゲストカーネルをVMPL1に配置します。そのコンポーネントは、暗号化されたゲスト内でvTPMを提供します。Azureはこれをparavisorで行っており、COCONUT-SVSMがオープンバージョンです。そこでは、ホストはTPMの状態を読み取ることができません。そのコンポーネントをTCB内に配置する代償を払います。アテステーションはそれを測定する方が良いでしょう。同じデバイスとコードですが、最初のセットアップではプラットフォームが鍵を読み取ることができ、2番目のセットアップではそれができません。これを誰かに説明する前に、どちらを使用しているか調べてください。

まとめ

これのどれも、マシン上の攻撃者が鍵を使用するのを止めません。彼らがそこにいる間は、好きなように署名できます。彼らができないのは、それを持ち出すことです。そのため、それを使用することは、彼らのアクセスが終わるとともに終了します。

ファイル内の秘密鍵は、マシン上に存在するベアラートークンです。TPM内の鍵は、マシン自体のプロパティに近いです。また、アテステーション後のマシンIDに対する唯一の解決策であり、アテステーションが確立したことを無効にしないものです。

欠けているのは、鍵とアテステーションの間のリンクです。そのため、SPIFFEを見ています。大まかに言うと、vTPMの公開鍵は証拠とともにアップロードされ、同じ鍵に対する短命な証明書がダウンして戻ってきます。これはX.509-SVIDです。秘密鍵は移動しません。証明書は期限切れになり、下の鍵はそのままに再発行されます。これは、この記事の前半と同じ分割です。

その先のmTLSは、ここで読んだコードであり、別のCAに向けられています。次にそれをまとめます。

作業メモ。ここに間違いがあれば教えてください。