セキュリティ
Windows Vista の CNG API を使用して暗号化を行う
Kenny Kerr
この記事の内容 : : - CryptoAPI と CNG の違い
- アルゴリズム プロバイダ
- 乱数、暗号化、署名、および検証
- .NET アプリケーションとの相互運用
| この記事は次のテクノロジを使用しています: Windows Vista
|
この記事で使用しているコードのダウンロード:CNG2007_07.exe(158 KB)
オンラインでコードを参照

コンテンツ
Windows Vista™ では、これまでの CryptoAPI に代わる新しい暗号化 API が導入されます。CryptoAPI は、初期バージョンの Windows® NT や Windows 95 を起源とする暗号化 API です。Cryptography Next Generation (CNG) は、CryptoAPI を今後長期にわたって置き換え、CryptoAPI が提供してきたすべての暗号化プリミティブの代用となることを目的としています。CNG は、CryptoAPI が提供するアルゴリズムをすべてサポートしますが、これをさらに進化させ、多数の新しいアルゴリズムや柔軟性に優れたデザインが導入されています。そのため、開発者は、暗号化操作を実行する方法や、アルゴリズムを組み合わせてさまざまな操作を実行する方法をこれまでより細かく制御できるようになります。
図 1 は、CNG のデザイン概要を示しています。この記事は BCrypt を中心に説明しています。BCrypt とは、暗号化プリミティブ (乱数の生成、ハッシュ関数、署名、暗号化キーなど) を提供する CNG のサブセットです。それに対して、NCrypt とは、非対称キーの保存をサポートするキー記憶域機能やスマートカードなどのハードウェアを提供するサブセットです。BCrypt や NCrypt の名前をあまり深く考えないでください。BCrypt は、単に、CNG の基本サービスを提供するヘッダー ファイルと DLL の名前です。この場合 "B" は "base" (基本) を表します。同様に、NCrypt は、単に、高レベルのキー記憶域機能を提供するヘッダー ファイルと DLL の名前です。"N" は "new" (新規) を表します。
.gif)
図 1 CNG のアーキテクチャ
BCrypt が提供する暗号化プリミティブはカーネル モードで直接使用できるため、ユーザー モードとカーネル モードのアプリケーションの両方に提供される初めての共通暗号化フレームワークです。ただし、NCrypt が提供するキー記憶域機能は、ユーザー モードのアプリケーションでしか使用できません。
CNG が提供する暗号化プリミティブには、主に 2 つの考え方があります。1 つは一連の論理オブジェクトという考え方です。つまり、呼び出し可能なメソッドと、照会可能な (場合によっては変更可能な) プロパティを備えたオブジェクトです。このようなオブジェクトには、アルゴリズム プロバイダ、ハッシュ関数、キー、秘密協定があります。乱数ジェネレータ、署名、およびさまざまな種類のキーはどこに含まれるのでしょう。結局のところ、乱数生成はアルゴリズム プロバイダによって直接処理され、他のほとんどはキーによって提供されます。キーは対称キーまたは非対称キーとして表現でき、ハッシュ署名の署名や検証に使用されます。ハッシュ オブジェクトとキー オブジェクトは、アルゴリズム プロバイダから派生され、秘密協定は、キー オブジェクトのペアから派生されます。キー オブジェクトは、セキュリティが確保された通信を目的として、あるプリンシパルでは公開キーとなり、別のプリンシパルでは秘密キーとなります。
CNG のもう 1 つの考え方は、暗号化操作用のソフトウェア ルーターまたは中間層としての考え方です。CNG API は、一連の論理暗号化インターフェイスを基に構築されています。たとえば、ハッシュ インターフェイスを使用すれば、特定のハッシュ アルゴリズムの実装をハードコーディングする必要がなくなります。ほとんどの暗号化 API はアルゴリズム主体のアプローチを使用しますが、CNG はインターフェイス主体のアプローチを採用しています。このアプローチにより、出荷後にアプリケーションで使用しているアルゴリズムの不備に気付き、交換の必要がある場合、開発者もアプリケーション管理者も対応の敏捷性が向上します。図 2 に、このインターフェイス主体の CNG の概要を示します。
.gif)
図 2 CNG インターフェイス (画像を拡大するには、ここをクリックします)
CNG 構成システムについては、別途記事を用意すべきですが、現状では、CNG を使用すれば、開発者はアルゴリズム プロバイダを指定する必要なくアルゴリズムを要求できることを知っておけば十分です。アルゴリズムの実装はプロバイダが担当します。別のアルゴリズム実装を使用する場合、CNG のこうした側面により、管理者がアプリケーションで宣言するか、システム ポリシーを使用して、アプリケーションを再構成するだけです。
アルゴリズム プロバイダ
概要はこれぐらいにして、CNG を使用して一般的なさまざまな暗号化操作を行う方法を見ていきましょう。最初に必要なのがアルゴリズム プロバイダです。BCrypt で定義されているすべての CNG オブジェクトは、BCRYPT_HANDLE で特定されます。アルゴリズム プロバイダも例外ではありません。BCryptOpenAlgorithmProvider 関数は、選択したアルゴリズムとオプションの実装に基づいてアルゴリズム プロバイダを読み込み、その後 CNG 関数の呼び出し時に使用するハンドルを返します。また、BCrypt は、ユーザー モードとカーネル モードのどちらでコードを記述しているかに関係なく、Windows Driver Kit (WDK) の NTSTATUS 型を使用して、エラー情報を示します。アルゴリズム プロバイダをメモリに読み込む方法を次に示します。
BCRYPT_HANDLE algorithmProvider = 0;
NTSTATUS status = ::BCryptOpenAlgorithmProvider(
&algorithmProvider, algorithmName,
implementation, flags);
if (NT_SUCCESS(status))
{
// Use algorithm provider
}
ほとんどの場合、implementation パラメータと flags パラメータの両方に 0 を渡します。implementation に 0 を渡すと、algorithmName パラメータで指定される特定のアルゴリズム用に既定のアルゴリズム プロバイダが読み込まれます。これまでに WDK を使用したことがあれば、NT_SUCCESS マクロには馴染みがあるでしょう。これは、COM 開発者が使用する SUCCEEDED に似たマクロで、状態値が成功または失敗のどちらを表すかを示します。
アルゴリズム プロバイダは終了時にアンロードする必要があります。これには、次に示すように、BCryptOpenAlgorithmProvider から返されたハンドルを BCryptCloseAlgorithmProvider 関数に渡します。
status = ::BCryptCloseAlgorithmProvider(
algorithmProvider, flags);
ASSERT(NT_SUCCESS(status));
現時点では、この関数の flags は定義されていないため、flags パラメータには 0 を渡す必要があります。当然、アサーションはなくてもかまいませんが、アプリケーションのバグを除去する際に役立ちます。
この記事の残りの部分では、NT_VERIFY マクロを使用して CNG 関数の結果を検証します。このマクロは、返された状態コードを確認する必要があることを忘れないようにする単なるプレースホルダです。つまり、次のようにマクロを定義できます。
#define NT_VERIFY(x) ASSERT(NT_SUCCESS(x))
運用環境で実行するコードでは、必ず、プロジェクトのエラー処理方針に従ったエラー処理を使用してください。
アルゴリズム プロバイダの読み込み操作は負荷が高いため、アプリケーションでのプロバイダの使用パターンを検討し、適度に再利用することをお勧めします。次に、アルゴリズム プロバイダを使用してさまざまな目標を実現します。しっかりついてきてください。
乱数の生成
乱数の生成は、BCryptGenRandom 関数によって行われます。この処理ではバッファ全体に乱数を設定できるため、ランダム バッファ生成と呼ばれることもあります。たとえば、10 個の乱数を生成する必要があるとします。図 3 にその方法を示します。
Figure 3 乱数
BCRYPT_HANDLE algorithmProvider = 0;
NT_VERIFY(::BCryptOpenAlgorithmProvider(
&algorithmProvider,
BCRYPT_RNG_ALGORITHM,
0, // implementation,
0)); // flags
for (int i = 0; i < 10; ++i)
{
UINT random = 0;
NT_VERIFY(::BCryptGenRandom(
algorithmProvider,
reinterpret_cast<PUCHAR>(&random),
sizeof(UINT),
0));
cout << random << endl;
}
この例では、BCRYPT_RNG_ALGORITHM アルゴリズム識別子を指定して、既定の乱数生成アルゴリズムを要求しています。デジタル署名アルゴリズム (DSA) に使用する Federal Information Processing Standards (FIPS) 準拠のアルゴリズムが必要であれば、BCRYPT_RNG_FIPS186_DSA_ALGORITHM アルゴリズム識別子も使用できます。
図 3 の例からわかるように、BCryptGenRandom 関数には、実用性の高い void (任意の型) のポインタではなく、バッファを特定する UCHAR へのポインタが必要です。さいわい、C++ のおかげで、タイプセーフな方法でこれを簡単に解決できます。次の関数テンプレートを見てください。
template <typename T>
__checkReturn
NTSTATUS GenRandom(BCRYPT_HANDLE algorithmProvider,
T& buffer, ULONG flags)
{
ASSERT(0 != algorithmProvider);
return ::BCryptGenRandom(algorithmProvider,
reinterpret_cast<PUCHAR>(&buffer),
sizeof(T), flags);
}
Standard Annotation Language (SAL) に馴染みのない読者のために説明すると、__checkReturn マクロは、関数の呼び出し側が関数の結果をチェックすることを示す、分析ツールへのヒントにすぎません。
この関数テンプレートは、あらゆる場合に適しているとは限りませんが (コレクション クラス内にラップされているバッファに設定する場合など)、キャストが関数テンプレート内に分離されるため、ほとんどの場合、乱数の生成をさらに実行しやすくします。また、テンプレートでは、バッファのサイズも自動的に決まります。10 個のランダムな GUID 値を生成する次の例を考えてみます。
for (int i = 0; i < 10; ++i)
{
GUID random = { 0 };
NT_VERIFY(GenRandom(algorithmProvider, random, 0));
// Use ‘random’ value
}
BCryptGenRandom 関数は、オプションのフラグをサポートします。このフラグにより、乱数生成アルゴリズムにエントロピーを追加できます。BCRYPT_RNG_USE_ENTROPY_IN_BUFFER フラグは、関数に渡されたバッファ内の値をアルゴリズムで使用する必要があることを示します。この値は、乱数を計算し、後で同じバッファに返す際に追加のエントロピーとして使用されます。次に例を示します。
UINT random = 0;
for (int i = 0; i < 10; ++i)
{
NT_VERIFY(GenRandom(algorithmProvider, random,
BCRYPT_RNG_USE_ENTROPY_IN_BUFFER));
// Use ‘random’ value
}
乱数変数の定義をループの外側に移動したため、前の乱数値が次に生成される数値の追加のエントロピーになることに注意してください。
ハッシュ関数
乱数の生成と同様に、現代のコンピューティングの多くの側面でセキュリティ対策や機能を実現する際に、ハッシュ関数が重要な役割を果たします。ハッシュ関数は CNG ではオブジェクトとして公開されますが、API はカーネル モードのコード (大部分は C で記述されています) からアクセスできる必要があるため、オブジェクトを戻すにはあらかじめ作業が必要です。最初に説明したように、CNG では、アルゴリズム プロバイダとハッシュ関数がオブジェクトで表されます。これらのオブジェクトは BCrypt に含まれるため、BCryptGetProperty 関数と BCryptSetProperty 関数を使用すれば、特定のオブジェクトの名前付きプロパティの照会と設定が可能です。オブジェクトのプロパティを処理するために、NCrypt にも対応する関数が用意されています。
BCryptCreateHash 関数では、新しいハッシュ オブジェクトを作成します。ただし、その関数を呼び出す前に、ハッシュ関数の処理に使用するバッファを割り当てておく必要があります。ここで、1 つの API でユーザー モードとカーネル モードの両方をサポートすることによる悪影響を 1 つ確認できます。カーネル モードでは、メモリの割り当て場所 (特に、ページ メモリと非ページ メモリのどちらから割り当てが行われるか) に細心の注意が必要です。ユーザーは、バッファ以外に、ハッシュ オブジェクトのハンドル リソースとハッシュ テーブル リソースの管理を担当します。
ハッシュ オブジェクトの作成に必要なバッファのサイズは、アルゴリズム プロバイダのプロパティで、このために照会するプロパティの名前は BCRYPT_OBJECT_LENGTH 識別子で指定します。図 4 は、SHA-256 ハッシュ アルゴリズム用アルゴリズム プロバイダを読み込み、ハッシュ バッファ サイズを照会します。
Figure 4 アルゴリズム プロバイダを使用する
BCRYPT_HANDLE algorithmProvider = 0;
NT_VERIFY(::BCryptOpenAlgorithmProvider(
&algorithmProvider,
BCRYPT_SHA256_ALGORITHM,
0, // implementation,
0)); // flags
ULONG hashBufferSize = 0;
ULONG bytesCopied = 0;
NT_VERIFY(::BCryptGetProperty(
algorithmProvider,
BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&hashBufferSize),
sizeof(ULONG),
&bytesCopied, 0));
ASSERT(sizeof(ULONG) == bytesCopied);
BCryptGetProperty の最初のパラメータは、照会するオブジェクトを示します。2 番目のパラメータはプロパティ名を示します。3 番目と 4 番目のパラメータは、プロパティ値が格納されるバッファとバッファのサイズを指定します。bytesCopied パラメータは、予測されるバッファ サイズが前もってわからない場合に便利です。現時点では、この関数の flags は定義されていないため、flags パラメータには 0 を渡す必要があります。
ハッシュ バッファのサイズが決まったら、ハッシュ オブジェクト用にメモリ チャンクをコミットする必要があります。カーネル モードを以外では、このバッファの割り当て先の場所はそれほど問題になりません。また、安定してさえいれば、どのような記憶域でもほぼ自由に使用できます (つまり、ピン設定が解除されたマネージ バイト配列は使用できません)。図 5 に、単純なユーザー モード バッファ割り当てに使用できる簡単な Buffer クラスを示します。このクラスは暗号化キー オブジェクトの作成時に再度使用します。そのため、ここで何らかの Buffer クラスを用意しておくことは価値があります。
前提条件がすべてそろったら、最後に、次に示すように、BCryptCreateHash 関数を使用してハッシュ オブジェクトを作成できます。
Buffer hashBuffer;
NT_VERIFY(hashBuffer.Create(hashBufferSize));
BCRYPT_HANDLE hash = 0;
NT_VERIFY(::BCryptCreateHash(algorithmProvider, &hash,
hashBuffer.GetData(),
hashBuffer.GetSize(),
0, // secret
0, // secret size
0)); // flags
BCryptCreateHash の最初のパラメータは、ハッシュ インターフェイスを実装するアルゴリズム プロバイダを示します。2 番目のパラメータは、ハッシュ オブジェクトへのハンドルを受け取ります。3 番目と 4 番目のパラメータは、ハッシュ バッファとそのサイズを指定します。キー付きハッシュ アルゴリズムでは、5 番目と 6 番目のシークレット パラメータを使用して秘密キーを指定します。現時点では、この関数の flags は定義されていないため、flags パラメータには 0 を渡す必要があります。ハッシュ オブジェクトの操作が完了したら、BCryptDestroyHash 関数を使用してオブジェクトを破棄し、ハッシュ オブジェクトのバッファを解放する必要があります。
ハッシュ オブジェクトの作成方法と破棄方法がわかったところで、実際に、ハッシュ オブジェクトを使って何か役立つことをしてみましょう。ハッシュ オブジェクトが作成されたら、BCryptHashData 関数を呼び出し、バッファの一方向ハッシュを実行できます。この関数を繰り返し呼び出して、追加のバッファをハッシュにまとめることができます。ハッシュ値のサイズはアルゴリズム プロバイダによって固定および決定されるため、BCryptHashData を何度呼び出しても、生成されるハッシュ値は常に同じサイズになることに注意してください。
この例では、COM IStream インターフェイスを使用してストリームから読み取ったデータをハッシュする方法を示します。
BYTE buffer[256] = { 0 };
ULONG bytesRead = 0;
while (SUCCEEDED(stream->Read(buffer, _countof(buffer),
&bytesRead)) && 0 < bytesRead)
{
NT_VERIFY(::BCryptHashData(hash, buffer, bytesRead, 0));
}
BCryptHashData の最初のパラメータは、BCryptCreateHash 関数から返されたハンドルです。2 番目と 3 番目のパラメータは、ハッシュするデータを含むバッファと、そのバッファのサイズを示します。ストリームから実際に読み取ったデータのみが含まれるように、バッファのサイズに bytesRead を渡すようにしたことに注目してください。最後に、BCryptHashData では flags がまだ定義されていないため、最後のパラメータには 0 を渡す必要があります。
すべてのデータをハッシュ関数に渡せば、BCryptFinishHash 関数を呼び出して、結果のハッシュ値を取得できます。結果のハッシュ値のサイズは、使用する特定のハッシュ アルゴリズムによって異なります。このサイズがわからない場合は、BCRYPT_HASH_LENGTH 識別子を使用してハッシュ オブジェクトに照会できます。次の例では、ハッシュ値のサイズを照会して、そのバッファを作成し、最後に、BCryptFinishHash を使用してハッシュ値をバッファにコピーします。
ULONG hashValueSize = 0;
NT_VERIFY(::BCryptGetProperty(hash, BCRYPT_HASH_LENGTH, ...))
Buffer hashValue;
NT_VERIFY(hashValue.Create(hashValueSize));
NT_VERIFY(::BCryptFinishHash(hash,
hashValue.GetData(),
hashValue.GetSize(),
0)); // flags
このパターンは、おそらくよく知っているでしょう。BCryptFinishHash の最初のパラメータは、ハッシュ オブジェクトのハンドルです。2 番目と 3 番目のパラメータは、ハッシュ値を受け取るバッファと、そのバッファのサイズを示します。最後のパラメータは flags を示します。これは、現在存在しません。
最後に、ハッシュ オブジェクトは複製できます。これは、何らかの共通データに基づいて複数のハッシュ値を作成する場合に役立ちます。最初に、1 つのハッシュ オブジェクトを作成して共通データをハッシュした後、1 回以上そのオブジェクトを複製して、複製するオブジェクトに差分を追加することができます。ハッシュ オブジェクトが複製されると、2 つのハッシュ オブジェクトは同じ状態を保持しますが、相互に結び付きはありません。そのため、各ハッシュ オブジェクトに一意なデータを追加したり、ハッシュ値を生成したり、一方のオブジェクトに影響を与えることなくもう一方のオブジェクトを破棄したりできます。
BCryptDuplicateHash 関数が、ハッシュ オブジェクトの複製を処理します。必要なのは、複製するハッシュ オブジェクトへのハンドルと、処理に使用する新しいバッファを用意することだけです。
Buffer newHashBuffer;
NT_VERIFY(newHashBuffer.Create(hashBufferSize));
BCRYPT_HANDLE newHash = 0;
NT_VERIFY(::BCryptDuplicateHash(hash,
&newHash,
newHashBuffer.GetData(),
newHashBuffer.GetSize(),
0));
対称暗号化
対称暗号化には、対称キーとそのオブジェクト バッファの作成と準備が必要です。この点は、ハッシュ関数とほぼ同様です。おなじみの手順として、アルゴリズム プロバイダの作成、プロバイダの BCRYPT_OBJECT_LENGTH プロパティの照会によるキー オブジェクト バッファ サイズの決定、そのサイズのバッファの割り当てを実行します。キーを初期化するシークレットを指定して BCryptGenerateSymmetricKey 関数を呼び出すと、対称キーが作成されます。図 6 に、このプロセスを示します。
.gif)
図 6 対称キー オブジェクトを作成する
ここまでにアルゴリズム プロバイダの開始とバッファの作成については既に説明したので、先に進み、対称キーの生成に注目します。次のコードは、通常の BCryptGenerateSymmetricKey の呼び出しの例を示しています。
BCRYPT_KEY_HANDLE key = 0;
NT_VERIFY(::BCryptGenerateSymmetricKey(algorithmProvider,
&key,
keyBuffer.GetData(),
keyBuffer.GetSize(),
secret.GetData(),
secret.GetSize(),
0)); // flags
最初のパラメータは、対称暗号化アルゴリズムを実装するアルゴリズム プロバイダを示します。2 番目のパラメータは、キー オブジェクトへのハンドルを受け取ります。3 番目と 4 番目のパラメータは、キー バッファとそのサイズを指定します。その次の 2 つのパラメータは、送信者と受信者が共有する秘密キーを含むバッファを示します。これはバイト配列にできます。空にすることもできますが、通常は、何らかのパスワードのハッシュを指定します。現時点では、この関数の flags は定義されていないため、flags パラメータには 0 を渡す必要があります。
キー オブジェクトの操作が完了したら、BCryptDestroyKey 関数を使用してオブジェクトを破棄し、キー オブジェクト バッファを解放する必要があります。
キー オブジェクトを作成すると、実際にデータの暗号化や暗号化解除を実行したくなってきたでしょう。では、早速取りかかりましょう。わかりやすい名前の BCryptEncrypt 関数と BCryptDecrypt 関数を使用して、対称キーと非対称キー両方のデータの暗号化と暗号化解除を行います。パディング方式と初期化ベクタ以外は、使用中のキーの種類に関係なく、関数は同じように使用します。これらの関数を初めて見ると、パラメータ リストが長くて脅威を感じるかもしれませんが、データの暗号化解除を成功させるために一致させる必要がある、一連の共通パラメータを受け取ることだけを注意してください。対称暗号化操作の送信者と受信者は、共通プロパティをいくつか共有する必要があります。両者には、同じシークレットを使用して作成されたキーと、対応するプロパティ値が必要です。また、初期化ベクタも、パディング方式も同じである必要があります。
対称暗号化は、驚くほど簡単に使用できます。このようなプロパティすべてを当事者間で共有することを検討すると、問題が発生します。確認が必要な最初の情報として、アルゴリズムに対するデータ ブロックのサイズがあります。
ULONG blockSize = 0;
NT_VERIFY(::BCryptGetProperty(key, BCRYPT_BLOCK_LENGTH, ...))
最も一般的な形式の対称アルゴリズムであるブロック暗号化アルゴリズムを使用するときは、さまざまな理由からブロックのサイズが役に立ちます。ブロック暗号化では、固定サイズのプレーンテキスト ブロックを同じサイズの暗号化テキスト ブロックに暗号化します。初期ベクタを使用する場合、このブロックのサイズは初期化ベクタのサイズも示します。
暗号化するメッセージと初期化ベクタの準備ができたら、BCryptEncrypt 関数を呼び出して、プレーンテキスト メッセージを暗号化することができます。当然、暗号化テキストを受け取るバッファのサイズを決める必要があります。これを行うには、output パラメータと output size パラメータの両方に 0 を渡して、BCryptEncrypt を呼び出すだけです。その結果、BCryptEncrypt から必要なサイズが返されます。
ULONG ciphertextSize = 0;
NT_VERIFY(::BCryptEncrypt(key,
message.GetData(),
message.GetSize(),
0, // padding info
iv.GetData(),
iv.GetSize(),
0, // output
0, // output size,
&ciphertextSize,
0)); // flags
最初のパラメータは、暗号化に使用するキーを示します。2 番目と 3 番目のパラメータは、暗号化するメッセージを指定します。4 番目のパラメータは、非対称暗号化の追加パディング情報を指定します。対称アルゴリズムではこのパラメータが使用されないため、0 を渡す必要があります。次の 2 つのパラメータは、使用する初期化ベクタを含むバッファを示します。最後から 2 番目のパラメータは、予想される暗号化テキストのサイズを受け取り、最後のパラメータは省略可能なパラメータを受け取ります。対称アルゴリズムでは BCRYPT_BLOCK_PADDING フラグのみが使用されます。このフラグは、特定のメッセージをブロック サイズの倍数になるようにパディングすることを考える必要がない場合に便利です。
次に、暗号化テキスト バッファを作成して、BCryptEncrypt を再度呼び出すことができます。このとき、暗号化テキストを受け取るバッファを指定します。
Buffer ciphertext;
NT_VERIFY(ciphertext.Create(ciphertextSize));
NT_VERIFY(::BCryptEncrypt(key,
message.GetData(),
message.GetSize(),
0, // padding info
iv.GetData(),
iv.GetSize(),
ciphertext.GetData(),
ciphertext.GetSize(),
&ciphertextSize,
0)); // flags
暗号化解除のプロセスもほぼ同じです。プレーンテキストのサイズがわからない場合は、暗号化テキストのサイズを決定するために、上記とほぼ同様に、CryptDecrypt 関数を呼び出します。最後に、暗号化テキスト、初期化ベクタ、およびプレーンテキスト バッファを指定して BCryptDecrypt を呼び出し、結果として暗号化解除されたメッセージを受け取ります。
Buffer plaintext;
NT_VERIFY(plaintext.Create(plaintextSize));
NT_VERIFY(::BCryptDecrypt(newKey,
ciphertext.GetData(),
ciphertext.GetSize(),
0, // padding info
iv.GetData(),
iv.GetSize(),
plaintext.GetData(),
plaintext.GetSize(),
&plaintextSize,
0)); // flags
メッセージの暗号化と同様に、メッセージの暗号化解除時に必ず同じフラグを指定します。
非対称暗号化
非対称暗号化では、両当事者が秘密キーと公開キーを保持する公開キー アプローチを使用して、シークレットや初期化ベクタの共有に関する問題を解決します。公開キーはだれでも使用できます。公開キーで暗号化されたデータの暗号解読には秘密キーしか使用できないように、キーどうしが関連付けられています。当然、この追加機能にはコストがかかります。また、非対称暗号化は、対称暗号化に比べて、計算に相当高いコストがかかることがわかります。それにもかかわらず、通信の確立時に、当事者が安全に対称暗号化キー情報を共有するためのメカニズムを提供するということだけでも、非常に重要です。図 7 に、非対称キーの作成プロセスを示します。
.gif)
図 7 非対称キーを作成する
BCryptGenerateKeyPair 関数を呼び出して公開キーと秘密キーのペアを生成する例を次に示します。
NT_VERIFY(::BCryptGenerateKeyPair(algorithmProvider,
&key, keySize,
0)); // flags
最初のパラメータは、非対称暗号化アルゴリズムを実装するアルゴリズム プロバイダを示します。2 番目のパラメータは、キー オブジェクトへのハンドルを受け取ります。3 番目のパラメータは、アルゴリズムで使用するキーのサイズを示します。この値は、バイト単位ではなくビット単位で表され、アルゴリズムによって異なります。たとえば、RSA アルゴリズムは、64 の倍数である 512 ビットから 16384 ビットまでのキー サイズをサポートします。一般に、キー サイズはアルゴリズムのパフォーマンスに直接影響しますが、もっと重要なのは、ブルート フォース攻撃を使用して暗号を解読する際のコストに影響することです。それほど重要ではありませんが、キー サイズは、アルゴリズムで使用するブロックのサイズも示します。ブロック サイズを決定するには、キーのサイズを 8 で割るだけです。
BCryptGenerateKeyPair を使用してキー ペアを生成したら、BCryptSetProperty 関数を使用して、アルゴリズム固有のさまざまなキー プロパティの設定が必要になる場合があります。ただし、キー ペアを使用する前に、次のように、BCryptFinalizeKeyPair 関数を呼び出して、キー オブジェクトの作成を終了しておく必要があります。
NT_VERIFY(::BCryptFinalizeKeyPair(key, 0));
この時点で、さいわいにも、既に説明した BCryptEncrypt 関数と BCryptDecrypt 関数を使用して、データの暗号化と暗号化解除を行うことができます。この場合、非対称キーでは初期化ベクタが無視される点だけがやや異なります。また、パディング方式は、選択するフラグによっては少し複雑になる場合があります。暗号化されるメッセージが、ブロック サイズの倍数のサイズのバッファで指定されているとすると、パディングの必要はないため、BCRYPT_PAD_NONE フラグを指定するだけです。一方、メッセージを暗号化する場合に、最初にブロック サイズの倍数になるようにパディングする必要がないときは、いくつかのオプションがあります。最も簡単なオプションは BCRYPT_PAD_PKCS1 フラグを指定することです。これにより、アルゴリズム プロバイダは、PKCS-1 標準に基づく乱数を使用して、入力バッファがブロック サイズの倍数になるようにパディングするよう指示されます。
当然ながら、キー ペアの公開部分を共有できない場合は、非対称暗号化はほぼ役に立ちません。BCryptExportKey 関数と BCryptImportKeyPair 関数は、キーのエクスポートとインポートに使用できます。ついでながら、対称キーのエクスポートには BCryptExportKey も使用できますが、バッファをキー オブジェクトに関連付ける必要があるため、対称キーのインポートには BCryptImportKey 関数を使用する必要があります。BCryptExportKey と BCryptImportKeyPair は、公開キーと秘密キーのペアまたは公開キーのみをエクスポートする際に使用できます。たとえば、キー ペアをエクスポートして後で使用するために保存したり、公開キーを別個にエクスポートして通信相手に提供したりすることが考えられます。
図 8 のコードは、公開キーのみを含むバイナリ BLOB を作成する方法を示しています。ご覧のとおり、BCryptExportKey は、既に説明したパターンに従い、最初に BLOB のサイズを決定するために呼び出し、次に実際に公開キー情報を受け取るために再度呼び出します。この関数の 3 番目のパラメータは注目する必要があり、アルゴリズムとエクスポートされる正確な内容の両方を示します。
Figure 8 公開キー バイナリ BLOB を作成する
ULONG publicKeyBlobSize = 0;
NT_VERIFY(::BCryptExportKey(key,
0, // reserved
BCRYPT_RSAPUBLIC_BLOB,
0, // output,
0, // output size
&publicKeyBlobSize,
0)); // flags
Buffer publicKeyBlob;
NT_VERIFY(publicKeyBlob.Create(publicKeyBlobSize));
NT_VERIFY(::BCryptExportKey(key,
0, // reserved
BCRYPT_RSAPUBLIC_BLOB,
publicKeyBlob.GetData(),
publicKeyBlob.GetSize(),
&publicKeyBlobSize,
0)); // flags
この例では、BCRYPT_RSAPUBLIC_BLOB を使用して、RSA キー ペアの公開キー部分のみをエクスポートする方法を示しました。公開キーと秘密キーの両方をエクスポートする場合は、代わりに BCRYPT_RSAPRIVATE_BLOB を使用するだけです。名前に惑わされないでください。秘密キーと公開キーの両方が含まれています。暗号化には公開キーのみが使用されているので、これを把握することが重要です。そのため、秘密キーをエクスポートしてからインポートした場合、メッセージを暗号化することはできますが、暗号化には秘密キーではなく、公開キーが使用されます。秘密キーは、暗号化解除のみに使用されます。
BLOB からキーをインポートするのは簡単です。
BCRYPT_HANDLE publicKey = 0;
NT_VERIFY(::BCryptImportKeyPair(algorithmProvider,
0, // reserved
BCRYPT_RSAPUBLIC_BLOB,
&publicKey,
publicKeyBlob.GetData(),
publicKeyBlob.GetSize(),
0)); // flags
3 番目のパラメータに再び注目する必要があり、インポートされる BLOB の詳細を示します。アルゴリズム プロバイダにインポートしたアルゴリズムを実装するだけで、正常に機能します。その後、結果として生成される公開キー ハンドルを使用すると、秘密キーの所有者だけが解読できるように、メッセージを安全に暗号化できます。
署名と検証
非対称暗号化の一般的な使用方法として、デジタル署名の作成があります。デジタル署名は、Windows や Microsoft® .NET Framework でよく使用され、メッセージ、実行可能ファイル、およびアセンブリの正当性を証明します。
既に説明した非対称暗号化や暗号化解除のプロセスとは異なり、署名は、作成に秘密キーを、検証に公開キーを使用します。そのため、公開キーを保持していれば、秘密キーの所有者が生成した特定の署名を検証できます。その秘密キーにアクセスする必要ありません。
基本は既に説明したため、署名を作成または検証するプロセスは非常に簡単です。最初に、署名するデータを特定した後、上記のハッシュ関数の説明に従ってデータをハッシュします。署名は、ハッシュ値とデジタル署名アルゴリズム用の秘密キーを基に、BCryptSignHash 関数で計算されます。図 9 に簡単な例を示します。
Figure 9 署名を計算する
ULONG signatureSize = 0;
NT_VERIFY(::BCryptSignHash(keyPair,
0, // padding info
hashValue.GetData(),
hashValue.GetSize(),
0, // output
0, // output size
&signatureSize,
0)); // flags
Buffer signature;
NT_VERIFY(signature.Create(signatureSize));
NT_VERIFY(::BCryptSignHash(keyPair,
0, // padding info
hashValue.GetData(),
hashValue.GetSize(),
signature.GetData(),
signature.GetSize(),
&signatureSize,
0)); // flags
通常、最初の呼び出しで、作成した署名のサイズが計算され、次の呼び出しで、実際に署名が算出されます。ハッシュ値のサイズと使用するアルゴリズムによっては、パディング情報の追加指定が必要になる場合があります。
署名の検証はさらに簡単です。つまり、ハッシュを別個に計算してから、このハッシュ値、署名者の公開キー、および受け取った署名を BCryptVerifySignature 関数に渡して検証するという考え方です。BCryptVerifySignature は、署名がハッシュ値と一致する場合に STATUS_SUCCESS を返し、一致しない場合に STATUS_INVALID_SIGNATURE を返します。図 10 にこの考え方の例を示します。
Figure 10 署名を検証する
NTSTATUS status = ::BCryptVerifySignature(publicKey,
0, // padding info
hashValue.GetData(),
hashValue.GetSize(),
signature.GetData(),
signature.GetSize(),
0); // flags
switch (status)
{
case STATUS_SUCCESS:
{
// The signature matches the hash.
break;
}
case STATUS_INVALID_SIGNATURE:
{
// The signature does not match the hash.
break;
}
default:
{
// An error occurred.
}
}
.NET との相互運用
.NET Framework 2.0 および 3.0 に付属する暗号化ライブラリでは、CryptoAPI からのネイティブ実装のラッパーだけでなく、さまざまなアルゴリズムのマネージ実装を提供しています。多くの場合、問題なく CNG データと相互運用できますが、.NET Framework 暗号化クラスの中には、不適切なアルゴリズム プロパティを既定のプロパティとして使用しているものもあります。これらのプロパティによって、トラブルシューティングを余儀なくされることもあります。このような場合の秘訣として、各キーのプロパティが CNG とマネージ実装との間で等しくなるように一致させると、正常に機能するようになります。
いくつかの点では、マネージ暗号化クラスよりも CNG を使用する方がかなり簡単です。これは、考慮すべき抽象型や継承される既定値が少ないためです。効果的な暗号化には、すべての詳細に詳しいプログラマが頼りです。
対称暗号化の場合、CNG と .NET Framework との間で暗号化テキストを交換できるように、.NET Framework SymmetricAlgorithm クラスのプロパティである BlockSize、IV、Key、KeySize、Mode、および Padding に注意してください。これらのうち 1 つを少しでも間違えると、エラーになります。CNG と .NET Framework で使用される既定値は必ずしも同じものとは限りませんが、通常、実装間でブロック サイズやパディング方式などに互換性のある共通の値を見つけることができます。
アルゴリズムによって使用するプロパティがさまざまなので、実装間で非対称暗号化を機能させるには手間がかかります。そのため、一致する共通プロパティ リストを見つけるだけでは、非対称暗号化を機能させることはできません。代わりに、さまざまな実装でキー情報をインポートおよびエクスポートする方法に重点を置き、自身で必要な変換を用意する必要があります。実装間で公開キーを通信するプロセスを示す具体的な例を見てみましょう。
RSA アルゴリズムでは、一般的なパラメータとして、公開キーと秘密キーの指数、モジュール (法)、1 組の素数、もう 1 組の指数、および係数を受け取ります。暗号学者や数学者であればこれに興味があるかもしれませんが、ほとんどの開発者が知りたいのは、キー ペアの公開部分を共有する方法だけです。RSA アルゴリズムの場合は、公開キーの指数とモジュールの値だけを共有する必要があります。
CNG が提供する BCryptExportKey 関数は、必要に応じて保持または共有できるバイナリ BLOB に暗号化キーをエクスポートする際に役立ちます。この関数も繰り返し説明してきたのと同じパターンに従っていることに注意してください。ここでは、最初に BLOB のサイズを決定するために呼び出してから、エクスポートされたキー情報を受け取れるように、新しく割り当てたバッファを指定して再度呼び出します。説明が必要なのは、次に示す 3 番目のパラメータのみです。
NT_VERIFY(::BCryptExportKey(key,
0, // reserved
BCRYPT_RSAPUBLIC_BLOB,
blob.GetData(),
blob.GetSize(),
&blobSize,
0)); // flags
このパラメータは、生成する BLOB 型を示します。この例の BCRYPT_RSAPUBLIC_BLOB 識別子は、関数が RSA キーの公開キー部分のみをエクスポートすることを示しています。
ここからが面白い部分です。.NET Framework RSA クラスがキー情報のインポートに別の形式を使用するため、その BLOB を開いてその内容を確認する必要があります。実際の BLOB は、ヘッダーとして先頭に BCRYPT_RSAKEY_BLOB 構造体を備えた単純な形式です。このヘッダーでは、BLOB の型だけでなく、さまざまなキー パラメータの領域を示します。図 11 に、公開キー BLOB のメモリ レイアウトを示します。
.gif)
図 11 公開キー BLOB のメモリ レイアウト
この情報がわかれば、必要なメモリ範囲をコピーして、RSAParameters 構造体に設定するだけです。この設定は、キー情報をインポートする場合に .NET Framework RSA クラスで必要になります。
図 12 に示すように、C++/CLI を使用して、ネイティブ RSA キー BLOB をマネージ RSAParameters 構造体に直接変換しました。この構造体の指数フィールドとモジュール フィールドには、新しく作成したマネージ バイト配列が割り当てられます。その後、最初に値をピン設定してから memcpy_s 関数を使用すると、値がこれらのバイト配列に直接コピーされます。たとえば、C# でバイナリ BLOB を受け取ったとすると、.NET Framework BinaryReader クラスを使用して同じ結果を得ることができます。残りのプロセスは簡単です。
Figure 12 RSA キー BLOB を .NET 構造体に変換する
BCRYPT_RSAKEY_BLOB* rsaBlob =
reinterpret_cast<BCRYPT_RSAKEY_BLOB*>(blob.GetData());
RSAParameters rsaParams;
rsaParams.Exponent = gcnew array<BYTE>(rsaBlob->cbPublicExp);
{
pin_ptr<BYTE> destination = &rsaParams.Exponent[0];
const BYTE* source = reinterpret_cast<BYTE*>(rsaBlob);
memcpy_s(destination,
rsaParams.Exponent->Length,
source + sizeof(BCRYPT_RSAKEY_BLOB),
rsaBlob->cbPublicExp);
}
rsaParams.Modulus = gcnew array<BYTE>(rsaBlob->cbModulus);
{
pin_ptr<BYTE> destination = &rsaParams.Modulus[0];
const BYTE* source = reinterpret_cast<BYTE*>(rsaBlob);
memcpy_s(destination,
rsaParams.Modulus->Length,
source + sizeof(BCRYPT_RSAKEY_BLOB) + rsaBlob->cbPublicExp,
rsaBlob->cbModulus);
}
RSACryptoServiceProvider rsa;
rsa.ImportParameters(rsaParams);
array<BYTE>^ ciphertext = rsa.Encrypt(plaintext, false);
RSACryptoServiceProvider クラスは、CryptoAPI に基づいて作成された .NET Framework RSA アルゴリズムの実装です。ImportParameters メソッドでは、準備した RSAParameters 構造体がインポートされます。Encrypt メソッドでは、先に進み、公開キーを使用してメッセージを暗号化します。このメッセージは、秘密キーを使用しないと、暗号化解除できません。
今後の展望
この記事の執筆時点では、マイクロソフトは Visual Studio の次期バージョン (コードネーム "Orcas") の開発に取り組んでいます。また、そのリリースにあわせて、.NET Framework の新しいバージョンについても見ていく予定です。.NET Framework 3.5 では、CNG に基づいた新しいアルゴリズムの実装が数多く導入されます。.NET Framework 3.5 がリリースされると、特に暗号化の分野で、ネイティブ コードとマネージ コード間の相互運用性がさらに向上することも予想できます。CNG が Windows プラットフォームの主要な暗号化プロバイダになるため、フレームワークに追加されたこうした新機能は、既存の実装の多くに置き換わります。以前 .NET Framework では見られなかった新しいアルゴリズムの実装もいくつか導入されています。このような実装は、CNG のキー記憶域機能によって提供される楕円曲線アルゴリズムの一部に対応しています。こうした実装は、この記事の冒頭部分で触れた NCrypt として知られている、CNG のサブセットに分類されます。この詳細については、「Cryptographic Next Generation のリソース」を参照してください。
Kenny Kerr は、Windows のソフトウェア開発を専門にしているソフトウェア設計者です。彼はプログラミングおよびソフトウェア設計に関して執筆を行い、開発者を指導しています。連絡先は
weblogs.asp.net/kennykerr (英語) です。