EternalWindows
CNG / 鍵ペアの作成

前節で述べたように、公開鍵/秘密鍵を安全に格納するためには、 アルゴリズムプロバイダではなくKSPを使用する必要があります。 まずアプリケーションは、ncrypt.dllのNCryptOpenStorageProviderを呼び出し、 使用するKSPを選択することになります。

SECURITY_STATUS WINAPI NCryptOpenStorageProvider(
  NCRYPT_PROV_HANDLE *phProvider,
  LPCWSTR pszProviderName,
  DWORD dwFlags
);

phProviderは、KSPのハンドルを受け取る変数のアドレスを指定します。 pszProviderNameは、使用するKSPの名前を指定します。 システムに最初から存在するMicrosoft Software Key Storage ProviderとMicrosoft Smart Card Key Storage Providerには、 MS_KEY_STORAGE_PROVIDERとMS_SMART_CARD_KEY_STORAGE_PROVIDERという専用の定数があるため、 それを指定することもできます。 NULLを指定した場合は、デフォルトのKSPが使用されます。 dwFlagsは、現在は使用されないため0を指定します。

KSPのハンドルを取得すれば、NCryptCreatePersistedKeyで鍵ペアを作成することができます。 この鍵ペアは、選択しているKSPよって安全に保存されます。

SECURITY_STATUS WINAPI NCryptCreatePersistedKey(
  NCRYPT_PROV_HANDLE hProvider,
  NCRYPT_KEY_HANDLE *phKey,
  LPCWSTR pszAlgId,
  LPCWSTR pszKeyName,
  DWORD dwLegacyKeySpec,
  DWORD dwFlags
);

hProviderは、KSPのハンドルを指定します。 phKeyは、鍵ペアのハンドルを受け取る変数のアドレスを指定します。 pszAlgIdは、鍵ペアのアルゴリズムを指定します。 pszKeyNameは、鍵ペアに設定する名前を指定します。 dwLegacyKeySpecは、鍵ペアの用途を指定します。 通常は0を指定しますが、CryptoAPIで用いたAT_KEYEXCHANGEやAT_SIGNATUREを指定することもできます。 dwFlagsは基本的に0を指定しますが、次に示す定数を指定することもできます。

定数 意味
NCRYPT_MACHINE_KEY_FLAG 鍵ペアを現在のユーザープロファイルではなく、ローカルコンピュータのプロファイルに保存にする。 これにより、どのユーザーから鍵ペアを利用することができるようになる。 ただし、鍵ペアにはセキュリティ記述子が設定されているため、 ユーザーによってはアクセスが拒否されることもある。
NCRYPT_OVERWRITE_KEY_FLAG 指定された名前を持った鍵ペアが既に存在する場合、それを新しい鍵ペアで上書きする。

NCryptCreatePersistedKeyは鍵ペアを作成しますが、 正確にはハンドルを返すだけであり、まだ物理的に鍵ペアは作成されていません。 このため、この時点ではNCryptEnumKeysで鍵ペアの存在を確認できませんし、 鍵ペアを使用することもできません。 鍵ペアはNCryptFinalizeKeyの呼び出しによって完成し、 実際に扱えるようになります。

SECURITY_STATUS NCryptFinalizeKey(
  NCRYPT_KEY_HANDLE hKey,
  DWORD dwFlags
);

hKeyは、NCryptCreatePersistedKeyで作成した鍵ペアのハンドルを指定します。 dwFlagsは基本的に0を指定しますが、次に示す定数を指定することもできます。

定数 意味
NCRYPT_NO_KEY_VALIDATION 鍵ペアの公開鍵を有効にしないとされているが、正確な意味は不明。 この定数を指定しても、公開鍵のエクスポートや暗号化、署名の検証など問題なく行える。
NCRYPT_WRITE_KEY_TO_LEGACY_STORE_FLAG 作成した鍵ペアをCryptoAPIからも利用できるようにする。 この場合は、NCryptCreatePersistedKeyにBCRYPT_RSA_ALGORITHMを指定しておくことになる。
NCRYPT_SILENT_FLAG 鍵ペアを利用するにあたり、UIが表示される可能性がある場合は、関数を失敗させるようにする。

不要になったKSPや鍵ペアは、NCryptFreeObjectで開放することになります。

SECURITY_STATUS WINAPI NCryptFreeObject(
  NCRYPT_HANDLE hObject
);

hObjectは、開放したいオブジェクトのハンドルを指定します。

KSPに保存した鍵ペアは、NCryptOpenKeyで取得することができます。

SECURITY_STATUS WINAPI NCryptOpenKey(
  NCRYPT_PROV_HANDLE hProvider,
  NCRYPT_KEY_HANDLE *phKey,
  LPCWSTR pszKeyName,
  DWORD dwLegacyKeySpec,
  DWORD dwFlags
);

hProviderは、KSPのハンドルを指定します。 phKeyは、鍵ペアのハンドルを受け取る変数のアドレスを指定します。 pszKeyNameは、取得したい鍵ペアの名前を指定します。 この名前は、NCryptCreatePersistedKeyで指定したものと同一になるはずです。 dwLegacyKeySpecは、鍵ペアの用途を指定します。 この値は、NCryptCreatePersistedKeyで指定したものと同一になるはずです。 dwFlagsは基本的に0を指定しますが、次に示す定数を指定することもできます。

定数 意味
NCRYPT_MACHINE_KEY_FLAG ローカルコンピュータのプロファイルに保存されている鍵ペアをオープンする。
NCRYPT_SILENT_FLAG 鍵ペアを利用するにあたり、UIが表示される可能性がある場合は、関数を失敗させるようにする。

KSPに保存された鍵ペアのうち、公開鍵ついてはエクスポートが必要になる場合があります。 このような場合、NCryptExportKeyを呼び出します。

SECURITY_STATUS WINAPI NCryptExportKey(
  NCRYPT_KEY_HANDLE hKey,
  NCRYPT_KEY_HANDLE hExportKey,
  LPCWSTR pszBlobType,
  NCryptBufferDesc *pParameterList,
  PBYTE pbOutput,
  DWORD cbOutput,
  DWORD *pcbResult,
  DWORD dwFlags
);

hKeyは、エクスポートしたい鍵ペアのハンドルを指定します。 hExportKeyは、エクスポートする鍵を暗号化するために使用する鍵のハンドルを指定します。 pszBlobTypeは、エクスポートする形式を示す定数を指定します。 pParameterListは、パラメータ情報を表すバッファを指定します。 不要な場合は、NULLを指定します。 pbOutputは、エクスポートされた鍵のデータを受け取るバッファを指定します。 cbOutputは、pbOutputに指定したバッファを指定します。 pcbResultは、バッファにコピーしたサイズを受け取る変数のアドレスを指定します。 pbOutputがNULLの場合は、バッファのサイズが返ります。 dwFlagsは、現在は使用されないため0を指定します。

KSPに保存された鍵ペアは、NCryptDeleteKeyで削除することができます。 この関数が成功した場合、保存しておいた鍵ペアに対してNCryptOpenKeyを呼び出すことができなくなります。

SECURITY_STATUS WINAPI NCryptDeleteKey(
  NCRYPT_KEY_HANDLE hKey,
  DWORD dwFlags
);

hKeyは、削除したい鍵ペアのハンドルを指定します。 このハンドルは、NCryptFreeObjectに指定する必要はありません。 dwFlagsは、現在は使用されないため0を指定します。

今回のプログラムは、KSPを使用して鍵ペアを作成し、公開鍵をエクスポートします。 また、既に鍵ペアが存在する場合は、鍵ペアを削除するかをユーザーに尋ねます。

#include <windows.h>
#include <ncrypt.h>

#pragma comment (lib, "ncrypt.lib")

BOOL ExportKeyData(NCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPWSTR lpszType);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	NCRYPT_PROV_HANDLE hProv;
	NCRYPT_KEY_HANDLE  hKey;
	WCHAR              szKeyName[] = L"ECDSA256Key";
	TCHAR              szFileName[] = TEXT("ecdsa-pubkey.dat");
	LPWSTR             lpszAlgId = BCRYPT_ECDSA_P256_ALGORITHM;
	LPWSTR             lpszType = BCRYPT_ECCPUBLIC_BLOB;
	SECURITY_STATUS    status;

	status = NCryptOpenStorageProvider(&hProv, MS_KEY_STORAGE_PROVIDER, 0);
	if (status != ERROR_SUCCESS)
		return 0;

	status = NCryptOpenKey(hProv, &hKey, szKeyName, 0, 0);
	if (status == ERROR_SUCCESS) {
		if (MessageBox(NULL, TEXT("指定された名前の鍵ペアが既に存在します。削除しますか?"), TEXT("確認"), MB_YESNO) == IDYES)
			NCryptDeleteKey(hKey, 0);
		else {
			ExportKeyData(hKey, szFileName, lpszType);
			NCryptFreeObject(hKey);
		}
		NCryptFreeObject(hProv);
		return 0;
	}
	
	status = NCryptCreatePersistedKey(hProv, &hKey, lpszAlgId, szKeyName, 0, 0);
	if (status != ERROR_SUCCESS) {
		NCryptFreeObject(hProv);
		return 0;
	}

	status = NCryptFinalizeKey(hKey, 0);
	if (status != ERROR_SUCCESS) {
		NCryptFreeObject(hKey);
		NCryptFreeObject(hProv);
		return 0;
	}
	
	ExportKeyData(hKey, szFileName, lpszType);
	
	MessageBox(NULL, TEXT("鍵を作成しました。"), TEXT("OK"), MB_OK);

	NCryptFreeObject(hKey);
	NCryptFreeObject(hProv);
	
	return 0;
}

BOOL ExportKeyData(NCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPWSTR lpszType)
{
	HANDLE          hFile;
	DWORD           dwWriteByte;
	DWORD           dwDataSize;
	LPBYTE          lpData;
	SECURITY_STATUS status;

	NCryptExportKey(hKey, NULL, lpszType, NULL, NULL, 0, &dwDataSize, 0);
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	status = NCryptExportKey(hKey, NULL, lpszType, NULL, lpData, dwDataSize, &dwDataSize, 0);
	if (status != ERROR_SUCCESS) {
		HeapFree(GetProcessHeap(), 0, lpData);
		return FALSE;
	}
	
	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpData, dwDataSize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	HeapFree(GetProcessHeap(), 0, lpData);

	return TRUE;
}

まずは、鍵ペアを格納するためのKSPのハンドルを取得する必要があります。 これは、NCryptOpenStorageProviderを呼び出すことで可能で、 MS_KEY_STORAGE_PROVIDERを指定していることからMicrosoft Software Key Storage Providerを利用することになります。 KSPのハンドルを取得すれば、鍵の作成やオープンをすることができます。 まずは、鍵の作成から確認します。

status = NCryptCreatePersistedKey(hProv, &hKey, lpszAlgId, szKeyName, 0, 0);
if (status != ERROR_SUCCESS) {
	NCryptFreeObject(hProv);
	return 0;
}

status = NCryptFinalizeKey(hKey, 0);
if (status != ERROR_SUCCESS) {
	NCryptFreeObject(hKey);
	NCryptFreeObject(hProv);
	return 0;
}

NCryptCreatePersistedKeyの第3引数にBCRYPT_ECDSA_P256_ALGORITHMを指定していることから、 ECDSA署名アルゴリズムの鍵ペアが作成されることになります。 これに合わせて、鍵の名前からアルゴリズムを推測できるよう、 szKeyNameをECDSAKeyという文字列で初期化しています。 作成した鍵ペアにプロパティを設定する場合は、 この後にNCryptSetPropertyを呼び出すことになりますが、 そのような設定を必要としない場合は直ぐにNCryptFinalizeKeyを呼び出して問題ありません。 これにより、鍵ペアを利用できるようになります。

自作関数のExportKeyDataは、鍵ペアをエクスポートする関数です。 第3引数にBCRYPT_RSAPUBLIC_BLOBを指定していることから、公開鍵がエクスポートされることになります。

NCryptExportKey(hKey, NULL, lpszType, NULL, NULL, 0, &dwDataSize, 0);
lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
status = NCryptExportKey(hKey, NULL, lpszType, NULL, lpData, dwDataSize, &dwDataSize, 0);

1回目の呼び出しでは、必要なバッファのサイズが分からないため、 第5引数にNULLを指定して、第7引数でバッファのサイズを取得します。 その後、バッファを確保し、2回目の呼び出しの第5引数に指定することで、 鍵のバイトデータを取得することができます。

今回のプログラムは、指定した名前を持った鍵ペアが既に存在するかを確認する処理があるため、 2回目の起動時にはその処理が実行されることになります。

status = NCryptOpenKey(hProv, &hKey, szKeyName, 0, 0);
if (status == ERROR_SUCCESS) {
	if (MessageBox(NULL, TEXT("指定された名前の鍵ペアが既に存在します。削除しますか?"), TEXT("確認"), MB_YESNO) == IDYES)
		NCryptDeleteKey(hKey, 0);
	else {
		ExportKeyData(hKey, szFileName, lpszType);
		NCryptFreeObject(hKey);
	}
	NCryptFreeObject(hProv);
	return 0;
}

NCryptOpenKeyの第3引数から第5引数までは、NCryptCreatePersistedKeyで指定した値となります。 NCryptOpenKeyは既に鍵ペアが存在する場合はERROR_SUCCESSを返すため、 この場合に鍵を削除するかどうかの確認を行っています。 結果がTRUEである場合はNCryptDeleteKeyで鍵ペアを削除し、 FALSEである場合は公開鍵をエクスポートしています。

鍵ペアとUIポリシー

KSPによって管理される鍵ペアを予期しないアプリケーションから使用されないように、 鍵ペアにはパスワードを設定することができます。 このような設定が必要な場合は、NCryptFinalizeKeyで鍵ペアを完成させる前に、 NCryptSetPropertyでプロパティの設定を行います。

NCRYPT_UI_POLICY uiPolicy;

uiPolicy.dwVersion        = 1;
uiPolicy.dwFlags          = NCRYPT_UI_FORCE_HIGH_PROTECTION_FLAG;
uiPolicy.pszCreationTitle = NULL;
uiPolicy.pszFriendlyName  = L"ECDSA256Key";
uiPolicy.pszDescription   = L"署名に使用する鍵です。";

NCryptSetProperty(hKey, NCRYPT_UI_POLICY_PROPERTY, (PBYTE)&uiPolicy, sizeof(NCRYPT_UI_POLICY), 0);

鍵ペアに設定できるプロパティには、UIポリシーというものがあります。 これは、秘密鍵の作成時やアクセス時にダイアログを表示する機能です、 第2引数のNCRYPT_UI_POLICY_PROPERTYがUIポリシーを表すプロパティであり、 この定数を指定する場合は第3引数にNCRYPT_UI_POLICY構造体のアドレスを指定します。 dwVersionは常に1であり、dwFlagsはUIポリシーに関する定数を指定します。 NCRYPT_UI_FORCE_HIGH_PROTECTION_FLAGは、ダイアログを表示してさらにパスワードを要求しますが、 NCRYPT_UI_PROTECT_KEY_FLAGという定数を指定した場合は、パスワードまで要求するとは限りません。 残りの3つのメンバは、ダイアログに表示される文字列を表しますが、 pszCreationTitleは使用されないように思えます。

UIポリシーの設定によって実際にどのようなダイアログが表示されることになるのかを確認してみましょう。 まず、NCryptSetPropertyの呼び出し時に、次のようなダイアログが表示されます。

このダイアログでは、秘密鍵を保護するためのパスワードを入力することになります。 NCRYPT_UI_POLICY.dwFlagsにNCRYPT_UI_PROTECT_KEY_FLAGを指定している場合は、 パスワードを要求するかどうかのチェックボックスも表示されます。 NCRYPT_UI_POLICY.pszFriendlyNameに指定した文字列はキー名の部分に表示され、 NCRYPT_UI_POLICY.pszDescriptionは詳細表示の場合に表示されます。

UIポリシーが設定されている状態で秘密鍵へのアクセスを行った場合は、 それを許可するかのダイアログが表示されることになります。 具体的には、NCryptSignHashやNCryptDecrypt、NCryptExportKeyによる秘密鍵のエクスポートで表示されます。

このダイアログで正しいパスワードを入力すれば、秘密鍵へアクセスできるようになります。 キー名の部分には、NCRYPT_UI_POLICY.pszFriendlyNameに指定していた文字列は表示されないようですが、 NCRYPT_UI_POLICY.pszDescriptionは詳細表示で確認することができます。 ちなみに、このようなUIが表示されることを避けたい場合は、 NCryptOpenKeyにNCRYPT_SILENT_FLAGを指定することができます。 この場合、UIが表示される関数を呼び出した場合は、 無条件に関数が失敗することになります。



戻る