TPMを使った鍵の暗号化をRustで実装してみた(Windows API)
Windows 上で windows-sys を使用して、TPMベースの暗号化を実装しました。
最初は tss-esapi を使った鍵ラップを試みましたが、これは主に Linux 向けに設計されていて、Windows で使用するにはより複雑なセットアップが必要だったため、 windows-sys を採用しました。
この記事では、その実装コードと、各関数についての説明などを公式ドキュメントを用いて解説しています!
Cargo.toml
[dependencies]
windows-sys = { version = "0.61", features = [
"Win32_Security_Cryptography",
"Win32_Foundation",
] }
rand = "0.9"
zeroize = "1.8"
実装
このライブラリは FFI を使用しているので、 unsafe で囲う必要があります。
use std::{
ffi::{OsStr, c_void},
os::windows::ffi::OsStrExt,
ptr,
};
use windows_sys::{
core::HRESULT,
Win32::{
Foundation::NTE_BAD_KEYSET,
Security::Cryptography::*,
},
};
use zeroize::Zeroize;
const KEY_NAME: &str = "RSA_KEY";
fn open_provider() -> Result<NCRYPT_PROV_HANDLE, HRESULT> {
unsafe{
let mut hprov: NCRYPT_PROV_HANDLE = 0;
let status = NCryptOpenStorageProvider(
&mut hprov,
MS_PLATFORM_CRYPTO_PROVIDER,
0,
);
if status != 0 {
Err(status)
} else {
Ok(hprov)
}
}
}
fn create_padding_info() -> BCRYPT_OAEP_PADDING_INFO {
BCRYPT_OAEP_PADDING_INFO {
pszAlgId: BCRYPT_SHA256_ALGORITHM,
pbLabel: ptr::null_mut(),
cbLabel: 0,
}
}
fn to_utf16(s: &str) -> Vec<u16> {
let mut utf16: Vec<u16> = OsStr::new(s).encode_wide().collect();
utf16.push(0);
utf16
}
// 対象の鍵は、推奨されている32バイト(AES-256)であると想定しています。
// statusはi32(HRESULT)値で、
// 0の場合は関数が正常に実行されたことを意味します。
fn wrap_key(mut target_key: [u8; 32]) -> Result<Vec<u8>, HRESULT> {
unsafe {
let hprov = open_provider()?;
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let key_name = to_utf16(KEY_NAME);
// TPMに登録されている鍵を取得する
let status = NCryptOpenKey(
hprov,
&mut hkey,
key_name.as_ptr(),
0,
0,
);
if status != 0 {
match status {
NTE_BAD_KEYSET => {
// このエラーは、TPMに指定のキーが登録されていない場合に発生する
// 指定した名前で永続的なRSAキーを作成する
let status_2 = NCryptCreatePersistedKey(
hprov,
&mut hkey,
BCRYPT_RSA_ALGORITHM,
key_name.as_ptr(),
0,
0,
);
if status_2 != 0 {
NCryptFreeObject(hprov);
return Err(status_2)
}
// 作成した鍵を使用できるようにする
let status_2 = NCryptFinalizeKey(
hkey,
0,
);
if status_2 != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status_2)
}
},
_ => {
NCryptFreeObject(hprov);
return Err(status)
},
}
}
// 暗号化のデータサイズを取得
let mut size: u32 = 0;
let padding_info = create_padding_info();
let status = NCryptEncrypt(
hkey,
target_key.as_ptr(),
target_key.len() as u32,
&padding_info as *const _ as *const c_void,
ptr::null_mut(),
0,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status)
}
// 対象の鍵をラップする
let mut wrapped_key = vec![0u8; size as usize];
let status = NCryptEncrypt(
hkey,
target_key.as_ptr(),
target_key.len() as u32,
&padding_info as *const _ as *const c_void,
wrapped_key.as_mut_ptr(),
size,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status)
}
// 元の鍵をゼロ化
target_key.zeroize();
// バッファを実際の暗号化データのサイズに調整する
wrapped_key.truncate(size as usize);
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
Ok(wrapped_key)
}
}
fn unwrap_key(target_key: Vec<u8>) -> Result<Vec<u8>, HRESULT> {
// 暗号化時とほぼ同じ処理です
unsafe {
let hprov = open_provider()?;
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let key_name = to_utf16(KEY_NAME);
let status = NCryptOpenKey(
hprov,
&mut hkey,
key_name.as_ptr(),
0,
0,
);
if status != 0 {
NCryptFreeObject(hprov);
return Err(status)
}
// 復号化のデータサイズを取得
let mut size: u32 = 0;
let padding_info = create_padding_info();
let status = NCryptDecrypt(
hkey,
target_key.as_ptr(),
target_key.len() as u32,
&padding_info as *const _ as *const c_void,
ptr::null_mut(),
0,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status)
}
// 対象の鍵をアンラップする
let mut unwrapped_key = vec![0u8; size as usize];
let status = NCryptDecrypt(
hkey,
target_key.as_ptr(),
target_key.len() as u32,
&padding_info as *const _ as *const c_void,
unwrapped_key.as_mut_ptr(),
size,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status)
}
// バッファを実際の復号化データのサイズに調整する
unwrapped_key.truncate(size as usize);
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
Ok(unwrapped_key)
}
}
このmain関数で実行してみてください!
use rand::RngCore;
fn main() {
let mut key = [0u8; 32];
let mut rng = rand::rng();
rng.fill_bytes(&mut key);
println!("元の鍵: {:?}", key);
let wrapped_key = match wrap_key(key) {
Ok(k) => {
println!("ラップした鍵: {:?}", k);
k
},
Err(e) => {
println!("Error: {e}");
return
},
};
match unwrap_key(wrapped_key) {
Ok(k) => println!("アンラップした鍵: {:?}", k),
Err(e) => println!("Error: {e}"),
}
}
復号した鍵と元の鍵は一致します!
もし鍵を削除したい場合は、
fn delete_key() -> Result<(), HRESULT> {
unsafe{
let hprov = open_provider()?;
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let key_name = to_utf16(KEY_NAME);
let status = NCryptOpenKey(
hprov,
&mut hkey,
key_name.as_ptr(),
0,
0,
);
if status != 0 {
NCryptFreeObject(hprov);
return Err(status)
}
// 登録した鍵を削除する
let status = NCryptDeleteKey(
hkey,
0,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
Err(status)
} else {
// 成功した場合、キーハンドルはNCryptDeleteKeyによって自動的に解放される
NCryptFreeObject(hprov);
Ok(())
}
}
}
エラーコード
これは主なエラーコードと、それに対応する定数および説明の一覧です!
| コード (i32) | コード (hex) | 定数名 | 説明 |
|---|---|---|---|
| 0 | 0x00000000 | ERROR_SUCCESS | 操作が正常に完了しました |
| -2146893802 | 0x80090006 | NTE_BAD_KEYSET | 鍵が存在しないか、TPMに登録されていません |
| -2146893809 | 0x8009000F | NTE_EXISTS | 同じ名前の鍵がすでに存在します |
| -2146893786 | 0x80090026 | NTE_INVALID_HANDLE | ハンドル(プロバイダまたはキー)が無効です |
| -2146893785 | 0x80090027 | NTE_INVALID_PARAMETER | 1つ以上のパラメータが無効です |
| -2146893810 | 0x8009000E | NTE_NO_MEMORY | 操作を完了するためのメモリが不足しています |
| -2146893808 | 0x80090010 | NTE_PERM | アクセスが拒否されたか、操作が許可されていません |
| -2146893792 | 0x80090020 | NTE_FAIL | 内部エラーが発生しました |
| -2146893783 | 0x80090029 | NTE_NOT_SUPPORTED | アルゴリズムまたはオプションがサポートされていません |
| -2146893815 | 0x80090009 | NTE_BAD_FLAGS | dwFlags に指定されたフラグが無効です |
| -2146893784 | 0x80090028 | NTE_BUFFER_TOO_SMALL | 出力バッファが小さすぎます |
| -2147479534 | 0x80090032 | NTE_INVALID_STATE | オブジェクトの状態が無効です(例:未初期化など) |
| -2146893816 | 0x80090008 | NTE_BAD_ALGID | アルゴリズム識別子が無効です |
| -2146893814 | 0x8009000A | NTE_BAD_TYPE | 鍵またはオブジェクトの型が無効です |
解説
どの関数を使用するかや各引数の意味を理解するためにMicrosoftの公式ドキュメントを参照し、引数の型の確認には windows-sys の公式ドキュメントを参照しました!
参考資料:
- Microsoft公式ドキュメント(NCrypt) → ncrypt.h 概要
- windows-sys ドキュメント → Module Cryptography
ここでは、これらの公式資料から学んだ内容をもとに、コード内で使用している各関数と引数について解説していきます。
1: TPMプロバイダを開く
まず、NCryptOpenStorageProvider 関数を使用してプロバイダを開く必要があります。
Microsoft ドキュメント:

windows-sys ドキュメント:

phprovider はプロバイダハンドルを受け取るために、可変の usize である必要があります。
pszprovidername には次の3つのオプションがあります:

今回は MS_PLATFORM_CRYPTO_PROVIDER を使用していますが、PCに TPM が搭載されていない場合は代わりに MS_KEY_STORAGE_PROVIDER を使用することもできます。
この dwflags 引数は予約されているため、現在は常に 0 を指定します。
let mut hprov: NCRYPT_PROV_HANDLE = 0;
let status = NCryptOpenStorageProvider(
&mut hprov,
MS_PLATFORM_CRYPTO_PROVIDER,
0,
);
戻り値(一部):

関数が失敗した場合は、ハンドルにアクセスしたり解放したりしないでください!
↓ これについてはMicrosoftのドキュメントにも記載されています

2: 永続キーを作成(存在しない場合)
NCryptCreatePersistedKey 関数は、指定した名前で永続キーを作成できます。
Microsoft ドキュメント:

windows-sys ドキュメント:

phkey はキーハンドルを受け取るための可変な usize 参照である必要があります。
pszalgid は暗号アルゴリズムを指定する、ヌル終端されたUnicode文字列を指定します。
↓ 標準的な CNG アルゴリズム識別子 は以下のリンクから確認できます
CNG アルゴリズム識別子
今回の目的は鍵ラップなので、 BCRYPT_RSA_ALGORITHM を選択しました。
pszkeyname には、キー名を含むヌル終端されたUnicode文字列への参照を指定する必要があります。このパラメータを std::ptr::null() に設定すると、永続化されない一時キー(エフェメラルキー)が作成されます。
fn to_utf16(s: &str) -> Vec<u16> {
let mut utf16: Vec<u16> = OsStr::new(s).encode_wide().collect();
utf16.push(0);
utf16
}
encode_wide() は &OsStr を UTF-16 に変換してくれます。
また、pszkeyname の末尾にはヌル文字が必要なため、文字列の最後に 0 を追加しています。
dwlegacykeyspec には次の3つのオプションがあります:

CNG では、この値は 0 に設定する必要があります。
他のオプションは CryptoAPI 用であり、0 以外を指定するとエラー(NTE_NOT_SUPPORTED)が発生します。
dwflags には次の5つのオプションがあります:

ただし、これらのオプションは一般的な TPM 操作ではほとんど使用されません。
特に指定がない場合は、この値を 0 に設定します。
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let key_name = to_utf16(KEY_NAME);
let status_2 = NCryptCreatePersistedKey(
hprov,
&mut hkey,
BCRYPT_RSA_ALGORITHM,
key_name.as_ptr(),
0,
0,
);
if status_2 != 0 {
NCryptFreeObject(hprov); // プロバイダーハンドルを解放する
return Err(status_2)
}
戻り値(一部):

NCryptSetProperty 関数を使用するとキーのプロパティを設定できますが、ここでは省略しています。
キーを作成した後は、使用する前に NCryptFinalizeKey 関数を呼び出す必要があります。
Microsoft ドキュメント:

windows-sys ドキュメント:

dwflags は次の3つのオプションがあります:

NCRYPT_SILENT_FLAG はユーザーインターフェイスが表示されないようにするためのフラグですが、TPM にはそもそもユーザーインターフェイスが存在しないので、設定してもあまり意味はないです。
特に指定が不要な場合は、この値を 0 に設定します。
let status_2 = NCryptFinalizeKey(
hkey,
0,
);
if status_2 != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status_2)
}
戻り値(一部):

2: 既存キーを取得(すでに存在する場合)
すでにキーが存在する場合は、NCryptOpenKey 関数を使用してプロバイダからそのキーを取得できます。
Microsoft ドキュメント:

windows-sys ドキュメント:

これらの引数は、暗号アルゴリズムを除いて NCryptCreatePersistedKey と同じです。
let mut hkey: NCRYPT_KEY_HANDLE = 0;
let key_name = to_utf16(KEY_NAME);
let status = NCryptOpenKey(
hprov,
&mut hkey,
key_name.as_ptr(),
0,
0,
);
指定したキー名が存在しない場合、NTE_BAD_KEYSET エラーが発生します。
戻り値を定数と比較することで、match や if 文を使ってエラーコードを処理できます。
if status != 0 {
match status {
NTE_BAD_KEYSET => {
// このエラーは、TPMに指定のキーが登録されていない場合に発生する
// 指定した名前で永続的なRSAキーを作成する
let status_2 = NCryptCreatePersistedKey(
hprov,
&mut hkey,
BCRYPT_RSA_ALGORITHM,
key_name.as_ptr(),
0,
0,
);
if status_2 != 0 {
NCryptFreeObject(hprov);
return Err(status_2)
}
let status_2 = NCryptFinalizeKey(
hkey,
0,
);
if status_2 != 0 {
NCryptFreeObject(hprov);
return Err(status_2)
}
},
_ => {
NCryptFreeObject(hprov);
return Err(status)
},
}
}
戻り値(一部):

3: 暗号化データのサイズを取得
暗号化を実行する前に、暗号化後のデータサイズを取得する必要があります。
Microsoft ドキュメント:

windows-sys ドキュメント:

pbinput には、暗号化対象のデータを格納した u8 バッファへの参照を指定する必要があります。
cdinput には、暗号化対象のデータのバイト数を指定します。
ppaddinginfo は非対称鍵で使用される引数で、BCRYPT_OAEP_PADDING_INFO 構造体を指定する必要があります。dwflags で NCRYPT_PAD_OAEP_FLAG を選択した場合のみ必須です。
Microsoft ドキュメント:

windows-sys ドキュメント:

pszAlgId には、パディングの作成に使用するハッシュアルゴリズムのいずれかを指定する必要があります。
↓ 以下のリンクから確認できます
CNG アルゴリズム識別子
pbLabel はラベルを追加する場合に使用され、パディング生成用のデータを格納した可変の u8 バッファへの参照を指定します。
不要な場合は、この値を std::ptr::null_mut() に設定します。
cbLabel も同様にラベルを追加する場合に使用され、pbLabel バッファ内のバイト数を u32 値として指定します。
不要な場合は、この値を 0 に設定します。
fn create_padding_info() -> BCRYPT_OAEP_PADDING_INFO {
BCRYPT_OAEP_PADDING_INFO {
pszAlgId: BCRYPT_SHA256_ALGORITHM,
pbLabel: ptr::null_mut(),
cbLabel: 0,
}
}
このコードでは、ハッシュアルゴリズムとして BCRYPT_SHA256_ALGORITHM を使用しています。
pboutput には、暗号化されたデータを受け取るための可変 u8 バッファへの参照を指定します。
暗号化データのサイズだけを取得したい場合は、std::ptr::null_mut() を指定します。
cboutput には、暗号化データを受け取るバッファのサイズを示す u32 値を指定します。
pboutput が std::ptr::null_mut() に設定されている場合、この引数は無視されるらしいですが、実際に 0 以外を指定した場合に NTE_INVALID_PARAMETER エラーが発生しました。
pcbresult には、可変の u32 への参照を指定します。
pboutput が std::ptr::null_mut() に設定されている場合、この変数に暗号化データのサイズが格納されます。
dwflags には、非対称鍵用の4つのオプションがあります:

NCRYPT_PAD_OAEP_FLAG は、現代的なセキュリティでは推奨されるフラグです。
使用する場合は、BCRYPT_OAEP_PADDING_INFO 構造体への参照を渡す必要があります。
対称鍵を使用する場合は、この値を 0 に設定します。
let mut size: u32 = 0;
let padding_info = create_padding_info();
let status = NCryptEncrypt(
hkey,
target_key.as_ptr(),
target_key.len() as u32,
&padding_info as *const _ as *const c_void,
ptr::null_mut(), // データサイズを取得したい場合はnullを設定する
0, // データサイズを取得したい場合は0を設定する
&mut size,
NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status)
}
戻り値(一部):

4: 作成したキーで対象の鍵をラップ
最後に、NCryptEncrypt 関数を使って鍵をラップします!
let mut wrapped_key = vec![0u8; size as usize];
let status = NCryptEncrypt(
hkey,
target_key.as_ptr(),
target_key.len() as u32,
&padding_info as *const _ as *const c_void,
wrapped_key.as_mut_ptr(),
size,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status)
}
前の処理で取得したサイズを、バッファサイズとして cboutput に、暗号化後の実際のデータサイズとして pcbresult に指定する必要があります。
また、wrapped_key のサイズは usize 型であるため、実際の暗号化データサイズに合わせて切り詰める必要があります。
wrapped_key.truncate(size as usize);
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
最後に、関数を抜ける前にこれらのハンドルを必ず解放することを忘れないでください!
対象の鍵をアンラップ
暗号化された鍵の復号は、暗号化とほぼ同じ手順です。
違いは、NCryptEncrypt 関数を NCryptDecrypt 関数に置き換えるだけです。
let mut size: u32 = 0;
let padding_info = create_padding_info();
let status = NCryptDecrypt(
hkey,
target_key.as_ptr(),
target_key.len() as u32,
&padding_info as *const _ as *const c_void,
ptr::null_mut(),
0,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status)
}
// 鍵をアンラップする
let mut unwrapped_key = vec![0u8; size as usize];
let status = NCryptDecrypt(
hkey,
target_key.as_ptr(),
target_key.len() as u32,
&padding_info as *const _ as *const c_void,
unwrapped_key.as_mut_ptr(),
size,
&mut size,
NCRYPT_PAD_OAEP_FLAG,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
return Err(status)
}
戻り値(一部):

登録済みのキーを削除
NCryptDeleteKey 関数を使用して登録済みのキーを削除できます。
処理の手順は、キーを取得するまでの流れと同じです。
Microsoft ドキュメント:

windows-sys ドキュメント:

dwflags では NCRYPT_SILENT_FLAG オプションのみです。
特に必要がない場合は、この値を 0 に設定します。
let status = NCryptDeleteKey(
hkey,
0,
);
if status != 0 {
NCryptFreeObject(hkey);
NCryptFreeObject(hprov);
Err(status)
} else {
// 成功した場合、キーハンドルはNCryptDeleteKeyによって自動的に解放される
NCryptFreeObject(hprov);
Ok(())
}
関数が正常に実行されると、キーハンドルは自動的に解放されます。
戻り値を返す前に、NCryptFreeObject を呼び出してプロバイダハンドルのみを解放してください!
↓ これについてはMicrosoftのドキュメントにも記載されています。

戻り値(一部):

まとめ
最初はC++にあまり詳しくなかったため、どっちが引数名か型なのかすら分かりませんでした...。
しかし、実装を進める中で読み方だけでなく、TPMの仕組みについてやその他いろいろなことを学ぶことができました!
次は、Linux向けにもTPMの実装に挑戦してみる予定です。
最後まで読んでいただき、ありがとうございました!
Discussion