EternalWindows
CSP / 検証と鍵のインポート

署名されたデータを検証するには、 そのデータから実際のデータの部分と署名の部分を抜き出し、 実際のデータのハッシュ値を算出します。 このハッシュ値と署名に加え、公開鍵のハンドルがあれば、 CryptVerifySignatureで署名の検証を行うことができます。

BOOL WINAPI CryptVerifySignature(
  HCRYPTHASH hHash, 
  BYTE *pbSignature, 
  DWORD dwSigLen, 
  HCRYPTKEY hPubKey, 
  LPCTSTR sDescription, 
  DWORD dwFlags 
);

hHashは、ハッシュオブジェクトのハンドルを指定します。 このハッシュオブジェクトは、ハッシュ値を持っていなければなりません。 pbSignatureは、署名を格納したバッファを指定します。 dwSigLenは、署名のサイズを指定します。 hPubKeyは、署名を複合化するために使う公開鍵のハンドルを指定します。 sDescriptionは、NULLを指定します。 dwFlagsはCSP特有のフラグであり、0を指定しても問題ありません。 この関数の戻り値がTRUEである場合、ハッシュオブジェクトに格納されているハッシュ値と 公開鍵で複合化したハッシュ値が同一であるということになり、 データは変更されていないことが分かります。

CryptSignHashが鍵の指定に定数を使うのに対して、 CryptVerifySignatureでは鍵のハンドルを指定しなければならない点については、 いくつか補足しておくべきところでしょう。 CryptVerifySignatureで必要となるのはCryptSignHashで使用された秘密鍵に 対応する公開鍵ですから、鍵コンテナに格納されている公開鍵は必ずしも使えるとは限りません。 CryptVerifySignatureの呼び出しが別のコンピュータで行われたならば、 そこのCSPの鍵コンテナに格納されている鍵ペアの公開鍵は、 CryptSignHashで利用された秘密鍵とは全く関係がありませんから、 AT_KEYEXCHANGEなどの定数で鍵コンテナを照会するという方法は成立しないわけです。 よって、明示的にエクスポートされた公開鍵をインポートする必要があります。 次に示すCryptImportKeyは、鍵のバイナリデータから鍵のハンドルを取得します。

BOOL WINAPI CryptImportKey(
  HCRYPTPROV hProv, 
  BYTE *pbData, 
  DWORD dwDataLen, 
  HCRYPTKEY hPubKey, 
  DWORD dwFlags, 
  HCRYPTKEY *phKey 
);

hProvは、CSPのハンドルを指定します。 pbDataは、インポートする鍵のバイナリデータを指定します。 dwDataLenは、データのサイズを指定します。 hPubKeyは、データが暗号化されている場合において、 複合化に使用する鍵のハンドルを指定します。 dwFlagsは、基本的に0を指定します。 phKeyは、インポートした鍵のハンドルが格納されます。

今回のプログラムは、前節で作成したsign.datの内容が変更されていないかを検証します。 検証に使用する公開鍵は、同じく前節で作成されたpubkey.datからインポートします。

#include <windows.h>

BOOL ImportPublicKey(HCRYPTPROV hProv, LPTSTR lpszFileName, HCRYPTKEY *phKey);
BOOL VerifyData(HCRYPTPROV hProv, HCRYPTKEY hPubKey, LPTSTR lpszFileName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HCRYPTPROV hProv;
	HCRYPTKEY  hPubKey;

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

	if (!ImportPublicKey(hProv, TEXT("pubkey.dat"), &hPubKey)) {
		MessageBox(NULL, TEXT("公開鍵のインポートに失敗しました。"), NULL, MB_ICONWARNING);
		CryptReleaseContext(hProv, 0);
		return 0;
	}

	if (VerifyData(hProv, hPubKey, TEXT("sign.dat")))
		MessageBox(NULL, TEXT("署名に問題はありません。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("署名の検証に失敗しました。"), NULL, MB_ICONWARNING);

	CryptDestroyKey(hPubKey);
	CryptReleaseContext(hProv, 0);

	return 0;
}

BOOL ImportPublicKey(HCRYPTPROV hProv, LPTSTR lpszFileName, HCRYPTKEY *phKey)
{
	HANDLE hFile;
	LPBYTE lpKey;
	DWORD  dwKeySize;
	DWORD  dwReadByte;
	BOOL   bResult;
	
	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;
	
	dwKeySize = GetFileSize(hFile, NULL);
	lpKey = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeySize);
	ReadFile(hFile, lpKey, dwKeySize, &dwReadByte, NULL);
	
	bResult = CryptImportKey(hProv, lpKey, dwKeySize, 0, 0, phKey);

	CloseHandle(hFile);	
	HeapFree(GetProcessHeap(), 0, lpKey);

	return bResult;
}

BOOL VerifyData(HCRYPTPROV hProv, HCRYPTKEY hPubKey, LPTSTR lpszFileName)
{
	HANDLE     hFile;
	BOOL       bResult;
	LPBYTE     lpData;
	LPBYTE     lpSignature;
	DWORD      dwReadByte;
	DWORD      dwDataSize;
	DWORD      dwSignatureSize;
	HCRYPTHASH hHash;

	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;

	ReadFile(hFile, &dwDataSize, sizeof(DWORD), &dwReadByte, NULL);
	ReadFile(hFile, &dwSignatureSize, sizeof(DWORD), &dwReadByte, NULL);
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	lpSignature = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignatureSize);
	ReadFile(hFile, lpData, dwDataSize, &dwReadByte, NULL);
	ReadFile(hFile, lpSignature, dwSignatureSize, &dwReadByte, NULL);

	if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) {
		CloseHandle(hFile);
		HeapFree(GetProcessHeap(), 0, lpData);
		HeapFree(GetProcessHeap(), 0, lpSignature);
	}

	CryptHashData(hHash, lpData, dwDataSize, 0);
	
	bResult = CryptVerifySignature(hHash, lpSignature, dwSignatureSize, hPubKey, NULL, 0);

	CryptDestroyHash(hHash);
	CloseHandle(hFile);
	HeapFree(GetProcessHeap(), 0, lpData);
	HeapFree(GetProcessHeap(), 0, lpSignature);

	return bResult;
}

このプログラムの最大の要点は、署名の検証に必要な公開鍵を外部から取得しているところです。 確かにこのプログラムを前節と同じコンピュータで実行させているならば、 CryptAcquireContextに鍵コンテナの名前と0を指定し、 CryptGetUserKeyで鍵ペアのハンドルを取得することができますが、 そのような自分で自分の署名を確認するコードが必要になることは滅多にありません。 よって、外部のコンピュータで検証が行われることを想定したコードを記述することになります。

BOOL ImportPublicKey(HCRYPTPROV hProv, LPTSTR lpszFileName, HCRYPTKEY *phKey)
{
	HANDLE hFile;
	LPBYTE lpKey;
	DWORD  dwKeySize;
	DWORD  dwReadByte;
	BOOL   bResult;
	
	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;
	
	dwKeySize = GetFileSize(hFile, NULL);
	lpKey = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwKeySize);
	ReadFile(hFile, lpKey, dwKeySize, &dwReadByte, NULL);
	
	bResult = CryptImportKey(hProv, lpKey, dwKeySize, 0, 0, phKey);

	CloseHandle(hFile);	
	HeapFree(GetProcessHeap(), 0, lpKey);

	return bResult;
}

この自作関数は、前節で作成したpubkey.datから公開鍵をインポートします。 ファイルには公開鍵のバイナリデータのみを書き込むようにしていたので、 ファイルのサイズをデータのサイズと解釈して読み取ることができます。 後は、そのデータをCryptImportKeyに指定すれば、第6引数に公開鍵のハンドルが返ることになります。 第4引数にはデータを複合化するときに使用する鍵のハンドルを指定しますが、 公開鍵のエクスポート時に暗号化が行われていることは基本的にありません。 よって、公開鍵のインポート時に複合化を行う必要もありませんから 第4引数は0を指定することになります。

BOOL VerifyData(HCRYPTPROV hProv, HCRYPTKEY hPubKey, LPTSTR lpszFileName)
{
	HANDLE     hFile;
	BOOL       bResult;
	LPBYTE     lpData;
	LPBYTE     lpSignature;
	DWORD      dwReadByte;
	DWORD      dwDataSize;
	DWORD      dwSignatureSize;
	HCRYPTHASH hHash;

	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;

	ReadFile(hFile, &dwDataSize, sizeof(DWORD), &dwReadByte, NULL);
	ReadFile(hFile, &dwSignatureSize, sizeof(DWORD), &dwReadByte, NULL);
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	lpSignature = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignatureSize);
	ReadFile(hFile, lpData, dwDataSize, &dwReadByte, NULL);
	ReadFile(hFile, lpSignature, dwSignatureSize, &dwReadByte, NULL);

	if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) {
		CloseHandle(hFile);
		HeapFree(GetProcessHeap(), 0, lpData);
		HeapFree(GetProcessHeap(), 0, lpSignature);
	}

	CryptHashData(hHash, lpData, dwDataSize, 0);
	
	bResult = CryptVerifySignature(hHash, lpSignature, dwSignatureSize, hPubKey, NULL, 0);

	CryptDestroyHash(hHash);
	CloseHandle(hFile);
	HeapFree(GetProcessHeap(), 0, lpData);
	HeapFree(GetProcessHeap(), 0, lpSignature);

	return bResult;
}

この自作関数では、前節で作成された署名を検証します。 まず、前節で書き込んだフォーマット通り、各データを順に読み取っていきます。 次に、取得した実際のデータ(前節の"sample-data")からハッシュ値を作成し、 これと取得した署名をCryptVerifySignatureに指定します。 これにより、署名が第4引数に指定した公開鍵で複合化され、 さらにその複合化された値とハッシュ値が比較されます。 この一連の作業に問題がなければ、署名が正しいことが分かります。

鍵と許可フラグ

外部から取得した鍵がどのような操作を行えるかは、重要な確認事項であるといえます。 たとえば、ファイルから公開鍵をインポートしたとして、 その公開鍵は暗号化を行うことができるのでしょうか。 AT_SIGNATUREの鍵ペアは暗号化がサポートされていないため、 インポートした公開鍵がAT_SIGNATUREのものであるとしたら、 暗号化に利用することはできないことになります。 鍵にはどのような操作が許可されるかを表すフラグが含まれているため、 これを確認することで実際の操作に入る前に判定を行うことができます。

dwBufferSize = sizeof(DWORD);
CryptGetKeyParam(hKey, KP_PERMISSIONS, (LPBYTE)&dwPermissions, &dwBufferSize, 0);

if (dwPermissions & CRYPT_ENCRYPT)
	MessageBox(NULL, TEXT("暗号化が許可されています。"), TEXT("OK"), MB_OK);
if (dwPermissions & CRYPT_EXPORT)
	MessageBox(NULL, TEXT("エクスポートが許可されています。"), TEXT("OK"), MB_OK);

CryptGetKeyParamにKP_PERMISSIONSを指定した場合、 第3引数に複数の許可フラグが格納されることになります。 ただし、話を分かりにくくするようですが、この許可フラグの値は信用してよいのか疑問です。 たとえば、暗号化が許可されていない公開鍵を指定しても、 dwPermissionsにはCRYPT_ENCRYPTが含まれることになります。 また、CRYPT_EXPORTを含んでいない鍵であっても、 その鍵をCryptExportKeyでエクスポートできる場合があることを指摘しておきます。 鍵にCRYPT_EXPORTABLEを指定した場合、 その鍵にはエクスポート可能であることを示すCRYPT_EXPORTが設定され、 CryptExportKeyの呼び出し時にそれは確認されていると思われます。 ただし、鍵ペアの公開鍵をエクスポートする場合は、 CRYPT_EXPORTが設定されていることを考慮しません。 公開鍵は公開されるべきものであるため、これは当然の仕様といえます。



戻る