EternalWindows
PKCS #7 / SignedData型と署名

今回から、Simplified Message Functionsと呼ばれる関数群を取り上げます。 これらの関数は、データをPKCS #7という標準化された形式にエンコードするため、 そのデータは多くのアプリケーションによって認識されることになります。 PKCS #7形式のデータは、主にSignedData型とEnvelopedData型という2種類が存在し、 前者が署名データのエンコード形式、後者が暗号化データのエンコード形式となります。 もし、アプリケーションが独自にエンコードした形式が PKCS# 7形式と類似する情報を含むようであれば、 PKCS# 7形式によるエンコードを検討するとよいでしょう。

SignedData型に含まれる情報は主に、署名対象のデータ、署名、ID(SerialNumberとIssuer)、そして証明書です。 ただし、これら全てのデータが必ずしも存在するとは限りません。 署名というのは、データのハッシュ値を秘密鍵で暗号化したものであり、 この秘密鍵は証明書から参照されることになります。 つまり、署名者の証明書には秘密鍵が関連付けられている必要があります。 Simplified Message Functionsの中で署名を行う関数は、CryptSignMessageです。

BOOL WINAPI CryptSignMessage(
  PCRYPT_SIGN_MESSAGE_PARA pSignPara,
  BOOL fDetachedSignature,
  DWORD cToBeSigned,
  const BYTE *rgpbToBeSigned[ ],
  DWORD rgcbToBeSigned[ ],
  BYTE *pbSignedBlob,
  DWORD *pcbSignedBlob
);

pSignParaは、署名情報を含むCRYPT_SIGN_MESSAGE_PARA構造体のアドレスを指定します。 fDetachedSignatureは、署名されたハッシュ値のみを取得したい場合はTRUEを指定します。 PKCS#7形式で署名データを取得したい場合は、FALSEを指定します。 cToBeSignedは、rgpbToBeSignedの要素数を指定します。 rgpbToBeSignedは、署名したいデータの配列を指定します。 rgcbToBeSignedは、署名したいデータのサイズを格納した配列を指定します。 pbSignedBlobは、署名されたデータを受け取るバッファを指定します。 pcbSignedBlobは、バッファのサイズを格納した変数のアドレスを指定します。 pbSignedBlobにNULLを指定した場合、バッファのサイズが返ることになります。

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

typedef struct _CRYPT_SIGN_MESSAGE_PARA {
  DWORD                       cbSize;
  DWORD                       dwMsgEncodingType;
  PCCERT_CONTEXT              pSigningCert;
  CRYPT_ALGORITHM_IDENTIFIER  HashAlgorithm;
  void                        *pvHashAuxInfo;
  DWORD                       cMsgCert;
  PCCERT_CONTEXT              *rgpMsgCert;
  DWORD                       cMsgCrl;
  PCCRL_CONTEXT               *rgpMsgCrl;
  DWORD                       cAuthAttr;
  PCRYPT_ATTRIBUTE            rgAuthAttr;
  DWORD                       cUnauthAttr;
  PCRYPT_ATTRIBUTE            rgUnauthAttr;
  DWORD                       dwFlags;
  DWORD                       dwInnerContentType;
#ifdef CRYPT_SIGN_MESSAGE_PARA_HAS_CMS_FIELDS
  CRYPT_ALGORITHM_IDNETIFIER  HashEncryptionAlgorithm;
  void                        pvHashEncryptionAuxInfo;
#endif
} CRYPT_SIGN_MESSAGE_PARA, *PCRYPT_SIGN_MESSAGE_PARA;

cbSizeは、構造体のサイズをバイト単位で指定します。 dwMsgAndCertEncodingTypeは、エンコーディングタイプを指定します。 pSigningCertは、署名に使用する証明書を指定します。 この証明書には、CSPの情報が関連付けられている必要があります。 HashAlgorithmは、ハッシュアルゴリズムを格納したCRYPT_ALGORITHM_IDENTIFIER構造体のアドレスを指定します。 pvHashAuxInfoは、使用されないためNULLを指定します。 cMsgCertは、rgpMsgCertの要素数を指定します。 rgpMsgCertは、署名データに含みたい証明書の配列を指定します。 cMsgCrlは、rgpMsgCrlの要素数を指定します。 rgpMsgCrlは、署名データに含みたいCRLの配列を指定します。 cAuthAttrは、rgAuthAttrの要素数を指定します。 rgAuthAttrは、認証済み属性の配列を指定します。 cUnauthAttrは、rgUnauthAttrの要素数を指定します。 rgUnauthAttrは、認証されていない属性の配列を指定します。 dwFlagsとdwInnerContentTypeは、基本的に0を指定します。

今回のプログラムは、CryptSignMessageでSignedData型のデータを作成し、それをファイルに保存します。 署名のため、MY証明書ストアに、関連する秘密鍵を持った証明書を用意しておいてください。

#include <windows.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	CRYPT_SIGN_MESSAGE_PARA    signPara;
	CRYPT_ALGORITHM_IDENTIFIER hashAlgorithm;
	HCERTSTORE                 hStore;
	PCCERT_CONTEXT             pSignerContext;
	LPCBYTE                    lpData = (LPBYTE)TEXT("sample-data");
	DWORD                      dwDataSize = sizeof(TEXT("sample-data"));
	BOOL                       bResult;
	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;

	signPara.cbSize             = sizeof(CRYPT_SIGN_MESSAGE_PARA);
	signPara.dwMsgEncodingType  = PKCS_7_ASN_ENCODING; 
	signPara.pSigningCert       = pSignerContext;
	signPara.HashAlgorithm      = hashAlgorithm;
	signPara.pvHashAuxInfo      = NULL;
	signPara.cMsgCert           = 1;
	signPara.rgpMsgCert         = &pSignerContext;
	signPara.cMsgCrl            = 0;
	signPara.rgpMsgCrl          = NULL;
	signPara.cAuthAttr          = 0;
	signPara.rgAuthAttr         = NULL;
	signPara.cUnauthAttr        = 0;
	signPara.rgUnauthAttr       = NULL;
	signPara.dwFlags            = 0;
	signPara.dwInnerContentType = 0;

	bResult = CryptSignMessage(&signPara, FALSE, 1, &lpData, &dwDataSize, NULL, &dwSignedBlobSize);
	if (!bResult) {
		MessageBox(NULL, TEXT("署名に失敗しました。"), NULL, MB_ICONWARNING);
		CertFreeCertificateContext(pSignerContext);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return 0;
	}

	lpSignedBlob = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignedBlobSize);

	bResult = CryptSignMessage(&signPara, FALSE, 1, &lpData, &dwDataSize, lpSignedBlob, &dwSignedBlobSize);
	if (!bResult) {
		MessageBox(NULL, TEXT("署名に失敗しました。"), NULL, MB_ICONWARNING);
		HeapFree(GetProcessHeap(), 0, lpSignedBlob);
		CertFreeCertificateContext(pSignerContext);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return 0;
	}

	MessageBox(NULL, TEXT("署名に成功しました。"), TEXT("OK"), MB_OK);

	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);
	CertFreeCertificateContext(pSignerContext);
	CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);

	return 0;
}

まず、CertOpenSystemStoreでMY証明書ストアをオープンし、 CertFindCertificatesInStoreで証明書を取得します。 次に、ハッシュアルゴリズムを表すCRYPT_ALGORITHM_IDENTIFIER構造体を初期化し、 MDハッシュを表すszOID_RSA_MD5を指定しています。 ここで取得した証明書とハッシュアルゴリズムをCRYPT_SIGN_MESSAGE_PARA構造体に指定します。

CRYPT_SIGN_MESSAGE_PARA構造体には、証明書を受け取るメンバが2つあります。 pSigningCertは、署名に使うための証明書であるため、必ず指定しなければなりませんが、 rgpMsgCertは何のために指定することになるのでしょうか。 答えは、署名を検証するための証明書を含めるためです。 署名を検証するには、秘密鍵に対応した公開鍵を持つ証明書が必要ですから、 pSigningCertが指している証明書を含めておく必要があります。 仮にrgpMsgCertが初期化されていなくても関数は成功するので注意してください。 次に、CryptSignMessageの呼び出しを確認します。

bResult = CryptSignMessage(&signPara, FALSE, 1, &lpData, &dwDataSize, NULL, &dwSignedBlobSize);

第2引数のFALSEは、署名されたハッシュ値をデータに含めることを意味します。 第3引数の1は、署名の対象とするデータが1つであることを意味します。 データが複数の場合は、第4引数と第5引数を共に配列で指定することになると思われますが、 試してみたところ、複数データへの署名はエラーとなるように思えます。 このCryptSignMessageの呼び出しによって、SignedData型のデータのサイズが最終引数に返るため、 その分のメモリを確保し、次のCryptSignMessageの呼び出しでSignedData型のデータを取得できることになります。


戻る