EternalWindows
CNG / 署名と検証

署名というのは現実におけるそれと同じように、あるデータの作成者が確かに自分であることを保障するためのものです。 署名されたデータの受信側はその署名を検証し、 その結果が想定する相手からのものであれば、データを取り扱うようになります。 署名を実行するステップは、次のようになります。

1, 署名したいデータ(元データ)を用意する。
2, そのデータのハッシュ値を生成する。
3, 生成したハッシュ値を秘密鍵で暗号化する。
4, 暗号化されたデータ(署名)と元データを1つにまとめる。

秘密鍵で暗号化するという点が重要です。 このようにすれば、署名を複合化できるのは秘密鍵に対応する公開鍵だけになるため、 複合化に成功したら、秘密鍵の所有者によってデータが提供されたことが分かります。 署名を検証するステップは、次のようになります。

1, 1つにまとめられたデータから署名と元データを取得する。
2, 元データのハッシュ値を生成する。
3, 署名を公開鍵で複合化する。
4, 生成したハッシュ値と複合化された値を比較する。

3の部分が成功した時点で、署名は正しく検証できているといえますが、 その複合化したデータと元データのハッシュ値を比較することで、 データが変更されていないかも検証することができるようになります。 1つのデータから得られるハッシュ値は常に同一であるため、 署名時に生成したハッシュ値と検証時に生成したハッシュ値が異なるのであれば、 署名と共に格納されている元データが不正に改ざんされていると考えることができます。

署名と元データを1つにまとめるという作業は、非常に重要です。 署名自体は、ハッシュ値を秘密鍵で暗号化したデータに過ぎませんから、 これを単一で通信相手に送信しても、受信側はどのようなデータに対しての署名なのかが特定できません。 つまり、署名と元データという分離された2つのデータを1つにした時に、 初めてデータに署名を行ったといえるようになります。 なお、公開鍵暗号で取り扱われるデータの形式を規格するPKCS #7では、 署名を含むデータの標準形式としてSignedData型を定義していますが、 アプリケーション固有の方法で形式でデータをまとめて問題はありません。

アルゴリズムプロバイダで署名を行うには、 鍵のアルゴリズムがDSAやECDSAに設定されている必要があります。 この場合、BCryptSignHashで署名を作成することができます。

NTSTATUS WINAPI BCryptSignHash(
  BCRYPT_KEY_HANDLE hKey,
  VOID *pPaddingInfo,
  PBYTE pbInput,
  DWORD cbInput,
  PBYTE pbOutput,
  DWORD cbOutput,
  DWORD *pcbResult,
  ULONG dwFlags
);

hKeyは、鍵のハンドルを指定します。 pPaddingInfoは、パディング情報を格納したバッファを指定します。 pbInputは、署名したいデータを格納したバッファを指定します。 通常、これはハッシュ値であるはずです。 cbInputは、pbInputのサイズを指定します。 pbOutputは、署名データを受け取るバッファを指定します。 cbOutputは、pbOutputのサイズを指定します。 pcbResultは、pbOutputにコピーされたデータのサイズを受け取る変数のアドレスを指定します。 pbOutputにNULLを指定した場合は、必要なバッファのサイズが返ります。 dwFlagsは、パディング情報の形式を表す定数を指定します。 不要な場合は、0を指定することができます。

署名を検証するには、BCryptVerifySignatureを呼び出します。

NTSTATUS WINAPI BCryptVerifySignature(
  BCRYPT_KEY_HANDLE hKey,
  VOID *pPaddingInfo,
  PUCHAR pbHash,
  ULONG cbHash,
  PUCHAR pbSignature,
  ULONG cbSignature,
  ULONG dwFlags
);

hKeyは、鍵のハンドルを指定します。 pPaddingInfoは、パディング情報を格納したバッファを指定します。 pbHashは、検証対象のデータのハッシュ値を指定します。 cbHashは、pbHashのサイズを指定します。 pbSignatureは、署名データを格納したバッファを指定します。 cbSignatureは、pbSignatureのサイズを指定します。 dwFlagsは、パディング情報の形式を表す定数を指定します。 不要な場合は、0を指定することができます。

署名を検証する側は、署名を行う側から予め公開鍵のデータを受け取っているはずであるため、 それを鍵として扱えるようインポートする必要があります。 これは、BCryptImportKeyPairで行うことができます。

NTSTATUS WINAPI BCryptImportKeyPair(
  BCRYPT_ALG_HANDLE hAlgorithm,
  BCRYPT_KEY_HANDLE hImportKey,
  LPCWSTR pszBlobType,
  BCRYPT_KEY_HANDLE *phKey,
  PUCHAR pbInput,
  ULONG cbInput,
  ULONG dwFlags
);

hAlgorithmは、アルゴリズムプロバイダのハンドルを指定します。 hImportKeyは、現在は使用されないためNULLを指定します。 pszBlobTypeは、インポートするデータの形式を指定します。 phKeyは、インポートした鍵を受け取る変数のアドレスを指定します。 pbInputは、インポートしたいデータを格納したバッファを指定します。 cbInputは、pbInputのサイズを指定します。 dwFlagsは、BCRYPT_NO_KEY_VALIDATIONという定数を指定できますが、基本的には0を指定します。

今回のプログラムは、署名と検証を行うコードで構成されています。 署名時には、privkey.datに格納されている秘密鍵を使用し、 検証時にはpubkey.datに格納されている公開鍵を使用します。 これらのファイルは、前節のプログラムで作成することができます。

#include <windows.h>
#include <bcrypt.h>

#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)Status) >= 0)
#endif

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

BOOL SignData(BCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPBYTE lpData, DWORD dwDataSize);
BOOL VerifyData(BCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPBYTE *lplpData, LPDWORD lpdwDataSize);
BOOL GetHashData(LPBYTE lpData, DWORD dwDataSize, LPBYTE *lplpHashData, LPDWORD lpdwHashDataSize);
BOOL ImportKeyData(BCRYPT_ALG_HANDLE hAlg, LPTSTR lpszFileName, LPWSTR lpszType, BCRYPT_KEY_HANDLE *phKey);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	BCRYPT_ALG_HANDLE  hAlg;
	BCRYPT_KEY_HANDLE  hKey;
	DWORD              dwDataSize;
	LPBYTE             lpData;
	NTSTATUS           status;
	TCHAR              szPrivKeyFileName[] = TEXT("privkey.dat");
	TCHAR              szPubKeyFileName[] = TEXT("pubkey.dat");
	TCHAR              szFileName[] = TEXT("sign.dat");
	TCHAR              szData[] = TEXT("sample-data");
	BOOL               bSign = TRUE;

	status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_ECDSA_P256_ALGORITHM, NULL, 0);
	if (!NT_SUCCESS(status))
		return 0;

	if (bSign) {
		if (ImportKeyData(hAlg, szPrivKeyFileName, BCRYPT_ECCPRIVATE_BLOB, &hKey)) {
			if (SignData(hKey, szFileName, (LPBYTE)szData, sizeof(szData))) {
				MessageBox(NULL, TEXT("署名をしました。"), TEXT("OK"), MB_OK);
				BCryptDestroyKey(hKey);
			}
		}
	}
	else {
		if (ImportKeyData(hAlg, szPubKeyFileName, BCRYPT_ECCPUBLIC_BLOB, &hKey)) {
			if (VerifyData(hKey, szFileName, &lpData, &dwDataSize)) {
				MessageBox(NULL, (LPTSTR)lpData, TEXT("OK"), MB_OK);
				HeapFree(GetProcessHeap(), 0, lpData);
				BCryptDestroyKey(hKey);
			}
		}
	}
	
	BCryptCloseAlgorithmProvider(hAlg, 0);

	return 0;
}

BOOL SignData(BCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPBYTE lpData, DWORD dwDataSize)
{
	HANDLE   hFile;
	DWORD    dwWriteByte;
	DWORD    dwSignatureSize;
	DWORD    dwHashDataSize;
	LPBYTE   lpSignature;
	LPBYTE   lpHashData;
	NTSTATUS status;

	if (!GetHashData(lpData, dwDataSize, &lpHashData, &dwHashDataSize))
		return FALSE;
	
	BCryptSignHash(hKey, NULL, (LPBYTE)lpHashData, dwHashDataSize, NULL, 0, &dwSignatureSize, 0);
	lpSignature = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignatureSize);
	status = BCryptSignHash(hKey, NULL, (LPBYTE)lpHashData, dwHashDataSize, lpSignature, dwSignatureSize, &dwSignatureSize, 0);
	if (!NT_SUCCESS(status)) {
		HeapFree(GetProcessHeap(), 0, lpHashData);
		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);
	
	HeapFree(GetProcessHeap(), 0, lpHashData);
	HeapFree(GetProcessHeap(), 0, lpSignature);

	return TRUE;
}

BOOL VerifyData(BCRYPT_KEY_HANDLE hKey, LPTSTR lpszFileName, LPBYTE *lplpData, LPDWORD lpdwDataSize)
{
	HANDLE   hFile;
	DWORD    dwReadByte;
	DWORD    dwDataSize;
	DWORD    dwSignatureSize;
	DWORD    dwHashDataSize;
	LPBYTE   lpData;
	LPBYTE   lpHashData;
	LPBYTE   lpSignature;
	NTSTATUS status;

	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);
	CloseHandle(hFile);
	
	if (!GetHashData(lpData, dwDataSize, &lpHashData, &dwHashDataSize)) {
		HeapFree(GetProcessHeap(), 0, lpData);
		HeapFree(GetProcessHeap(), 0, lpSignature);
		return FALSE;
	}

	status = BCryptVerifySignature(hKey, NULL, lpHashData, dwHashDataSize, lpSignature, dwSignatureSize, 0);

	*lplpData = lpData;
	*lpdwDataSize = dwDataSize;

	HeapFree(GetProcessHeap(), 0, lpHashData);
	HeapFree(GetProcessHeap(), 0, lpSignature);

	return NT_SUCCESS(status);
}

BOOL GetHashData(LPBYTE lpData, DWORD dwDataSize, LPBYTE *lplpHashData, LPDWORD lpdwHashDataSize)
{
	BCRYPT_ALG_HANDLE  hAlg;
	BCRYPT_HASH_HANDLE hHash;
	DWORD              dwResult;
	DWORD              dwHashObjectSize;
	LPBYTE             lpHashObject;
	NTSTATUS           status;

	status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_MD5_ALGORITHM, NULL, 0);
	if (!NT_SUCCESS(status))
		return FALSE;

	BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH, (LPBYTE)&dwHashObjectSize, sizeof(DWORD), &dwResult, 0);
	lpHashObject = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwHashObjectSize);
	status = BCryptCreateHash(hAlg, &hHash, lpHashObject, dwHashObjectSize, NULL, 0, 0);
	if (!NT_SUCCESS(status)) {
		HeapFree(GetProcessHeap(), 0, lpHashObject);
		BCryptCloseAlgorithmProvider(hAlg, 0);
		return FALSE;
	}
	
	BCryptHashData(hHash, lpData, dwDataSize, 0);

	BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH, (LPBYTE)lpdwHashDataSize, sizeof(DWORD), &dwResult, 0);
	*lplpHashData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, *lpdwHashDataSize);
	BCryptFinishHash(hHash, *lplpHashData, *lpdwHashDataSize, 0);

	HeapFree(GetProcessHeap(), 0, lpHashObject);
	BCryptDestroyHash(hHash);
	BCryptCloseAlgorithmProvider(hAlg, 0);
	
	return TRUE;
}

BOOL ImportKeyData(BCRYPT_ALG_HANDLE hAlg, LPTSTR lpszFileName, LPWSTR lpszType, BCRYPT_KEY_HANDLE *phKey)
{
	HANDLE   hFile;
	DWORD    dwReadByte;
	DWORD    dwDataSize;
	LPBYTE   lpData;
	NTSTATUS status;

	hFile = CreateFile(lpszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;
	
	dwDataSize = GetFileSize(hFile, NULL);
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	ReadFile(hFile, lpData, dwDataSize, &dwReadByte, NULL);
	CloseHandle(hFile);

	status = BCryptImportKeyPair(hAlg, NULL, lpszType, phKey, lpData, dwDataSize, 0);

	HeapFree(GetProcessHeap(), 0, lpData);

	return NT_SUCCESS(status);
}

このプログラムは、署名と検証のコードの両方を含んでいますが、 本来想定している状況は、別コンピュータ上での実行です。 つまり、片方のコンピュータではbSignをTRUEにして実行し、 もう片方のコンピュータではbSignをFALSEにして実行します。 また、bSignをFALSEにして実行する検証側には、 予め公開鍵(pubkey.dat)と今回のプログラムで作成した署名されたファイル(sample.dat)を 送信しておきます。 秘密鍵の方は、作成元のコンピュータで管理するべきものですから、 決して公開してはいけません。 それではまず、WinMainの処理を確認します。

if (bSign) {
	if (ImportKeyData(hAlg, szPrivKeyFileName, BCRYPT_ECCPRIVATE_BLOB, &hKey)) {
		if (SignData(hKey, szFileName, (LPBYTE)szData, sizeof(szData))) {
			MessageBox(NULL, TEXT("署名をしました。"), TEXT("OK"), MB_OK);
			BCryptDestroyKey(hKey);
		}
	}
}
else {
	if (ImportKeyData(hAlg, szPubKeyFileName, BCRYPT_ECCPUBLIC_BLOB, &hKey)) {
		if (VerifyData(hKey, szFileName, &lpData, &dwDataSize)) {
			MessageBox(NULL, (LPTSTR)lpData, TEXT("OK"), MB_OK);
			HeapFree(GetProcessHeap(), 0, lpData);
			BCryptDestroyKey(hKey);
		}
	}
}

bSignがTRUEの場合は、BCryptSignHashで署名を行うことになるため、 秘密鍵を事前にインポートしておくことになります。 インポートを行うのはImportKeyDataという関数であり、 秘密鍵のインポートの場合はBCRYPT_ECCPRIVATE_BLOB、 公開鍵のインポートの場合はBCRYPT_ECCPUBLIC_BLOBを指定します。 ECC鍵タイプを指定するのは、前節で作成した鍵ペアがECDSAアルゴリズムであるためです。 ImportKeyDataの処理は次のようになっています。

dwDataSize = GetFileSize(hFile, NULL);
lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
ReadFile(hFile, lpData, dwDataSize, &dwReadByte, NULL);
CloseHandle(hFile);

status = BCryptImportKeyPair(hAlg, NULL, lpszType, phKey, lpData, dwDataSize, 0);

ファイルには鍵のバイトデータのみを書き込んでいるため、 ファイルサイズを鍵のサイズと考えることができます。 この値をファイル読み取り、データを格納したバッファをBCryptImportKeyPairに指定すれば、 第4引数に鍵のハンドルが返ることになります。

鍵のハンドルを取得すれば、BCryptSignHashで署名を行うことができます。 次のコードは、SignDataの一部です。

if (!GetHashData(lpData, dwDataSize, &lpHashData, &dwHashDataSize))
	return FALSE;

BCryptSignHash(hKey, NULL, (LPBYTE)lpHashData, dwHashDataSize, NULL, 0, &dwSignatureSize, 0);
lpSignature = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignatureSize);
status = BCryptSignHash(hKey, NULL, (LPBYTE)lpHashData, dwHashDataSize, lpSignature, dwSignatureSize, &dwSignatureSize, 0);
if (!NT_SUCCESS(status)) {
	HeapFree(GetProcessHeap(), 0, lpHashData);
	HeapFree(GetProcessHeap(), 0, lpSignature);
	return FALSE;
}

BCryptSignHashで署名の対象するのはデータのハッシュ値であるため、 最初にGetHashDataを呼び出してハッシュ値を取得しておきます。 この関数の処理はハッシュオブジェクトについて説明した際のものと同様です。 BCryptSignHashの1回目の呼び出しでは、作成される署名のサイズが分からないので、 第5引数にNULLを指定して第7引数でサイズを取得します。 そして、バッファを確保し、2回目の呼び出しで署名を取得します。

取得した署名は元データ(今回の場合"sample-data")と共に格納しなければならないため、 SignDataではそのような処理も行っています。 データだけ書き込んだ場合は、各データのサイズが分からないため、 まず、元データのサイズと署名のサイズをファイルに書き込み、 その後に元データと署名を書き込むようにしています。 VerifyDataではこの通りの順番にファイルを読み取り、 BCryptVerifySignatureを呼び出すことになります。

if (!GetHashData(lpData, dwDataSize, &lpHashData, &dwHashDataSize)) {
	HeapFree(GetProcessHeap(), 0, lpData);
	HeapFree(GetProcessHeap(), 0, lpSignature);
	return FALSE;
}

status = BCryptVerifySignature(hKey, NULL, lpHashData, dwHashDataSize, lpSignature, dwSignatureSize, 0);

BCryptVerifySignatureでは、署名を秘密鍵で複合化すると共に、 その値と第3引数に指定したデータを比較することになっています。 このため、元データのハッシュ値を指定することになります。


戻る