Windows 11 上の .NET 8(TSS.NET)で、外部で生成した 256bit AES 鍵を TPM 2.0 に永続化しようとしたところ Tpm2.Import() が TpmRc.Attributes で失敗する——この“ハマりどころ”を、原因の芯からほぐし、確実に成功させる最短手順と実務的な設計指針、検証コードまでまとめました。RSA では通るのに AES でだけ落ちる理由、正しい属性ビット、テンプレート、実装上の注意点を一気通貫で解説します。
想定環境と症状の整理
| 項目 | 内容 |
|---|---|
| OS / ランタイム | Windows 11 / .NET 8.0 |
| ライブラリ | TSS.NET(TSS.MSR / Tpm2Lib) |
| 目的 | System.Security.Cryptography で生成した 256bit AES 鍵を TPM 内オブジェクトとして永続化し、複数デバイスで共通利用 |
| 発生事象 | Tpm2.Import() が TpmRc.Attributes で失敗(“{Attributes} was returned for command Import.”)。同じ _primaryHandle で RSA は成功 |
結論(先に要点)
- 外部生成鍵の Import では
FixedTPM/FixedParent/SensitiveDataOriginを付けてはいけません。AES キー(対称鍵)ならDecrypt|Encrypt|UserWithAuth(必要に応じてNoDA)のみが基本。 - Name アルゴリズムは
Sha256を推奨。Nullを選ぶと、一部のユーティリティやヘルパー(例:Name を参照する実装)でNullReferenceExceptionの温床になります。 - テンプレートの タイプは
SymCipher。パラメータは AES-256 / CFB(CFB は TPM2.0 の必須モードで互換性が高い)。 - 実装は 「LoadExternal → Duplicate → Import → Load → EvictControl」の順が堅実。これなら暗号ラッパーの作成を TPM に任せられ、属性ミスマッチを避けやすい。
なぜ RSA は通って AES は落ちるのか
RSA 鍵を TPM 内で生成(CreatePrimary / Create)している場合、TPM 自身が作成者なので FixedTPM / FixedParent / SensitiveDataOrigin が立っていても整合が取れます。一方、外部生成 AES 鍵を Import するときにこれらの属性が立っていると、「TPM が作っていないのに TPM が作ったことになっている」という矛盾で TpmRc.Attributes になります。
最短の解決手順(コピペ基点)
以下は TSS.NET(Tpm2Lib)を前提にした、確実に通る構成の最短コードです。ポイントは「LoadExternal で一時的に外部鍵を TPM に載せ、Duplicate でターゲット親(ストレージキー)向けの複製ブロブを TPM に作らせ、Import で正式な TPM 内オブジェクト化する」流れです。
using System;
using System.Linq;
using System.Security.Cryptography;
using Tpm2Lib;
public class TpmAesImportSample
{
private readonly Tpm2 _tpm;
private readonly TpmHandle _primaryHandle; // 既存のストレージ親(RSA 2048 / nameAlg: SHA-256 を想定)
public TpmAesImportSample(Tpm2 tpm, TpmHandle primaryHandle)
{
_tpm = tpm;
_primaryHandle = primaryHandle;
}
public TpmHandle ImportAes256ToPersistent(ushort slot = 0x20, byte[]? userAuth = null)
{
// 1) 外部生成の AES-256 キー(既にある場合はそれを使う)
byte[] aesKey = RandomNumberGenerator.GetBytes(32); // 256 bit
// 2) 対称鍵の Public テンプレート(NameAlgはSha256。属性は最小限)
var symPub = new TpmPublic(
TpmAlgId.Symcipher, // type
TpmAlgId.Sha256, // nameAlg
ObjectAttr.Decrypt | ObjectAttr.Encrypt | ObjectAttr.UserWithAuth | ObjectAttr.NoDA,
null, // authPolicy
new SymDefObject(TpmAlgId.Aes, 256, TpmAlgId.Cfb),
new Tpm2bDigestSymcipher());
// 3) 外部鍵として一時ロード(FixedTPM等のビットは当然オフ)
// TSS.NET のバージョンにより構築方法が少し異なります。概念的には
// 「type: Symcipher / key: aesKey / authValue: (任意)」を inPrivate に詰めます。
var sens = new Sensitive(
TpmAlgId.Symcipher,
new SensitiveUnion(aesKey), // 対称鍵の平文
userAuth ?? Array.Empty<byte>(), // AuthValue(UserWithAuthを使うなら非空推奨)
Array.Empty<byte>()); // seedValue
var inPriv = new Tpm2bSensitive(sens);
var ext = _tpm.LoadExternal(inPriv, symPub, TpmRh.Null);
try
{
// 4) 親の public を取得(Duplicate と Import で使う)
_tpm.ReadPublic(_primaryHandle, out var parentPub, out _, out _);
// 5) Duplicate:TPM に複製ブロブを作らせる(inner wrap なし)
var nullSym = new SymDefObject(TpmAlgId.Null, 0, TpmAlgId.Null);
var duplicateOut = _tpm.Duplicate(ext, _primaryHandle, null, nullSym);
var duplicate = duplicateOut.duplicate;
var outSymSeed = duplicateOut.outSymSeed;
var encKeyOut = duplicateOut.encryptionKeyOut;
// 6) Import:親の下に取り込める private を得る
var importedPriv = _tpm.Import(
_primaryHandle,
encKeyOut,
symPub,
duplicate,
outSymSeed,
nullSym);
// 7) Load:実体化(Transient)
var imported = _tpm.Load(_primaryHandle, importedPriv, symPub);
// 8) 永続化(EvictControl)
var persistent = new TpmHandle((uint)(TpmHc.PersistentFirst + slot)); // 例: 0x8100_0020
// 既に使用中なら別番号を選ぶ(後述の「ハンドル管理」を参照)
_tpm.EvictControl(TpmRh.Owner, imported, persistent);
return persistent;
}
finally
{
// 一時オブジェクトは後片付け
_tpm.FlushContext(ext);
}
}
}
補足: 上のコードは TSS.NET の一般的な型名に合わせていますが、バージョンによってコンストラクタの引数や Symcipher/SymCipher の表記ゆれがあります。意味は「タイプ=対称鍵、NameAlg=SHA-256、属性=Decrypt|Encrypt|UserWithAuth(+NoDA)、パラメータ=AES-256/CFB」です。
属性ビットの正解と NG 組み合わせ
| 属性 | 外部生成鍵の Import | 理由・メモ |
|---|---|---|
FixedTPM | 付けない | TPM 自身が生成した鍵にのみ許可。外部生成では矛盾 → TpmRc.Attributes |
FixedParent | 付けない | 同上。Import 元は TPM 外なので固定親は不可 |
SensitiveDataOrigin | 付けない | TPM 内生成専用フラグ。外部生成鍵では 0 |
Decrypt / Encrypt | 付ける | 対称鍵で EncryptDecrypt2 を使うために必要。どちらか一方でもよいが両方が実務上便利 |
UserWithAuth | 付ける | 平易なパスワード/バイト列で使用制御する場合。後述の authValue 設定とセットで |
NoDA | 推奨 | 辞書攻撃カウンタの影響を受けたくない場合に付与(運用ポリシー次第) |
Restricted | 付けない | 一般的な AES 利用(EncryptDecrypt2)では不要。誤って付けると不整合 |
Name アルゴリズム設定の落とし穴
nameAlg = Sha256 を推奨します。Null にするとオブジェクトの Name が生成されず、Name 参照を前提にするヘルパー(例:Duplication ブロブ作成やテンプレート検証)で NullReferenceException を誘発します。TPM 上の衝突リスクや相互運用性の観点でも SHA-256 を選んでおくのが無難です。
Import パイプラインの全体像(なぜこの順序か)
- LoadExternal:外部生成鍵を“外来オブジェクト”として一時ロード。ここで
FixedTPM/FixedParent/SensitiveDataOriginは当然オフ。 - Duplicate:TPM にターゲット親(ストレージキー)向けの複製ブロブ(
duplicateとoutSymSeed)を作らせる。内側ラップ(symmetricAlg)を Null にすればシンプル。 - Import:
duplicateとoutSymSeedを渡して、親の下に取り込めるTpmPrivateを得る。 - Load:
TpmPrivateと公開領域(TpmPublic)を渡して transient に実体化。 - EvictControl:空きの永続ハンドルに配置。以後はそのハンドルで直接利用可能。
「GetDuplicationBlob を使う方法」との比較
外部ライブラリ/ユーティリティの GetDuplicationBlob()(親公開鍵で複製ブロブを外部生成)→ Import() → Load() でも構いません。ただし nameAlg = Null のように Name を前提にしない設計だと内部で Name 参照が起きた際に例外や不整合の温床になります。TPM に Duplicate させる方式は、暗号ラップの細部を TPM 任せにできるため、実装の安定度が高いのが利点です。
ハンドル管理と後片付け
永続ハンドルは 0x8100_0000〜の範囲です。衝突を避けるため、現在使用中の一覧を取得して空き番号を選んでください。
// 使用中の永続ハンドル列挙(概念コード)
var caps = _tpm.GetCapability(TpmCap.Handles, (uint)Ht.Persistent, 100);
var used = ((HandleArray)caps.capabilityData).handle;
uint first = (uint)TpmHc.PersistentFirst; // 0x8100_0000
uint candidate = first + 0x20;
while (used.Contains(new TpmHandle(candidate))) candidate++;
var persistent = new TpmHandle(candidate);
// 永続化
_tpm.EvictControl(TpmRh.Owner, imported, persistent);
// 後片付け
_tpm.FlushContext(imported);
ポイント: 永続オブジェクトは再起動後もハンドルで参照できます。開放忘れの transient ハンドルは FlushContext() で必ず掃除しましょう。
チェックリスト(詰まりやすい箇所)
- キーサイズとモードの一致:テンプレートの
SymDefObject(AES-256/CFB)が、実際の鍵(256bit)と整合しているか。 - 属性ビット:
FixedTPM/FixedParent/SensitiveDataOriginを付けていないか。対称鍵でRestrictedを付けていないか。 - Name アルゴリズム:
Sha256を選んだか(Nullは避ける)。 - 親ハンドル:ストレージ親(例:RSA 2048 / SHA-256)が有効で、
Duplicate/Importの対象になっているか。 - 永続ハンドルの重複:同じ番号を使い回していないか。競合時は別スロットへ。
- 権限:
EvictControlはTpmRh.Owner権限が必要。UserWithAuthを使うならauthValueをハンドルにセット。
利用例:TPM で AES-256-CFB を実行
インポートして永続化したハンドル(例:0x8100_0020)で EncryptDecrypt2 を呼び出します。IV は AES ブロック長(16 バイト)。
byte[] iv = new byte[16]; // 例: 全ゼロ(運用では毎回ランダム生成・保存推奨)
byte[] plaintext = System.Text.Encoding.UTF8.GetBytes("hello tpm");
var ct = tpm.EncryptDecrypt2(persistentHandle, plaintext, false, TpmAlgId.Cfb, iv);
var pt = tpm.EncryptDecrypt2(persistentHandle, ct, true, TpmAlgId.Cfb, iv);
注意: CFB は IV の再利用厳禁。複数メッセージで同一 IV を使わない設計(IV をメッセージごとに生成・保存)にしてください。
複数デバイスで同一 AES 鍵を使うには
同じ平文 AES キーを各デバイスの TPM に Import すれば共通利用は可能です。ただし次の設計が現実的です:
- 各デバイスの親公開鍵で Duplicate/Import を行う(親はデバイスごとに異なる)。
- テンプレート(type / nameAlg / parameters / attributes)を同一にしておくと挙動差が出にくい。
- セキュリティ設計上は、共通平文鍵を永久運用するのは避け、KEK/DEK 構成(共通のラップ鍵で一時 DEK を保護)や、デバイス固有鍵 + ラップへ寄せる方が安全。
テスト戦略(再現と回帰を潰す)
1) Import の成否と属性検証
- 意図的に
FixedTPMをオンにしてTpmRc.Attributesを再現できるか。 nameAlg = Nullにして、Name 参照系のユーティリティが例外を出すか。
2) 暗号の相互運用テスト
- 同じ鍵・IV・平文で、TPM の
EncryptDecrypt2と .NET のAes(CFB) の結果が一致するか。 - デバイス A で暗号化 → デバイス B で復号ができるか(同一鍵であることの確認)。
3) 永続ハンドルの生存確認
- 再起動後にハンドルへアクセスできるか。
- 意図せず上書きしていないか(
GetCapabilityで一覧を取って照合)。
よくあるエラーと対処
| エラー | 代表原因 | 対処 |
|---|---|---|
TpmRc.Attributes | 外部生成なのに FixedTPM / FixedParent / SensitiveDataOrigin が立っている/対称鍵に不適切な属性(Restricted 等) | 属性を Decrypt|Encrypt|UserWithAuth(+NoDA) に絞る |
TpmRc.Value | テンプレートの SymDefObject と実際の鍵サイズ/モードが不一致 | AES-256/CFB にそろえる(CFB は TPM2.0 で必須) |
TpmRc.Policy / TpmRc.AuthMissing | UserWithAuth なのに authValue 未設定、または誤り | ハンドルに authValue を設定してからコマンドを実行 |
NullReferenceException(C#) | nameAlg = Null により Name 未生成のままヘルパーが Name を参照 | nameAlg = Sha256 に変更 |
実装ディテール(迷いがちなところの補足)
- 対称鍵のユニーク値:
TpmPublic.uniqueはTpm2bDigestSymcipherを使い、通常はゼロ埋めで問題ありません(鍵そのものを公開領域に入れない)。 - Import の
symmetricAlg:内側ラップを使わないならNullを渡すのが簡潔。ラップ鍵を自前で運びたい高度な構成でのみ AES を指定します。 - 親の nameAlg:子オブジェクトの
nameAlgは親と同一である必要はありませんが、実務上は SHA-256 に統一しておくとトラブルが少ない。 - 辞書攻撃カウンタ:
UserWithAuthを使う鍵はNoDAを付けるとロックアウト事故を避けやすい(ポリシー運用と相談)。 - LoadExternal の階層:
TpmRh.Nullを使うのが一般的。Import 後は階層に依存しません。
セキュリティ設計の現実解
「同一の平文 AES 鍵を複数端末に配布して TPM に格納」するのは、攻撃面の分散(単一点破壊)になりがちです。理想は次のいずれか:
- デバイス固有鍵 + ラップ:端末ごとに一意の DEK(データ鍵)を生成し、上位の KEK(共通鍵)または公開鍵でラップして配布。
- 用途ごと鍵分割:暗号化・MAC・ラップなど用途を分離し、TPM 内で用途限定(属性・ポリシー)を徹底。
- 鍵ローテーション:IV 管理と併せて定期的に鍵を入れ替える前提で設計しておく。
トラブルシューティング・クイックフローチャート
- Import が
TpmRc.Attributes→ 属性を見直す(FixedTPM/FixedParent/SensitiveDataOriginを外す)。 - 改善しない →
nameAlgを Sha256 に変更。 - まだダメ → テンプレートの type = Symcipher / AES-256/CFB を確認。
- それでも → LoadExternal → Duplicate → Import の順(TPM にブロブ作成を任せる)。
- 永続化でエラー →
TpmRh.Ownerの権限・ハンドル衝突を確認。
参考テンプレート(最小構成)
var importKeyTemplate = new TpmPublic(
TpmAlgId.Symcipher, // type
TpmAlgId.Sha256, // nameAlg
ObjectAttr.Decrypt | ObjectAttr.Encrypt | ObjectAttr.UserWithAuth | ObjectAttr.NoDA,
null,
new SymDefObject(TpmAlgId.Aes, 256, TpmAlgId.Cfb),
new Tpm2bDigestSymcipher());
FAQ(よくある質問)
Q. 親のアルゴリズムは RSA でよい?
はい。一般的なストレージ親は RSA 2048 / SHA-256 で問題ありません(ECC 親でも可)。
Q. Encrypt と Decrypt は両方必要?
暗号化・復号の双方で使うなら両方付けるのが実務的です。片方向だけに絞る運用なら必要な方のみ。
Q. LoadExternal したオブジェクトを直接永続化できる?
できません。LoadExternal のオブジェクトは外部起源で、EvictControl の対象外です。必ず Import → Load を経てください。
Q. Name は端末間で同一になる?
テンプレートが同一で unique をゼロにしていれば、Name も一致する構成が一般的です(Name は公開情報)。ただし一致を前提にした設計は避け、ハンドル管理とメタデータで識別する方が安全です。
まとめ
- 原因:外部生成鍵に不適切な属性(
FixedTPM/FixedParent/SensitiveDataOriginなど)が立っている/NameAlg がNull。 - 解決:
Decrypt|Encrypt|UserWithAuth(+NoDA) に絞った Symcipher/AES-256/CFB テンプレート + LoadExternal → Duplicate → Import → Load → EvictControl。 - 注意:IV 管理、ハンドル衝突回避、権限(
EvictControlは Owner)を忘れない。 - 設計:共通平文鍵の配布は最小限に。可能なら KEK/DEK、用途分離、ローテーションで“安全に長生きする鍵”を目指す。

コメント