EternalWindows
PKCS #7 / SignedData型と検証

SignedData型の署名データを検証するには、CryptVerifyMessageSignatureを呼び出します。

BOOL WINAPI CryptVerifyMessageSignature(
  PCRYPT_VERIFY_MESSAGE_PARA pVerifyPara,
  DWORD dwSignerIndex,
  const BYTE *pbSignedBlob,
  DWORD cbSignedBlob,
  BYTE *pbDecoded,
  DWORD *pcbDecoded,
  PCCERT_CONTEXT *ppSignerCert
);

pVerifyParaは、検証情報を含んでいるCRYPT_VERIFY_MESSAGE_PARA構造体のアドレスを指定します。 dwSignerIndexは、どの署名者の証明書で検証を行うかのインデックスを指定します。 CryptSignMessageでは複数の署名者を設定できないので、署名者のインデックスは0になるはずです、 pbSignedBlobは、署名されたデータを指定します。 cbSignedBlobは、署名されたデータのサイズを指定します。 pbDecodedは、デコードされたデータを受け取るバッファを指定します。 pcbDecodedは、バッファのサイズを指定します。 pbDecodedにNULLを指定した場合、バッファのサイズを受け取ることができます。 ppSignerCertは、署名されたデータに含まれる署名者の証明書が返ります。 不要な場合は、NULLを指定することができます。

CRYPT_VERIFY_MESSAGE_PARA構造体の定義を次に示します。

typedef struct _CRYPT_VERIFY_MESSAGE_PARA {
  DWORD                             cbSize;
  DWORD                             dwMsgAndCertEncodingType;
  HCRYPTPROV_LEGACY                 hCryptProv;
  PFN_CRYPT_GET_SIGNER_CERTIFICATE  pfnGetSignerCertificate;
  void*                             pvGetArg;
} CRYPT_VERIFY_MESSAGE_PARA,         *PCRYPT_VERIFY_MESSAGE_PARA;

cbSizeは、構造体のサイズをバイト単位で指定します。 dwMsgAndCertEncodingTypeは、エンコーディングタイプを指定します。 hCryptProvは、署名を検証するためのCSPのハンドルを指定します。 Windows Vista以降では、0を指定します。 pfnGetSignerCertificateは、署名者の証明書を検索するコールバック関数のアドレスを指定します。 NULLを指定した場合、既定の方法で署名者の証明書が取得されます。 pvGetArgは、コールバック関数に渡したい引数を指定します。 コールバック関数の定義は、次のようになります。

PCCERT_CONTEXT WINAPI CryptGetSignerCertificateCallback(
  void *pvGetArg,
  DWORD dwCertEncodingType,
  PCERT_INFO pSignerId,
  HCERTSTORE hMsgCertStore
);

pvGetArgは、CRYPT_VERIFY_MESSAGE_PARA構造体のpvGetArgメンバの値が格納されます。 dwCertEncodingTypeは、エンコーディングタイプが格納されます。 pSignerIdは、IssuerメンバとSerialNumberメンバを初期化したCERT_INFO構造体のアドレスが格納されます。 SignedData型には、署名の際に使用した証明書のIssuerとSerialNumberが含まれるため、 このような引数を受け取ることができます。 hMsgCertStoreは、CryptGetMessageCertificatesの戻り値が格納されます。 この関数は、PKCS #7形式のデータを証明書ストアと見立てます。 次にコールバック関数の実装例を示します。

PCCERT_CONTEXT WINAPI CryptGetSignerCertificateCallback(void *pvGetArg, DWORD dwCertEncodingType, PCERT_INFO pSignerId, HCERTSTORE hMsgCertStore)
{
	HCERTSTORE     hStore;
	PCCERT_CONTEXT pSignerContext;

	pSignerContext = CertGetSubjectCertificateFromStore(hMsgCertStore, PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, pSignerId);
	if (pSignerContext == NULL) {
		hStore = CertOpenSystemStore(0, TEXT("MY"));
		if (hStore == NULL)
			return NULL;
		pSignerContext = CertGetSubjectCertificateFromStore(hStore, PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, pSignerId);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
	}
	
	return pSignerContext;
}

前節で述べたように、署名に使った証明書というのはデフォルトでは署名データに含まれません。 含めるようにした場合、hMsgCertStoreに証明書が格納されていることから、 CertGetSubjectCertificateFromStoreがpSignerIdの情報を持つ証明書を返すことになりますが、 含まれていない場合でも対策はあります。 それは、IDを基にシステムストアから証明書を検索するという方法です。 検証を署名を行ったコンピュータ上で行うのであれば、 署名に使った証明書を検索することができため、 コールバック関数を独自に用意することにより、 あたかも署名データに証明書が含まれているかのように振舞うことができるのです。

今回のプログラムは、前節で作成したsign.datをCryptVerifyMessageSignatureを用いて検証します。

#include <windows.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	CRYPT_VERIFY_MESSAGE_PARA verifyPara;
	LPBYTE                    lpData;
	DWORD                     dwDataSize;
	BOOL                      bResult;
	LPBYTE                    lpSignedBlob;
	DWORD                     dwSignedBlobSize;
	HANDLE                    hFile;
	DWORD                     dwReadByte;
	
	hFile = CreateFile(TEXT("sign.dat"), GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return 0;

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

	verifyPara.cbSize                   = sizeof(CRYPT_VERIFY_MESSAGE_PARA);
	verifyPara.dwMsgAndCertEncodingType = PKCS_7_ASN_ENCODING | X509_ASN_ENCODING;
	verifyPara.hCryptProv               = 0;
	verifyPara.pfnGetSignerCertificate  = NULL;
	verifyPara.pvGetArg                 = NULL;

	bResult = CryptVerifyMessageSignature(&verifyPara, 0, lpSignedBlob, dwSignedBlobSize, NULL, &dwDataSize, NULL);
	if (!bResult) {
		MessageBox(NULL, TEXT("検証に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
 
	bResult = CryptVerifyMessageSignature(&verifyPara, 0, lpSignedBlob, dwSignedBlobSize, lpData, &dwDataSize, NULL);
	if (!bResult) {
		MessageBox(NULL, TEXT("検証に失敗しました。"), NULL, MB_ICONWARNING);
		HeapFree(GetProcessHeap(), 0, lpSignedBlob);
		return 0;
	}
	
	MessageBox(NULL, (LPTSTR)lpData, TEXT("OK"), MB_OK);

	HeapFree(GetProcessHeap(), 0, lpSignedBlob);

	return 0;
}

前節では、CryptSignMessageで取得したデータをそのままファイルに書き込んでいたため、 ファイルのサイズの値が署名データのサイズとなります。 これを基にメモリを確保し、ファイルを読み取れば署名データを取得することができます。 CRYPT_VERIFY_MESSAGE_PARA構造体のdwMsgAndCertEncodingTypeは、 PKCS_7_ASN_ENCODINGを指定するのは当然ですが、X509_ASN_ENCODINGも指定しています。 これは、署名データに証明書が含まれているからであり、 その証明書に含まれる公開鍵を使って秘密鍵で暗号化されたハッシュ値を複合化する必要があるからです。 よって、証明書のフォーマットを理解できなければならないため、 X509_ASN_ENCODINGを指定することになります。 CryptVerifyMessageSignatureの呼び出しは、次のようになっています。

bResult = CryptVerifyMessageSignature(&verifyPara, 0, lpSignedBlob, dwSignedBlobSize, NULL, &dwDataSize, NULL);

第2引数は署名者のインデックスですが、前節では署名者を1人しか設定していないので0を指定します。 このCryptVerifyMessageSignatureの呼び出しによって検証されるデータのサイズを取得できるため、 後はその分のメモリを確保し、2回目のCryptVerifyMessageSignatureで実際に検証を行えることになります。 1回目のCryptVerifyMessageSignatureの呼び出しでは、たとえデータが変更されていても 関数が成功するので注意してください。


戻る