EternalWindows
PKCS #7 / SignedData型とエンコード

今回は、データをSignedData型にエンコードする方法について説明します。 SignedData型は、署名データを維持することができる形式ですが、 実際には必ずしもそれが含まれるわけではありません。 たとえば、証明書をPKCS#7形式でエクスポートした場合、 その.p7bファイルの形式はSignedData型になっており、 署名者が存在しないことから、署名データは含まれていません。 つまり、単純に証明書を維持するための形式としても利用することができます。 データをエンコードするには、CryptMsgOpenToEncodeを呼び出します。

HCRYPTMSG WINAPI CryptMsgOpenToEncode(
  DWORD dwMsgEncodingType,
  DWORD dwFlags,
  DWORD dwMsgType,
  const void *pvMsgEncodeInfo,
  LPSTR pszInnerContentObjID,
  PCMSG_STREAM_INFO pStreamInfo
);

dwMsgEncodingTypeは、エンコーディングタイプを指定します。 dwFlagsは、基本的に0を指定します。 dwMsgTypeは、メッセージタイプを指定します。 SignedData型ならCMSG_SIGNEDを指定し、EnvelopedData型ならCMSG_ENVELOPEDを指定します。 pvMsgEncodeInfoは、メッセージタイプに関連する構造体を指定します。 pszInnerContentObjIDは、コンテンツのOIDを指定することができますが、NULLで問題ありません。 pStreamInfoは、ストリーミングに関する引数ですが、基本的にNULLを指定します。 戻り値は、メッセージのハンドルが返ります。

CryptMsgOpenToEncodeのdwMsgTypeにCMSG_SIGNEDを指定した場合、 pvMsgEncodeInfoにCMSG_SIGNED_ENCODE_INFO構造体のアドレスを指定します。

typedef struct _CMSG_SIGNED_ENCODE_INFO {
  DWORD                      cbSize;
  DWORD                      cSigners;
  PCMSG_SIGNER_ENCODE_INFO   rgSigners;
  DWORD                      cCertEncoded;
  PCERT_BLOB                 rgCertEncoded;
  DWORD                      cCrlEncoded;
  PCRL_BLOB                  rgCrlEncoded;
#ifdef CMSG_SIGNED_ENCODE_INFO_HAS_CMS_FIELDS
  DWORD                      cAttrCertEncoded;
  PCERT_BLOB                 rgAttrCertEncoded;
#endif
CMSG_SIGNED_ENCODE_INFO,   *PCMSG_SIGNED_ENCODE_INFO;

cbSizeは、この構造体のサイズをバイト単位で指定します。 cSignersは、rgSignersの要素数を指定します。 rgSignersは、CMSG_SIGNER_ENCODE_INFO構造体のアドレスを指定します。 この構造体は署名者の情報を含んでおり、NULLを指定した場合は署名は行われません。 cCertEncodedは、rgCertEncodedの要素数を指定します。 rgCertEncodedは、証明書のバイトデータの配列を指定します。 cCrlEncodedは、rgCrlEncodedの要素数を指定します。 rgCrlEncodedは、CRLの配列を指定します。 cAttrCertEncodedは、rgAttrCertEncodedを指定します。 rgAttrCertEncodedは、証明書の属性を格納したデータの配列を指定します。

今回のプログラムは、データをSignedData型にエンコードとしてファイルに保存します。

#include <windows.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	CMSG_SIGNER_ENCODE_INFO    signerEncodeInfo;
	CMSG_SIGNED_ENCODE_INFO    signedEncodeInfo;
	CRYPT_ALGORITHM_IDENTIFIER hashAlgorithm;
	CERT_BLOB                  signerCertBlob;
	HCERTSTORE                 hStore;
	PCCERT_CONTEXT             pSignerContext;
	HCRYPTPROV                 hProv;
	DWORD                      dwKeySpec;
	HCRYPTMSG                  hMsg;
	LPCBYTE                    lpData = (LPBYTE)TEXT("sample-data");
	DWORD                      dwDataSize = sizeof(TEXT("sample-data"));
	LPBYTE                     lpSignedBlob;
	DWORD                      dwSignedBlobSize;
	HANDLE                     hFile;
	DWORD                      dwWriteByte;
	
	hStore = CertOpenSystemStore(0, TEXT("MY"));
	if (hStore == NULL)
		return 0;
	
	pSignerContext = CertFindCertificateInStore(hStore, X509_ASN_ENCODING, 0, CERT_FIND_SUBJECT_STR, L"MyCert Publisher", NULL);
	if (pSignerContext == NULL) {
		MessageBox(NULL, TEXT("証明書の取得に失敗しました。"), NULL, MB_ICONWARNING);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);	
		return 0;
	}

	ZeroMemory(&hashAlgorithm, sizeof(CRYPT_ALGORITHM_IDENTIFIER));
	hashAlgorithm.pszObjId = szOID_RSA_MD5;

	CryptAcquireCertificatePrivateKey(pSignerContext, 0, NULL, &hProv, &dwKeySpec, NULL);
	ZeroMemory(&signerEncodeInfo, sizeof(CMSG_SIGNER_ENCODE_INFO));
	signerEncodeInfo.cbSize        = sizeof(CMSG_SIGNER_ENCODE_INFO);
	signerEncodeInfo.pCertInfo     = pSignerContext->pCertInfo;
	signerEncodeInfo.hCryptProv    = hProv;
	signerEncodeInfo.dwKeySpec     = dwKeySpec;
	signerEncodeInfo.HashAlgorithm = hashAlgorithm;

	signerCertBlob.cbData = pSignerContext->cbCertEncoded;
	signerCertBlob.pbData = pSignerContext->pbCertEncoded;

	ZeroMemory(&signedEncodeInfo, sizeof(CMSG_SIGNED_ENCODE_INFO));
	signedEncodeInfo.cbSize        = sizeof(CMSG_SIGNED_ENCODE_INFO);
	signedEncodeInfo.cSigners      = 1;
	signedEncodeInfo.rgSigners     = &signerEncodeInfo;
	signedEncodeInfo.cCertEncoded  = 1;
	signedEncodeInfo.rgCertEncoded = &signerCertBlob;

	hMsg = CryptMsgOpenToEncode(PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, 0, CMSG_SIGNED, &signedEncodeInfo, NULL, NULL);
	if (hMsg == NULL) {
		CertFreeCertificateContext(pSignerContext);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return 0;
	}

	if (!CryptMsgUpdate(hMsg, lpData, dwDataSize, TRUE)) {
		MessageBox(NULL, TEXT("署名に失敗しました。"), NULL, MB_ICONWARNING);
		CryptMsgClose(hMsg);
		CertFreeCertificateContext(pSignerContext);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return 0;
	}
	
	MessageBox(NULL, TEXT("署名に成功しました。"), TEXT("OK"), MB_OK);

	CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, NULL, &dwSignedBlobSize);
	lpSignedBlob = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignedBlobSize);
	CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, lpSignedBlob, &dwSignedBlobSize);

	hFile = CreateFile(TEXT("sign.dat"), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	WriteFile(hFile, lpSignedBlob, dwSignedBlobSize, &dwWriteByte, NULL);
	CloseHandle(hFile);
	
	HeapFree(GetProcessHeap(), 0, lpSignedBlob);
	CryptMsgClose(hMsg);
	CertFreeCertificateContext(pSignerContext);
	CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);

	return 0;
}

CryptMsgOpenToEncodeでデータをSignedData型にエンコードする場合は、 CMSG_SIGNED_ENCODE_INFO構造体を初期化することになります。 今回は、SignedData型へのエンコードと共に署名も行うということで、 署名者の情報を表すCMSG_SIGNER_ENCODE_INFO構造体を初期化することにします。

pSignerContext = CertEnumCertificatesInStore(hStore, NULL);
if (pSignerContext == NULL) {
	CertCloseStore(hStore, 0);
	return 0;
}

ZeroMemory(&hashAlgorithm, sizeof(CRYPT_ALGORITHM_IDENTIFIER));
hashAlgorithm.pszObjId = szOID_RSA_MD5;

CryptAcquireCertificatePrivateKey(pSignerContext, 0, NULL, &hProv, &dwKeySpec, NULL);
ZeroMemory(&signerEncodeInfo, sizeof(CMSG_SIGNER_ENCODE_INFO));
signerEncodeInfo.cbSize        = sizeof(CMSG_SIGNER_ENCODE_INFO);
signerEncodeInfo.pCertInfo     = pSignerContext->pCertInfo;
signerEncodeInfo.hCryptProv    = hProv;
signerEncodeInfo.dwKeySpec     = dwKeySpec;
signerEncodeInfo.HashAlgorithm = hashAlgorithm;

CMSG_SIGNER_ENCODE_INFO構造体を初期化するためには、 署名者の証明書とハッシュアルゴリズムが必要です。 署名者の証明書は、MY証明書ストアの中で最初に見つかったものを使用し、 ハッシュアルゴリズムはszOID_RSA_MD5とします。 この他にCMSG_SIGNER_ENCODE_INFO構造体は、署名に使う秘密鍵を維持するCSPと鍵の用途を要求しますが、 これらの情報は証明書に関連付けられているはずなので、CryptAcquireCertificatePrivateKeyで取得することができます。 続いて、CMSG_SIGNED_ENCODE_INFO構造体の初期化について見ていきます。

signerCertBlob.cbData = pSignerContext->cbCertEncoded;
signerCertBlob.pbData = pSignerContext->pbCertEncoded;

ZeroMemory(&signedEncodeInfo, sizeof(CMSG_SIGNED_ENCODE_INFO));
signedEncodeInfo.cbSize        = sizeof(CMSG_SIGNED_ENCODE_INFO);
signedEncodeInfo.cSigners      = 1;
signedEncodeInfo.rgSigners     = &signerEncodeInfo;
signedEncodeInfo.cCertEncoded  = 1;
signedEncodeInfo.rgCertEncoded = &signerCertBlob;

CMSG_SIGNED_ENCODE_INFO構造体には、先に初期化したCMSG_SIGNER_ENCODE_INFO構造体と エンコードされたデータに追加したい証明書をCERT_BLOB構造体の形で指定します。 署名者の証明書を追加したいので、そのサイズとバイトデータをsignerCertBlobに指定しています。

CMSG_SIGNED_ENCODE_INFO構造体の初期化が終われば、 後はこの構造体とCMSG_SIGNED定数をCryptMsgOpenToEncodeに指定し、 メッセージのハンドルを取得することができます。 このメッセージには、SignedData型にエンコードするための情報が含まれているため、 CryptMsgUpdateの呼び出しが成功した場合は、 第2引数のデータがSignedData型にエンコードされてメッセージのコンテンツとして格納されています。 メッセージのコンテンツは、CMSG_CONTENT_PARAMを指定したCryptMsgGetParamで取得できます。 なお、署名データを含まない場合は、CryptMsgUpdateの呼び出しは必要ありません。

CryptMsgControlによる署名の検証

署名が含まれているSignedData型のデータをデコードし、 実際に署名を検証するコードを次に示します。

#include <windows.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	LPBYTE         lpData;
	DWORD          dwDataSize;
	BOOL           bResult;
	LPBYTE         lpSignedBlob;
	DWORD          dwSignedBlobSize;
	HANDLE         hFile;
	DWORD          dwReadByte;
	HCRYPTMSG      hMsg;
	PCERT_INFO     pSignerInfo;
	PCCERT_CONTEXT pSignerContext;
	HCERTSTORE     hMsgStore;
	
	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);

	hMsg = CryptMsgOpenToDecode(PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, 0, 0, 0, NULL, NULL);
	if (hMsg == NULL) {
		HeapFree(GetProcessHeap(), 0, lpSignedBlob);
		return 0;
	}

	if (!CryptMsgUpdate(hMsg, lpSignedBlob, dwSignedBlobSize, TRUE)) {
		HeapFree(GetProcessHeap(), 0, lpSignedBlob);
		CryptMsgClose(hMsg);
		return 0;
	}

	CryptMsgGetParam(hMsg, CMSG_SIGNER_CERT_INFO_PARAM, 0, NULL, &dwDataSize);
	pSignerInfo = (PCERT_INFO)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	CryptMsgGetParam(hMsg, CMSG_SIGNER_CERT_INFO_PARAM, 0, pSignerInfo, &dwDataSize);

	hMsgStore = CertOpenStore(CERT_STORE_PROV_MSG, PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, 0, 0, hMsg);
	pSignerContext = CertGetSubjectCertificateFromStore(hMsgStore, PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, pSignerInfo);

	bResult = CryptMsgControl(hMsg, 0, CMSG_CTRL_VERIFY_SIGNATURE, pSignerContext->pCertInfo);
	if (!bResult) {
		MessageBox(NULL, TEXT("検証に失敗しました。"), NULL, MB_ICONWARNING);
		HeapFree(GetProcessHeap(), 0, lpSignedBlob);
		HeapFree(GetProcessHeap(), 0, pSignerInfo);
		CryptMsgClose(hMsg);
		CertFreeCertificateContext(pSignerContext);
		CertCloseStore(hMsgStore, 0);
		return 0;
	}
	
	CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, NULL, &dwDataSize);
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, lpData, &dwDataSize);

	MessageBox(NULL, (LPTSTR)lpData, TEXT("OK"), MB_OK);
	
	HeapFree(GetProcessHeap(), 0, lpSignedBlob);
	HeapFree(GetProcessHeap(), 0, pSignerInfo);
	CryptMsgClose(hMsg);
	CertFreeCertificateContext(pSignerContext);
	CertCloseStore(hMsgStore, CERT_CLOSE_STORE_CHECK_FLAG);

	return 0;
}

ファイルからSignedData型のデータを取得し、それをCryptMsgUpdateに指定します。 hMsgがCryptMsgOpenToDecodeで得られたものであるならば、 メッセージのコンテンツはデコードされたデータが格納されています。 このデータは、CMSG_CONTENT_PARAMを指定したCryptMsgGetParamで取得できますが、 その前にデータが改ざんされていないかの検証が必要です。 この検証は、CMSG_CTRL_VERIFY_SIGNATUREを指定したCryptMsgControlで行うことができますが、 事前に検証に使うためのCERT_INFO構造体を取得しておく必要があります。 この取得部分は、次のようになっています。

CryptMsgGetParam(hMsg, CMSG_SIGNER_CERT_INFO_PARAM, 0, NULL, &dwDataSize);
pSignerInfo = (PCERT_INFO)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
CryptMsgGetParam(hMsg, CMSG_SIGNER_CERT_INFO_PARAM, 0, pSignerInfo, &dwDataSize);

hMsgStore = CertOpenStore(CERT_STORE_PROV_MSG, PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, 0, 0, hMsg);
pSignerContext = CertGetSubjectCertificateFromStore(hMsgStore, PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, pSignerInfo);

CryptMsgGetParamにCMSG_SIGNER_CERT_INFO_PARAMを指定した場合、 署名者の情報をCERT_INFO構造体で取得することができます。 しかし、この構造体の中で有効になっているメンバは、 IssuerとSerialNumberだけであり、CryptMsgControlに指定する情報としては不十分です。 この2つのメンバを初期化できるのは、 これらの情報がSignedData型に明示的に含まれているためであり、 証明書自体はDERエンコードされたバイトデータとして存在するため、 このバイトデータをIssuerとSerialNumberを基に取得する必要があります。 CertGetSubjectCertificateFromStoreは正にそのための関数であり、 第3引数に指定されたCERT_INFO構造体のIssuerとSerialNumberと 同一の値を持つ証明書コンテキストを返すことができます。 検索の対象とする証明書ストアは、メッセージ内のコンテンツでなければならないため、 CertOpenStoreでメッセージのハンドルをを証明書ストアと見立ておきます。 取得した証明書コンテキストは、バイトデータを基に作成されたものであるため、 証明書コンテキストに含まれるCERT_INFO構造体は適切に初期化されています。



戻る