EternalWindows
ファイル署名 / spcとpvk

ユーザーにとって、証明書と秘密鍵を管理する方法は主に2通り考えられます。 1つは、システムに証明書と秘密鍵を保存する方法で、この場合、 証明書をシステムストアにインストールし、秘密鍵をCSPの鍵コンテナに格納しておくことになります。 もう1つは、ファイルベースの管理方法で、この場合、 証明書を.spcファイルに格納し、秘密鍵を.pvkファイルに格納します。 CAに対して証明書を申請した場合は、この形式でファイルを取得することになるでしょう。 どちらかの管理方法に優劣があるわけではありませんが、 今回は1の方法で管理された証明書と秘密鍵を、 .spcファイルと.pvkファイルにエクスポートについて考えてみたいと思います。

まず、.spcファイルへのエクスポートですが、 .spcファイルはPKCS #7形式を保存した.p7bファイルと同じ形式なので、 PKCS #7形式のエクスポートを行う関数を利用することができます。 そのような関数には、たとえばCryptUIWizExportがあるでしょう。 .pvkファイルのエクスポートは、SignerSignと同じくmssin32.dllに 実装されているPvkPrivateKeySaveを呼び出すことになります。

BOOL WINAPI PvkPrivateKeySave(
  HCRYPTPROV hCryptProv,
  HANDLE hFile,
  DWORD dwKeySpec,
  HWND hwndOwner,
  LPCWSTR pwszKeyName,
  DWORD dwFlags
);

hCryptProvは、CSPのハンドルを指定します。 hFileは、書き込みアクセスで開いたファイルハンドルを指定します。 dwKeySpecは、鍵コンテナに格納されている鍵ペアの鍵用途を指定します。 hwndOwnerは、秘密鍵のパスワードを入力する際に表示されるダイアログの親ウインドウを指定します。 不要な場合は、NULLを指定して問題ありません。 pwszKeyNameは、ダイアログに表示するための文字列を指定します。 不要な場合は、NULLを指定して問題ありません。 dwFlagsは、CryptExportKeyのdwFlagsと同様の値を指定できるとされていますが、 基本的には0を指定することになるでしょう。 ちなみに、CryptExportKeyには鍵ペアをエクスポートするPRIVATEKEYBLOBという定数が用意されていますが、 この定数を指定してエクスポートされたファイルは.pvkファイルと形式が異なるので注意してください。

今回のプログラムは、MY証明書ストアに存在する証明書から.spcファイルとpvkファイルをエクスポートします。 証明書にはCSP情報が関連付けられている必要があります。

#include <windows.h>
#include <cryptuiapi.h>

#pragma comment (lib, "crypt32.lib")
#pragma comment (lib, "cryptui.lib")

typedef BOOL (WINAPI *LPFNPVKPRIVATEKEYSAVE)(HCRYPTPROV, HANDLE, DWORD, HWND, LPCWSTR, DWORD);

BOOL ExportSpcFile(LPWSTR lpszFileName, PCCERT_CONTEXT pContext);
BOOL ExportPvkFile(LPWSTR lpszFileName, PCCERT_CONTEXT pContext);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HCERTSTORE     hStore;
	PCCERT_CONTEXT pContext;
	
	hStore = CertOpenSystemStore(0, TEXT("MY"));
	if (hStore == NULL)
		return 0;
	
	pContext = CryptUIDlgSelectCertificateFromStore(hStore, NULL, 0, L"エクスポートする証明書を選択してください。", 0, 0, NULL);
	if (pContext == NULL) {
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return 0;
	}
	
	if (!ExportSpcFile(L"sample.spc", pContext)) {
		MessageBox(NULL, TEXT("SPCファイルのエクスポートに失敗しました。"), NULL, MB_ICONWARNING);
		CertFreeCertificateContext(pContext);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return 0;
	}
	
	if (!ExportPvkFile(L"sample.pvk", pContext)) {
		MessageBox(NULL, TEXT("PVKファイルのエクスポートに失敗しました。"), NULL, MB_ICONWARNING);
		CertFreeCertificateContext(pContext);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return 0;
	}

	MessageBox(NULL, TEXT("エクスポートに成功しました。"), TEXT("OK"), MB_OK);

	CertFreeCertificateContext(pContext);
	CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);

	return 0;
}

BOOL ExportSpcFile(LPWSTR lpszFileName, PCCERT_CONTEXT pContext)
{
	BOOL                                bResult;
	HCERTSTORE                          hStores[2];
	CRYPTUI_WIZ_EXPORT_INFO             exportInfo;
	CRYPTUI_WIZ_EXPORT_CERTCONTEXT_INFO contextInfo;

	hStores[0] = CertOpenSystemStore(0, TEXT("CA"));
	hStores[1] = CertOpenSystemStore(0, TEXT("Root"));

	exportInfo.dwSize             = sizeof(CRYPTUI_WIZ_EXPORT_INFO);
	exportInfo.pwszExportFileName = lpszFileName;
	exportInfo.dwSubjectChoice    = CRYPTUI_WIZ_EXPORT_CERT_CONTEXT;
	exportInfo.pCertContext       = pContext;
	exportInfo.cStores            = 2;
	exportInfo.rghStores          = hStores;

	ZeroMemory(&contextInfo, sizeof(CRYPTUI_WIZ_EXPORT_CERTCONTEXT_INFO));
	contextInfo.dwSize         = sizeof(CRYPTUI_WIZ_EXPORT_CERTCONTEXT_INFO);
	contextInfo.dwExportFormat = CRYPTUI_WIZ_EXPORT_FORMAT_PKCS7;
	contextInfo.fExportChain   = TRUE;

	bResult = CryptUIWizExport(CRYPTUI_WIZ_NO_UI, NULL, NULL, &exportInfo, &contextInfo);

	CertCloseStore(hStores[0], CERT_CLOSE_STORE_CHECK_FLAG);
	CertCloseStore(hStores[1], CERT_CLOSE_STORE_CHECK_FLAG);
	
	return bResult;
}

BOOL ExportPvkFile(LPWSTR lpszFileName, PCCERT_CONTEXT pContext)
{
	HCRYPTPROV            hProv;
	DWORD                 dwKeySpec;
	HMODULE               hmod;
	LPFNPVKPRIVATEKEYSAVE lpfnPvkPrivateKeySave;
	HANDLE                hFile;
	BOOL                  bResult;
	
	if (!CryptAcquireCertificatePrivateKey(pContext, 0, NULL, &hProv, &dwKeySpec, NULL))
		return FALSE;

	hmod = LoadLibrary(TEXT("mssign32.dll"));
	if (hmod == NULL)
		return FALSE;

	lpfnPvkPrivateKeySave = (LPFNPVKPRIVATEKEYSAVE)GetProcAddress(hmod, "PvkPrivateKeySave");
	if (lpfnPvkPrivateKeySave == NULL) {
		FreeLibrary(hmod);
		return 0;
	}

	hFile = CreateFileW(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	bResult = lpfnPvkPrivateKeySave(hProv, hFile, dwKeySpec, NULL, L"秘密鍵のパスワードを入力してください。", 0);
	CloseHandle(hFile);

	FreeLibrary(hmod);

	return bResult;
}

WinMainではまず適切な証明書を選択し、その後、エクスポートの関数を呼び出すことになっています。 .spcファイルをエクスポートするExportSpcFileの内部を次に示します。

hStores[0] = CertOpenSystemStore(0, TEXT("CA"));
hStores[1] = CertOpenSystemStore(0, TEXT("Root"));

exportInfo.dwSize             = sizeof(CRYPTUI_WIZ_EXPORT_INFO);
exportInfo.pwszExportFileName = lpszFileName;
exportInfo.dwSubjectChoice    = CRYPTUI_WIZ_EXPORT_CERT_CONTEXT;
exportInfo.pCertContext       = pContext;
exportInfo.cStores            = 2;
exportInfo.rghStores          = hStores;

ZeroMemory(&contextInfo, sizeof(CRYPTUI_WIZ_EXPORT_CERTCONTEXT_INFO));
contextInfo.dwSize         = sizeof(CRYPTUI_WIZ_EXPORT_CERTCONTEXT_INFO);
contextInfo.dwExportFormat = CRYPTUI_WIZ_EXPORT_FORMAT_PKCS7;
contextInfo.fExportChain   = TRUE;

bResult = CryptUIWizExport(CRYPTUI_WIZ_NO_UI, NULL, NULL, &exportInfo, &contextInfo);

証明書を基にエクスポートするので、dwSubjectChoiceにはCRYPTUI_WIZ_EXPORT_CERT_CONTEXTを指定し pCertContextに証明書コンテキストを指定します。 rghStoresに、CAとRootの2つの証明書ストアのハンドルを指定するのは、 .p7b(spc)ファイルが自身に署名した証明書も含むことができるからであり、 そのような証明書を検索するために他の証明書ストアを指定する必要があるのです。 ただし、fExportChainにFALSEを指定した場合は、証明書を含めないことになるので、 証明書ストアの指定は不要です。 dwExportFormatにCRYPTUI_WIZ_EXPORT_FORMAT_PKCS7を指定すれば、 証明書が.p7bファイルとしてエクスポートされることになります。

続いて、ExportPvkFileの内部を確認します。

if (!CryptAcquireCertificatePrivateKey(pContext, 0, NULL, &hProv, &dwKeySpec, NULL))
	return FALSE;

hmod = LoadLibrary(TEXT("mssign32.dll"));
if (hmod == NULL)
	return FALSE;

lpfnPvkPrivateKeySave = (LPFNPVKPRIVATEKEYSAVE)GetProcAddress(hmod, "PvkPrivateKeySave");
if (lpfnPvkPrivateKeySave == NULL) {
	FreeLibrary(hmod);
	return 0;
}

hFile = CreateFileW(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
bResult = lpfnPvkPrivateKeySave(hProv, hFile, dwKeySpec, NULL, L"秘密鍵のパスワードを入力してください。", 0);
CloseHandle(hFile);

証明書にCSP情報が関連付けられている場合、CryptAcquireCertificatePrivateKeyでCSPのハンドルと 鍵用途を取得することができるため、これをPvkPrivateKeySaveに指定すればよいことになります。 PvkPrivateKeySaveを呼び出すとダイアログが表示されパスワードを入力することになりますが、 「なし」というボタンを選択すると、.pvkファイルの利用時にパスワードを入力する必要はありません。 ちなみに、PvkPrivateKeySaveを成功させるためには、鍵コンテナに存在する鍵ペアがエクスポート可能になっていなければならず、 具体的にはCryptGenKeyの呼び出し時にCRYPT_EXPORTABLEを指定している必要があります。 鍵ペアがエクスポート可能になっていない場合、PvkPrivateKeySaveにおけるGetLastErrorの戻り値は、 NTE_BAD_KEY_STATEになります。

.pvkファイルとCryptoAPI

.pvkファイルに格納されている秘密鍵をCryptoAPIで使うためには、 まずこの秘密鍵をCSPの鍵コンテナに格納する必要があります。 そのような動作を行うPvkGetCryptProvの使い方を次に示します。

#include <windows.h>
#include <cryptuiapi.h>

#pragma comment (lib, "crypt32.lib")
#pragma comment (lib, "cryptui.lib")

typedef HRESULT (WINAPI *LPFNPVKGETCRYPTPROV)(HWND, LPCWSTR, LPCWSTR, DWORD, LPCWSTR, LPCWSTR, DWORD *, LPWSTR *, HCRYPTPROV *);
typedef HRESULT (WINAPI *LPFNPVKFREECRYPTPROV)(HCRYPTPROV, LPCWSTR, DWORD, LPCWSTR);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HMODULE              hmod;
	LPFNPVKGETCRYPTPROV  lpfnPvkGetCryptProv;
	LPFNPVKFREECRYPTPROV lpfnPvkFreeCryptProv;
	HCRYPTPROV           hProv;
	LPWSTR               lpszTmpContainer;
	HRESULT              hr;

	hmod = LoadLibrary(TEXT("mssign32.dll"));
	if (hmod == NULL)
		return FALSE;

	lpfnPvkGetCryptProv = (LPFNPVKGETCRYPTPROV)GetProcAddress(hmod, "PvkGetCryptProv");
	if (lpfnPvkGetCryptProv == NULL) {
		FreeLibrary(hmod);
		return 0;
	}
	
	lpfnPvkFreeCryptProv = (LPFNPVKFREECRYPTPROV)GetProcAddress(hmod, "PvkFreeCryptProv");
	if (lpfnPvkFreeCryptProv == NULL) {
		FreeLibrary(hmod);
		return 0;
	}

	hr = lpfnPvkGetCryptProv(NULL, L"秘密鍵のパスワードを入力してください。", NULL, PROV_RSA_FULL, L"sample.pvk", NULL, NULL, &lpszTmpContainer, &hProv);
	if (hr != S_OK) {
		MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		FreeLibrary(hmod);
		return 0;
	}
	
	MessageBox(NULL, TEXT("CSPのハンドルを取得しました。"), TEXT("OK"), MB_OK);

	lpfnPvkFreeCryptProv(hProv, NULL, PROV_RSA_FULL, lpszTmpContainer);

	return 0;
}

PvkGetCryptProvの第1引数は表示するダイアログの親ウインドウのハンドルですが、NULLで問題ありません。 第2引数は、ダイアログに表示される文字列です。 第3引数はCSPの名前、第4引数はプロバイダタイプです。 第5引数は.pvkファイルの名前を指定し、指定した場合は第6引数にNULLを指定することができます。 第7引数は鍵用途を受け取る変数のアドレスを指定しますが、不要な場合はNULLを指定して問題ありません。 第8引数は作成された鍵コンテナの名前を受け取る変数のアドレス、 第9引数はCSPのハンドルを受け取ります。 CSPのハンドルを取得できれば、CSPのハンドルを要求するCryptoAPIを呼び出せるようになります。

PvkGetCryptProvが作成する鍵コンテナは一時的なコンテナであると言われていますが、 正確にはPvkFreeCryptProvを呼び出さなければ、破棄されることはありません。 鍵コンテナは名前こそ、TmpKeyXXXXのようになっていますが、 PvkFreeCryptProvが呼び出さなければ鍵コンテナは作成されたままとなります。 このため、PvkFreeCryptProvは必ず呼び出すようにしておかなければなりません。 なお、PvkGetCryptProvは.pvkファイルではなく、 既存の鍵コンテナからCSPのハンドルを取得する機能も持っており、 その呼び出し方は次のようになります。

hr = lpfnPvkGetCryptProv(NULL, NULL, NULL, PROV_RSA_FULL, NULL, lpszContainerName, &dwKeySpec, &lpszTmpContainer, &hProv);

第6引数に鍵コンテナの名前を指定し、第7引数に0で初期化した鍵用途を受け取る変数を指定するのが重要です。 鍵コンテナが作成されるわけではないので、lpszTmpContainerは不要のように思えますが、 指定しない場合は関数が失敗することになります。 基本的にCSPのハンドルはCryptAcquireContextで取得することになるため、 PvkGetCryptProvをこのような目的で使用することはないでしょう。



戻る