Windows11/.NETでTPM2へ外部生成AES鍵をImportする完全ガイド|TSS.NETでのTpmRc.Attributes解消と正しい属性設定

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 パイプラインの全体像(なぜこの順序か)

  1. LoadExternal:外部生成鍵を“外来オブジェクト”として一時ロード。ここで FixedTPM/FixedParent/SensitiveDataOrigin は当然オフ。
  2. Duplicate:TPM にターゲット親(ストレージキー)向けの複製ブロブ(duplicate と outSymSeed)を作らせる。内側ラップ(symmetricAlg)を Null にすればシンプル。
  3. Import:duplicate と outSymSeed を渡して、親の下に取り込める TpmPrivate を得る。
  4. Load:TpmPrivate と公開領域(TpmPublic)を渡して transient に実体化。
  5. 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() で必ず掃除しましょう。

チェックリスト(詰まりやすい箇所)

  1. キーサイズとモードの一致:テンプレートの SymDefObject(AES-256/CFB)が、実際の鍵(256bit)と整合しているか。
  2. 属性ビット:FixedTPM/FixedParent/SensitiveDataOrigin を付けていないか。対称鍵で Restricted を付けていないか。
  3. Name アルゴリズム:Sha256 を選んだか(Null は避ける)。
  4. 親ハンドル:ストレージ親(例:RSA 2048 / SHA-256)が有効で、Duplicate / Import の対象になっているか。
  5. 永続ハンドルの重複:同じ番号を使い回していないか。競合時は別スロットへ。
  6. 権限: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.AuthMissingUserWithAuth なのに 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 管理と併せて定期的に鍵を入れ替える前提で設計しておく。

トラブルシューティング・クイックフローチャート

  1. Import が TpmRc.Attributes → 属性を見直す(FixedTPM/FixedParent/SensitiveDataOrigin を外す)。
  2. 改善しない → nameAlg を Sha256 に変更。
  3. まだダメ → テンプレートの type = Symcipher / AES-256/CFB を確認。
  4. それでも → LoadExternal → Duplicate → Import の順(TPM にブロブ作成を任せる)。
  5. 永続化でエラー → 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、用途分離、ローテーションで“安全に長生きする鍵”を目指す。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次