EternalWindows
CSP / 鍵ペアの作成

セッション鍵によって暗号化されたデータを複合化するには、 暗号化の際に使用したセッション鍵と同一の鍵が必要です。 この事実は、暗号化したデータをネットワーク上に送信するような場合、 受信側が事前にセッション鍵を持っていなければならないことを意味しますが、 ここでいくつかのセキュリティ的な懸念が生じます。 たとえば、送信したセッション鍵を第三者に傍受された場合、 当事者間で暗号化されて送受信されるデータは、 その第三者によって複合化されてしまうことになります。 これは、暗号化と複合化に同じ鍵を利用するというセッション鍵特有の問題です。 また、セッション鍵のパスワードを送信するという方法も、 やはりそれを傍受されてしまっては、第三者はセッション鍵を作成することができてしまいます。

公開鍵/秘密鍵を利用した公開鍵暗号は、このセッション鍵の問題を解決しています。 公開鍵は文字通り不特定多数に公開される鍵ですが、 この鍵で暗号化されたデータは対応する秘密鍵でしか復号化できないという性質があります。 このため、仮に暗号化されたデータを第三者に傍受されたとしても、 その第三者は暗号化に使用された公開鍵に対応する秘密鍵を持っていませんから、 複合化することはできないわけです。 公開鍵暗号は、秘密鍵を持つ相手にだけデータの複合化を許可するという信頼に基づいた面があるため、 公開鍵/秘密鍵の作成者は秘密鍵を厳重に管理し、 決して第三者に漏洩することのないようにしなければなりません。

CryptoAPIでは、公開鍵/秘密鍵を鍵ペアという1セットで扱い、 CSPが定義する鍵コンテナに格納されることになっています。 鍵ペアを鍵コンテナに作成するには、CryptGenKeyを呼び出します。

BOOL WINAPI CryptGenKey(
  HCRYPTPROV hProv, 
  ALG_ID Algid, 
  DWORD dwFlags, 
  HCRYPTKEY *phKey 
);

hProvは、CSPのハンドルを指定します。 Algidは、アルゴリズムIDを指定します。 CryptGenKeyはセッション鍵を作成する機能も持っており、 この引数にCALG_RC4のようなアルゴリズムIDを指定した場合は、 セッション鍵が作成されることになっています。 鍵ペアを作成する場合は、AT_KEYEXCHANGEかAT_SIGNATUREのいずれかを指定します。 dwFlagsは、上位16ビットに鍵のサイズを指定し、下位16ビットに鍵に設定するフラグを指定します。 鍵のサイズを0に指定すると、CSPがデフォルトのサイズを設定します。 phKeyは、新しく作成された鍵ペアのハンドル、またはセッション鍵のハンドルが返ります。

dwFlagsの下位16ビットに指定できる定数の一部を次に示します。

定数 意味
CRYPT_EXPORTABLE 作成した鍵がセッション鍵であり、それをエクスポートしたい場合はこの定数を指定する。 作成した鍵が鍵ペアであり、鍵ペアの公開鍵だけをエクスポートしたい場合は、 この定数を指定する必要はない。 逆に、鍵ペアの公開鍵/秘密鍵の両方をエクスポートしたい場合は、この定数を指定する。 ただし、その場合は必ず共にCRYPT_FORCE_KEY_PROTECTION_HIGHを指定するか、 CRYPT_USER_PROTECTEDを指定してセキュリティレベルを高にするべきである。 そうでなければ、鍵ペアの作成者以外が暗黙のうちに鍵ペアをエクスポートできることになってしまう。
CRYPT_USER_PROTECTED 鍵ペアの秘密鍵が利用されるときにダイアログを表示するか、 あるいはパスワードの入力を促すことができる。
CRYPT_FORCE_KEY_PROTECTION_HIGH 鍵ペアの秘密鍵が利用されるときにパスワードの入力を促すことができる。 この定数はWindows Vista以降から指定できるとされているが、 XP環境下でも問題なく使用できるように思える。

CryptGenKeyで鍵ペアを作成するためには、 第2引数にAT_KEYEXCHANGEかAT_SIGNATUREのどちらかを指定することになります。 これらの定数は鍵ペアの用途を表しており、 CSPの鍵コンテナには鍵交換用(AT_KEYEXCHANGE)の鍵ペアと 署名用(AT_SIGNATURE)の鍵ペアの2種類を作成できることになっています。 両者の主な違いは、AT_KEYEXCHANGEが暗号化と署名の両方を行えるのに対して、 AT_SIGNATUREは署名しか行えないという点です。 開発者にとって鍵ペアを種類分けすることは、 その鍵ペアの公開鍵を実際に誰かに公開するかを表す指針となるでしょう。 AT_KEYEXCHANGEの鍵ペアの公開鍵は公開するけれども、 AT_SIGNATUREの鍵ペアの公開鍵は公開しないという設計にすれば、 グローバルなセキュリティ通信を行う場合にはAT_KEYEXCHANGEを利用し、 ローカルコンピュータの単一プロセスで署名を行いたい場合は、 AT_SIGNATUREを利用するというような使い分けができるようになります。

CryptGenKeyで作成した鍵ペアはCSPの鍵コンテナに格納されることになるため、 アプリケーションが終了しても、また再び利用できることになります。 このときには、鍵コンテナから鍵ペアを取得することになるため、 CryptGetUserKeyという関数を呼び出すことになります。

BOOL WINAPI CryptGetUserKey(
  HCRYPTPROV hProv, 
  DWORD dwKeySpec, 
  HCRYPTKEY *phUserKey 
);

hProvは、CSPのハンドルを指定します。 dwKeySpecは、AT_KEYEXCHANGEかAT_SIGNATUREのいずれかを指定します。 先に述べたように鍵ペアには用途があるため、どちらの用途の鍵ペアを取得するかを指定する必要があるわけです。 phUserKeyは、取得した鍵ペアのハンドルが返ります。

今回のプログラムは、CryptGenKeyでAT_SIGNATUREの鍵ペアを作成しています。 作成した鍵ペアを使って何かを行うわけではありませんが、 鍵コンテナに鍵ペアを作成したという点で、意味のある事を行っています。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szContainerName[] = TEXT("MyContainer");
	HCRYPTPROV hProv;
	HCRYPTKEY  hKey;
	DWORD      dwKeySpec = AT_SIGNATURE;

	if (!CryptAcquireContext(&hProv, szContainerName, MS_DEF_PROV, PROV_RSA_FULL, 0)) {
		if (GetLastError() != NTE_BAD_KEYSET) {
			MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}
		if (!CryptAcquireContext(&hProv, szContainerName, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_NEWKEYSET)) {
			MessageBox(NULL, TEXT("鍵コンテナの作成に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}
		else
			MessageBox(NULL, TEXT("鍵コンテナを作成しました。"), TEXT("OK"), MB_OK);
	}
	
	if (!CryptGetUserKey(hProv, dwKeySpec, &hKey)) {
		if (GetLastError() != NTE_NO_KEY) {
			MessageBox(NULL, TEXT("鍵ペアのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			CryptReleaseContext(hProv, 0);
			return 0;
		}
		if (!CryptGenKey(hProv, dwKeySpec, 0, &hKey)) {
			MessageBox(NULL, TEXT("鍵ペアの作成に失敗しました。"), NULL, MB_ICONWARNING);
			CryptReleaseContext(hProv, 0);
			return 0;
		}
		else
			MessageBox(NULL, TEXT("鍵ペアを作成しました。"), TEXT("OK"), MB_OK);
	}
	else
		MessageBox(NULL, TEXT("鍵ペアのハンドルを取得しました。"), TEXT("OK"), MB_OK);
	
	CryptDestroyKey(hKey);
	CryptReleaseContext(hProv, 0);

	return 0;
}

今回のプログラムを初めて実行した場合、 鍵コンテナと鍵ペアを作成したことを示すメッセージが表示され、 2回目以降の実行では鍵ペアを取得したことを示すメッセージが表示されるはずです。 鍵ペアを作成するには、それを格納する鍵コンテナから作成します。

if (!CryptAcquireContext(&hProv, szContainerName, MS_DEF_PROV, PROV_RSA_FULL, 0)) {
	if (GetLastError() != NTE_BAD_KEYSET) {
		MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	if (!CryptAcquireContext(&hProv, szContainerName, MS_DEF_PROV, PROV_RSA_FULL, CRYPT_NEWKEYSET)) {
		MessageBox(NULL, TEXT("鍵コンテナの作成に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	else
		MessageBox(NULL, TEXT("鍵コンテナを作成しました。"), TEXT("OK"), MB_OK);
}

最初のCryptAcquireContextの呼び出しでは、第5引数に0を指定しています。 これは、第2引数に指定した名前の鍵コンテナをオープンするという意味で、 既に鍵コンテナが作成されている場合は新たに作成する必要はありませんから、 まずオープンの処理から行います。 NTE_BAD_KEYSETという値は、第2引数に指定した名前の鍵コンテナが存在しないことによるエラーを意味しますから、 この値の時には鍵コンテナの作成に入ることができます。 CryptAcquireContextにCRYPT_NEWKEYSETを指定して呼び出すと、 第2引数に指定した名前を持った鍵コンテナが作成されることになります。 第3引数にCSPの名前を明示的に指定しているのは、 どのCSPに鍵コンテナを作成したのかを明確にしておきたいという意味であり、 MS_DEF_PROVに限らず他のCSPでもNULLでも問題もありません。

CryptAcquireContextの第2引数にNULLを指定すると、 デフォルトの鍵コンテナをオープンしたり作成したりすることができますが、 これはあまり好ましいことではないと思われます。 たとえば、インストール時に鍵コンテナを作成して、 アンインストール時に鍵コンテナを削除するというアプリケーションがあったとします。 この動作は、使用したCSPに痕跡を残さないという点で優れていますが、 作成した鍵コンテナがデフォルトの鍵コンテナであれば、 他のアプリケーションがその鍵コンテナに鍵ペアを作成することは十分考えられることです。 このようなとき、アンインストール時に鍵コンテナを削除してしまっては、 他のアプリケーションは鍵ペアを使用できなくなるという問題が発生してしまいます。 鍵コンテナのオープン時には、偶然存在するかもしれない鍵コンテナを当てにしないように、 できるだけ推測されにくい名前を選択するのがよいと思われます。

続いて、鍵ペアの取得と作成に関するコードを確認します。 CryptAcquireContextが返したCSPのハンドルはオープンした鍵コンテナと関連していますから、 以後に行う鍵ペアの取得や作成はオープンした鍵コンテナを対象とします。

if (!CryptGetUserKey(hProv, dwKeySpec, &hKey)) {
	if (GetLastError() != NTE_NO_KEY) {
		MessageBox(NULL, TEXT("鍵ペアのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		CryptReleaseContext(hProv, 0);
		return 0;
	}
	if (!CryptGenKey(hProv, dwKeySpec, 0, &hKey)) {
		MessageBox(NULL, TEXT("鍵ペアの作成に失敗しました。"), NULL, MB_ICONWARNING);
		CryptReleaseContext(hProv, 0);
		return 0;
	}
	else
		MessageBox(NULL, TEXT("鍵ペアを作成しました。"), TEXT("OK"), MB_OK);
}
else
	MessageBox(NULL, TEXT("鍵ペアのハンドルを取得しました。"), TEXT("OK"), MB_OK);

既に鍵ペアが作成されている場合は、その鍵ペアを取得するべきですから、 まずはCryptGetUserKeyで鍵ペアの取得を試みます。 このとき、鍵ペアが存在しないという理由で関数が失敗した場合は、 CryptGenKeyで鍵ペアを作成することになります。 CryptGenKeyの第3引数は0となっているため、鍵のサイズをデフォルトにし、 鍵に対して特別なフラグは設定されないことになります。 既に特定の種類の鍵ペアが存在しているのにも関わらずCryptGenKeyを呼び出した場合、 既存の鍵ペアは新しく作成した鍵ペアで上書きされることに注意してください。 既存の鍵ペアと新しい鍵ペアは互い異なるものですから、 既存の鍵ペアで暗号化したデータを新しい鍵ペアで複合化するようなことはできません。

最後に、作成した鍵コンテナを削除する方法について触れておきます。 これには、CryptAcquireContextにCRYPT_DELETEKEYSETを指定します。

CryptAcquireContext(&hProv, TEXT("MyContainer"), MS_DEF_PROV, PROV_RSA_FULL, CRYPT_DELETEKEYSET);

この場合、MS_DEF_PROVの"MyContainer"という名前の鍵コンテナが削除され、 鍵コンテナに格納されている鍵ペアも削除されることになります。 鍵ペアにパスワードが設定されている場合でも削除することができてしまうため、 非常に注意が必要といえます。 ちなみに、鍵コンテナをスマートカードに格納している場合は、 PIN入力なしで鍵コンテナを削除することができないため、安全といえます。

鍵ペアとパスワード

アプリケーションが作成した鍵ペアにアクセスするのは、 当然ながらそのアプリケーションのみであることが望まれますが、 実際にはその限りではありません。 特定のCSPが持っている全ての鍵コンテナの名前は列挙することができるため、 そこで列挙した名前をCryptAcquireContextを指定すれば、 鍵コンテナをオープンすることが可能となります。 ただし、鍵ペアの作成時にパスワードの設定を示すフラグを指定すれば、 鍵ペアの秘密鍵が利用されるときにパスワードの入力をUI経由で促すことができるため、 秘密鍵の利用を制限することはできます。

CryptGenKey(hProv, dwKeySpec, CRYPT_USER_PROTECTED, &hKey);

使用するCSPにもよるかもしれませんが、 CryptGenKeyにCRYPT_USER_PROTECTEDを指定すると、 次のようなダイアログが表示されると思われます。

このダイアログでは、秘密鍵の利用に対するセキュリティレベルを中か高のいずれかに選択することができます。 中の場合は、秘密鍵の利用の際に確認を問うダイアログが表示されるだけですが、 高の場合は秘密鍵の利用の際にパスワードの入力を求められます。 このため、設定ボタンを押してセキュリティレベルを高にすると、 次のようなパスワードを設定するためのダイアログが表示されます。

CryptoAPI 秘密キーという文字列のエディットボックスには、 実際に秘密鍵を利用する際に表示される文字列を入力することになります。 独自の文字列を表示するつもりがない場合は、特に変更する必要はないでしょう。 CryptGenKeyにCRYPT_FORCE_KEY_PROTECTION_HIGHを指定した場合は、 セキュリティレベルが既定で高となっているため、 必ずこのダイアログでパスワードの入力を行うことになります。 ちなみに、ダイアログのタイトルが交換キーとなっているのは、 CryptGenKeyに指定した鍵の種類がAT_KEYEXCHANGEであるためであり、 AT_SIGNATUREの場合は署名キーと表示されるはずです。

実際に、秘密鍵を利用する際に表示されるダイアログも確認しておきましょう。 次のようなダイアログは、CryptSignHashの呼び出しや、 鍵ペアのハンドルを指定したCryptDecryptの呼び出し、 あるいはCryptExportKeyにPRIVATEKEYBLOBを指定した場合などで表示されることがあります。 当然がら、ダイアログのタイトルは呼び出した関数によって異なります。

設定しておいたパスワードと入力した値が一致すれば、 秘密鍵の利用が許可され、関数は秘密鍵にアクセスすることになります。 パスワードを記憶するというチェックボックスについては、 ここでチェックを入れても、アプリケーションを再び起動した場合は、 同じようにパスワードを入力することになります。 また、チェックを入れなくても、2回目以降の秘密鍵の利用に関しては、 パスワードの入力は省略されることになっています。



戻る