EternalWindows
CSP / 署名と鍵のエクスポート

何らかのファイルやデータが、自分の想定する相手によって作成されていることや、 送信されていることを特定することは重要な意味を持ちます。 たとえば、あるwebサイトからファイルをダウンロードするような場合、 そのファイルは恐らくそのwebサイトによって提供されているものだと思われますが、 実際には第三者によって不正にアップロードされたファイルであるかもしれません。 このような問題を踏まえた場合、ファイルを公開するwebサイトでは、 その公開するファイルに署名をしておく必要があるでしょう。 署名をするというのは、そのファイルやデータに作成者の情報を添付することであり、 これにより受信側はその署名を確認することで、作成者が誰であるかを知ることができます。

署名の実装方法は、ハッシュと公開鍵/秘密鍵の利用に基づいています。 データの通信を例に考えてみましょう。 まず、送信側は送信したいデータを用意し、そのハッシュ値を求めます。 次に、算出したハッシュ値を秘密鍵で暗号化し、 この署名(暗号化されたハッシュ値)と送信するつもりだった実際のデータをあわせて送信します。 受信側が受信するデータには署名と実際のデータが含まれており、 受信側はこの署名を公開鍵で複合化します。 ここで複合化が成功した場合は、受信側が使用している公開鍵に対応する秘密鍵で 暗号化が行われたことを意味しますから、 データの送信側は秘密鍵の持ち主であることが保障されます。 また、署名はデータが変更されていないかを検証することも可能で、 実際のデータの部分のハッシュ値を算出し、 それを先ほど複合化したハッシュ値と比較して一致すれば、 データが変更されていないことが分かります。

署名を作成するには、まず署名したいデータを用意し、そのハッシュ値を算出します。 そして、そのハッシュ値と鍵ペアの用途を表す定数をCryptSignHashに指定します。

BOOL WINAPI CryptSignHash(
  HCRYPTHASH hHash, 
  DWORD dwKeySpec, 
  LPCTSTR sDescription, 
  DWORD dwFlags, 
  BYTE *pbSignature, 
  DWORD *pdwSigLen 
);

hHashは、ハッシュオブジェクトのハンドルを指定します。 このハッシュオブジェクトは、ハッシュ値を持っていなければなりません。 dwKeySpecは、AT_KEYEXCHANGEまたはAT_SIGNATUREを指定します。 指定した用途の鍵ペアの秘密鍵がハッシュ値の暗号化に利用されます。 sDescriptionは、NULLを指定します。 dwFlagsはCSP特有のフラグであり、0を指定しても問題ありません。 pbSignatureは、作成された署名を受け取るバッファを指定します。 pdwSigLenは、作成された署名のサイズを受け取る変数のアドレスを指定します。

pbSignatureには署名が格納されますが、 これを単一で保存したり送信したりすることには意味がありません。 データに署名をするという言い方をすることがありますが、 これは文字通りの意味であり、実際のデータに署名を添付するからこそ、 そのデータが誰によって作成されたのかが分かるようになるわけです。 ただし、実際にはCryptSignHashで作成される署名は秘密鍵で暗号化されたハッシュ値ですから、 作成者の情報は含まれていません。

署名が秘密鍵を使用して作成されていることから、この検証には公開鍵が必要になるため、 データの作成者は事前に公開鍵を公開しておくことになります。 次に示すCryptExportKeyは、鍵のハンドルからエクスポート可能なバイナリデータを返します。

BOOL WINAPI CryptExportKey(
  HCRYPTKEY hKey, 
  HCRYPTKEY hExpKey, 
  DWORD dwBlobType, 
  DWORD dwFlags, 
  BYTE *pbData, 
  DWORD *pdwDataLen 
);

hKeyは、エクスポートしたい鍵のハンドルを指定します。 hExpKeyは、エクスポートされる鍵のバイナリデータを暗号化するためのハンドルを指定します。 dwBlobTypeは、hKeyが示す鍵のエクスポート方法を表す定数を指定します。 dwFlagsはCSP特有のフラグであるため、0を指定して問題ありません。 pbDataは、鍵のバイナリデータを受け取るバッファを指定します。 データのフォーマットは、dwBlobTypeによって異なりますが、 基本的にはPUBLICKEYSTRUC構造体を先頭としたフォーマットになります。 pdwDataLenは、バッファのサイズを格納している変数のアドレスを指定します。 データがバッファにエクスポートされた場合は、 そのエクスポートされたサイズが格納されます。

dwBlobTypeに指定できる定数の一部を次に示します。

定数 意味
PRIVATEKEYBLOB 公開鍵/秘密鍵をエクスポートする。hKeyは、鍵ペアのハンドル。 この鍵ペアは、CRYPT_EXPORTABLEを指定して作成されている必要がある。 秘密鍵がエクスポートされるということで、それを暗号化すべく、 hExpKeyに暗号化に用いるセッション鍵のハンドルを指定することもできる。
PUBLICKEYBLOB 公開鍵をエクスポートする。hKeyは、鍵ペアのハンドル。 hExpKeyは、基本的に0を指定する。
SIMPLEBLOB セッション鍵をエクスポートする。hKeyは、セッション鍵のハンドル。 この鍵は、CRYPT_EXPORTABLEを指定して作成されている必要がある。 hExpKeyには必ず鍵ペアのハンドルを指定し、 この結果鍵ペアの秘密鍵でデータが暗号化される。

今回のプログラムは、データに署名を添付してそれをファイルに保存します。 また、その署名を検証できるように、公開鍵をエクスポートします。 署名には秘密鍵が必要になることから、事前に前節のプログラムを実行して鍵ペアを作成しておいてください。

#include <windows.h>

BOOL ExportPublicKey(HCRYPTPROV hProv, LPTSTR lpszFileName, DWORD dwKeySpec);
BOOL SignData(HCRYPTPROV hProv, LPTSTR lpszFileName, DWORD dwKeySpec, LPBYTE lpData, DWORD dwDataSize);

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

	if (!CryptAcquireContext(&hProv, szContainerName, MS_DEF_PROV, PROV_RSA_FULL, 0)) {
		MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	if (!ExportPublicKey(hProv, TEXT("pubkey.dat"), dwKeySpec)) {
		MessageBox(NULL, TEXT("公開鍵のエクスポートに失敗しました。"), NULL, MB_ICONWARNING);
		CryptReleaseContext(hProv, 0);
		return 0;
	}

	if (SignData(hProv, TEXT("sign.dat"), dwKeySpec, (LPBYTE)szData, sizeof(szData)))
		MessageBox(NULL, TEXT("署名しました。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("署名に失敗しました。"), NULL, MB_ICONWARNING);
	
	CryptReleaseContext(hProv, 0);

	return 0;
}

BOOL ExportPublicKey(HCRYPTPROV hProv, LPTSTR lpszFileName, DWORD dwKeySpec)
{
	HANDLE    hFile;
	LPBYTE    lpKey;
	DWORD     dwKeySize;
	DWORD     dwWriteByte;
	HCRYPTKEY hKey;
	
	if (!CryptGetUserKey(hProv, dwKeySpec, &hKey))
		return FALSE;
	
	CryptExportKey(hKey, 0, PUBLICKEYBLOB, 0, NULL, &dwKeySize);
	lpKey = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeySize);
	CryptExportKey(hKey, 0, PUBLICKEYBLOB, 0, lpKey, &dwKeySize);

	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpKey, dwKeySize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	CryptDestroyKey(hKey);
	HeapFree(GetProcessHeap(), 0, lpKey);

	return TRUE;
}

BOOL SignData(HCRYPTPROV hProv, LPTSTR lpszFileName, DWORD dwKeySpec, LPBYTE lpData, DWORD dwDataSize)
{
	HANDLE     hFile;
	LPBYTE     lpSignature;
	DWORD      dwSignatureSize;
	DWORD      dwWriteByte;
	HCRYPTHASH hHash;
	
	if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash))
		return FALSE;

	CryptHashData(hHash, lpData, dwDataSize, 0);

	if (!CryptSignHash(hHash, dwKeySpec, NULL, 0, NULL, &dwSignatureSize)) {
		CryptDestroyHash(hHash);
		return FALSE;
	}

	lpSignature = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignatureSize);
	if (!CryptSignHash(hHash, dwKeySpec, NULL, 0, lpSignature, &dwSignatureSize)) {
		CryptDestroyHash(hHash);
		HeapFree(GetProcessHeap(), 0, lpSignature);
		return FALSE;
	}

	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, &dwDataSize, sizeof(DWORD), &dwWriteByte, NULL);
	WriteFile(hFile, &dwSignatureSize, sizeof(DWORD), &dwWriteByte, NULL);
	WriteFile(hFile, lpData, dwDataSize, &dwWriteByte, NULL);
	WriteFile(hFile, lpSignature, dwSignatureSize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	CryptDestroyHash(hHash);
	HeapFree(GetProcessHeap(), 0, lpSignature);

	return TRUE;
}

このプログラムは、MyContainerという鍵コンテナにAT_SIGNATUREの鍵ペアが存在していることを前提としています。 また、鍵ペアの秘密鍵にアクセスをするという関係上、CryptAcquireContextの第5引数にはCRYPT_VERIFYCONTEXTを指定していません。 ExportPublicKeyという自作関数は、鍵コンテナからAT_SIGNATUREの鍵ペアを取得し、 その鍵ペアの公開鍵をファイルに保存します。

BOOL ExportPublicKey(HCRYPTPROV hProv, LPTSTR lpszFileName, DWORD dwKeySpec)
{
	HANDLE    hFile;
	LPBYTE    lpKey;
	DWORD     dwKeySize;
	DWORD     dwWriteByte;
	HCRYPTKEY hKey;
	
	if (!CryptGetUserKey(hProv, dwKeySpec, &hKey))
		return FALSE;
	
	CryptExportKey(hKey, 0, PUBLICKEYBLOB, 0, NULL, &dwKeySize);
	lpKey = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeySize);
	CryptExportKey(hKey, 0, PUBLICKEYBLOB, 0, lpKey, &dwKeySize);

	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpKey, dwKeySize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	CryptDestroyKey(hKey);
	HeapFree(GetProcessHeap(), 0, lpKey);

	return TRUE;
}

まず、CryptGetUserKeyを呼び出して、鍵コンテナから鍵ペアのハンドルを取得します。 そして、このハンドルをCryptExportKeyに指定し、 第3引数にPUBLICKEYBLOBを指定すると、第5引数に公開鍵のバイナリデータが格納されます。 後はこれをファイルに書き込めば、ファイルを通じて公開鍵を公開できたことになります。 続いて、署名を行う関数を見てみます。

BOOL SignData(HCRYPTPROV hProv, LPTSTR lpszFileName, DWORD dwKeySpec, LPBYTE lpData, DWORD dwDataSize)
{
	HANDLE     hFile;
	LPBYTE     lpSignature;
	DWORD      dwSignatureSize;
	DWORD      dwWriteByte;
	HCRYPTHASH hHash;
	
	if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash))
		return FALSE;

	CryptHashData(hHash, lpData, dwDataSize, 0);

	if (!CryptSignHash(hHash, dwKeySpec, NULL, 0, NULL, &dwSignatureSize)) {
		CryptDestroyHash(hHash);
		return FALSE;
	}

	lpSignature = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignatureSize);
	if (!CryptSignHash(hHash, dwKeySpec, NULL, 0, lpSignature, &dwSignatureSize)) {
		CryptDestroyHash(hHash);
		HeapFree(GetProcessHeap(), 0, lpSignature);
		return FALSE;
	}

	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, &dwDataSize, sizeof(DWORD), &dwWriteByte, NULL);
	WriteFile(hFile, &dwSignatureSize, sizeof(DWORD), &dwWriteByte, NULL);
	WriteFile(hFile, lpData, dwDataSize, &dwWriteByte, NULL);
	WriteFile(hFile, lpSignature, dwSignatureSize, &dwWriteByte, NULL);
	CloseHandle(hFile);

	CryptDestroyHash(hHash);
	HeapFree(GetProcessHeap(), 0, lpSignature);

	return TRUE;
}

署名を行うには、データのハッシュ値が必要であるため、 まずはCryptHashDataでハッシュ値を算出します。 1回目のCryptSignHashでは作成される署名のサイズが分からないため、 第5引数にNULLを指定してサイズを取得することに専念します。 CryptSignHashでは、第2引数で指定された用途の鍵ペアが鍵コンテナに存在しない場合は 失敗することになるため、1回目の呼び出し時にその確認を行うようにしています。 2回目の呼び出しでは実際に署名が作成されるわけですが、 このときには秘密鍵へのアクセスが内部で生じることから、 場合によっては確認のダイアログが表示されることがあります。 ここでの応答が、作業を中止する旨を示すものである場合は関数が失敗するため、、 ここでも戻り値を確認するようにしています。 ファイルに書き込むのは、できれば実際のデータと署名のみにしておきたいところですが、 これだけでは各データがどれだけのサイズが読み取り時に分からないため、 各データのサイズも書き込むことになります。 書き込んでいる順番が、いわばファイルのフォーマットということになります。

さて、sign.datというファイルを作成したわけですが、 このファイルは結局のところ一体何なのでしょうか。 このファイルは、"sample-data"というテキストデータとそのデータから作成した署名で構成されていますから、 テキストデータに署名したファイルが、sign.datということになるでしょう。 後はこのファイルと公開鍵を格納したpubkey.datを公開すれば、 受信側は公開鍵を使ってsign.datの安全性を確認することができます。 ただし、sign.datは署名を格納しているという関係上、 テキストファイルとして存在することができませんから、 ファイルの中身を確認するには専用の検証アプリケーションが必要となります。 次節では、このアプリケーションを作成します。

公開鍵と証明書

実際問題として、公開鍵を単一のバイナリデータとしてファイルに保存したり、 ネットワーク上に送信することは非常に稀なことだと思われます。 確かに公開鍵は鍵コンテナからエクスポートされるものですが、 それはあくまで証明書に公開鍵を格納するためであり、 証明書の公開を通じて公開鍵を公開するのが基本だからです。 証明書には、公開鍵に対応する秘密鍵を持つ所有者情報が記述されているため、 証明書に含まれる公開鍵を使ってデータを複合化できたならば、 まさしく証明書に記述されている所有者によってデータが暗号化されたことが分かります。 単純に公開鍵だけが公開されてデータを復号化できた場合は、 そのデータが対応する秘密鍵の所有者によって暗号化されたことを示すだけであり、 その所有者が実際のところ誰なのかまでは特定できません。

署名には所有者情報が含まれると述べましたが、 CryptSignHashで作成される署名は、単に秘密鍵で暗号化されたハッシュ値に過ぎません。 一般的にいわれる署名では、これに加えて署名者の証明書が添付され、 署名の検証には証明書に含まれる公開鍵が使用されます。 証明書ベースの署名関数であるCryptSignMessageが返すデータは、 実際のデータと署名、そして証明書の3つを含むことになっています。 このデータはPKCS#7形式でフォーマットされており、 これを利用すれば、今回のプログラムのように、 独自のフォーマットで個別にデータを保存する必要もなかったでしょう。 CryptSignHashの署名に証明書が含まれない点は、 インターネットのようなグローバルな通信において欠点となる場合があります。



戻る