EternalWindows
ファイル署名 / 署名の構造

前節で述べたように、署名されたファイルにはSignedData型のデータが格納されています。 このため、ファイルに署名するとは、SignedData型のデータを作成し、 それをSIP関数に指定してファイルに格納するということになります。 SignedData型の作成には、CryptSignMessageとCryptMsgOpenToEncodeのいずれかの関数を 呼び出すことができますが、今回のようにファイルに署名を格納する場合は、 CryptMsgOpenToEncodeを呼ぶことになります。 この理由については、次節で説明します。

さて、SignedData型のデータには、署名とコンテンツ(署名対象となったデータ)などが格納されていますが、 このコンテンツは一体何になるのでしょうか。 たとえば、"sample"というデータに署名をするのならば、署名対象のデータは"sample"となりますが、 ファイルに対して署名する場合はどのようなデータになるのでしょうか。 結論から述べると、これはspcIndirectDataContextと呼ばれるデータになります。 このデータは、署名がAuthenticode署名であることを示す指標でもあります。 次に示すSIP_INDIRECT_DATA構造体は、このデータの内容を定義しています。

typedef struct SIP_INDIRECT_DATA_ {
  CRYPT_ATTRIBUTE_TYPE_VALUE Data;
  CRYPT_ALGORITHM_IDENTIFIER DigestAlgorithm;
  CRYPT_HASH_BLOB Digest;
} SIP_INDIRECT_DATA,  *PSIP_INDIRECT_DATA;

Dataは、署名対象のファイルを表すOIDとデータを含むCRYPT_ATTRIBUTE_TYPE_VALUE構造体が格納されます。 ファイルを表すOIDについては、たとえばPEファイルならばSPC_PE_IMAGE_DATA_OBJIDとなり、 データはSPC_PE_IMAGE_DATA構造体となります。 DigestAlgorithmは、Digestを算出するために使用したハッシュアルゴリズムが格納されます。 Digestは、署名対象のファイルから生成したハッシュ値が格納されます。 この構造体を適切にエンコードした場合、それはspcIndirectDataContextデータとなります。

上記の構造体のメンバから分かるように、spcIndirectDataContextデータを作成するためには、 署名対象のファイルのハッシュ値を求めなければなりません。 このハッシュ値というのは、たとえば.exeのようなPEファイルであれば、 まずチェックサムの部分を除外し、さらに既存の署名が格納されている場合は、 その領域(IMAGE_DIRECTORY_ENTRY_SECURITY)も除外し、残ったデータから ハッシュ値を生成することになります。 この作業を踏まえると、アプリケーションは署名対象のファイルフォーマットを熟知していなければ ならないように思えますが、実際にはそのような必要はありません。 SIPは、SignedData型のデータをファイルの適切な位置に格納できるわけですから、 ファイルフォーマットの知識は十分に持っており、 SIP_INDIRECT_DATA構造体そのものを初期化する関数を提供しているのです。 その関数は、次に示すCryptSIPCreateIndirectDataになります。

BOOL WINAPI CryptSIPCreateIndirectData(
  SIP_SUBJECTINFO *pSubjectInfo,
  DWORD *pcbIndirectData,
  SIP_INDIRECT_DATA *pIndirectData
);

pSubjectInfoは、初期化済みのSIP_SUBJECTINFO構造体のアドレスを指定します。 pcbIndirectDataは、pIndirectDataに指定するバッファのサイズを格納した変数のアドレスを指定します。 pIndirectDataにNULLを指定した場合、必要なサイズが返ることになります。 pIndirectDataは、SIP_INDIRECT_DATA構造体を受け取るバッファを指定します。

今回のプログラムは、CryptSIPCreateIndirectDataの使用例を示しています。

#include <windows.h>
#include <mssip.h>
#include <wintrust.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{	
	WCHAR              szFileName[] = L"sample.exe";
	GUID               guidSubject;
	SIP_SUBJECTINFO    subejectInfo;
	PSIP_INDIRECT_DATA pIndirectData;
	LPBYTE             lpContentData;
	DWORD              dwDataSize;
	TCHAR              szBuf[1024];
	DWORD              dwBufferSize;

	if (!CryptSIPRetrieveSubjectGuidForCatalogFile(szFileName, NULL, &guidSubject))
		return 0;

	ZeroMemory(&subejectInfo, sizeof(SIP_SUBJECTINFO));
	subejectInfo.cbSize        = sizeof(SIP_SUBJECTINFO);
	subejectInfo.hFile         = INVALID_HANDLE_VALUE;
	subejectInfo.pwsFileName   = szFileName;
	subejectInfo.pgSubjectType = &guidSubject;

	ZeroMemory(&subejectInfo.DigestAlgorithm, sizeof(CRYPT_ALGORITHM_IDENTIFIER));
	subejectInfo.DigestAlgorithm.pszObjId = szOID_OIWSEC_sha;

	CryptSIPCreateIndirectData(&subejectInfo, &dwDataSize, NULL);
	pIndirectData = (PSIP_INDIRECT_DATA)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	if (!CryptSIPCreateIndirectData(&subejectInfo, &dwDataSize, pIndirectData)) {
		HeapFree(GetProcessHeap(), 0, pIndirectData);
		return 0;
	}

	CryptEncodeObject(PKCS_7_ASN_ENCODING, SPC_INDIRECT_DATA_OBJID, pIndirectData, NULL, &dwDataSize);
	lpContentData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
	CryptEncodeObject(PKCS_7_ASN_ENCODING, SPC_INDIRECT_DATA_OBJID, pIndirectData, lpContentData, &dwDataSize);
	
	dwBufferSize = sizeof(szBuf);
	CryptBinaryToString(lpContentData, dwDataSize, CRYPT_STRING_HEXASCII, szBuf, &dwBufferSize);
	MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);

	HeapFree(GetProcessHeap(), 0, pIndirectData);
	HeapFree(GetProcessHeap(), 0, lpContentData);

	return 0;
}

CryptSIPCreateIndirectDataを呼び出すためには、SIP_SUBJECTINFO構造体から初期化しなければなりません。 CryptSIPGetSignedDataMsgの呼び出しと同じように、ファイル名やサブジェクトGUIDを指定することになりますが、 忘れてはならないのが、ハッシュアルゴリズムを表すOIDの指定です。

ZeroMemory(&lpSubejectInfo->DigestAlgorithm, sizeof(CRYPT_ALGORITHM_IDENTIFIER));
lpSubejectInfo->DigestAlgorithm.pszObjId = szOID_OIWSEC_sha;

ここで指定したOIDは、SIP_INDIRECT_DATA.DigestAlgorithm.pszObjIdに設定され、 SIP_INDIRECT_DATA.Digestの初期化に利用されることになります。 次に、CryptSIPCreateIndirectDataの呼び出しを確認します。

CryptSIPCreateIndirectData(lpSubejectInfo, &dwDataSize, NULL);
pIndirectData = (SIP_INDIRECT_DATA *)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
if (!CryptSIPCreateIndirectData(lpSubejectInfo, &dwDataSize, pIndirectData)) {
	HeapFree(GetProcessHeap(), 0, lpSubejectInfo);
	HeapFree(GetProcessHeap(), 0, pIndirectData);
	return 0;
}

1回目の呼び出しでは、OIDやハッシュ値の長さを含めたSIP_INDIRECT_DATA構造体のサイズが分からないので、 第3引数にNULLを指定してサイズを取得することに専念し、 2回目の呼び出しでデータを取得することになります。 SIP_SUBJECTINFO構造体が正しく初期化されていない場合は関数が失敗することになりますが、 その確認は1回目の呼び出しでは行わないようにしています。 これは、CryptSIPCreateIndirectDataの第3引数にNULLを指定すると、 SIP_SUBJECTINFO構造体の内容を問わず関数が失敗するからです。

CryptSIPCreateIndirectDataで得られるデータは、OIDが文字列として格納されているなどの関係上、 バイナリデータとしてファイルに格納するには適していません。 このため、実際にファイルに格納する場合は、適切にエンコードしていなければなりません。

CryptEncodeObject(PKCS_7_ASN_ENCODING, SPC_INDIRECT_DATA_OBJID, pIndirectData, NULL, &dwDataSize);
lpContentData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwDataSize);
CryptEncodeObject(PKCS_7_ASN_ENCODING, SPC_INDIRECT_DATA_OBJID, pIndirectData, lpContentData, &dwDataSize);	

spcIndirectDataContextデータをエンコードするには、SPC_INDIRECT_DATA_OBJIDまたは、 SPC_INDIRECT_DATA_CONTENT_STRUCTをCryptEncodeObjectを指定することになります。 これらの定数は、wintrust.hに定義されています。 ここで返される値は、正規のspcIndirectDataContextデータそのものであり、 実際にSignedData型のコンテンツとして格納できるため、lpContentDataという名前で識別することにしています。 この変数の具体的な使い方は次節で説明し、今回はバイナリ表示を行うだけにしています。

署名の検証

署名されたファイルが後になって変更されていないかを検証するには、 既にファイルに格納されているspcIndirectDataContextデータと、 新しく作成したspcIndirectDataContextデータを比較します。 spcIndirectDataContextデータはファイルのハッシュ値を含みますから、 その値が一致しない場合は、ファイルが変更されていることになります。

#include <windows.h>
#include <mssip.h>
#include <wintrust.h>

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

BOOL GetSpcIndirectDataContext(LPBYTE lpSignedDataMsg, DWORD dwSignedDataMsgSize, LPBYTE *lplpContentData, LPDWORD lpdwDataSize);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{	
	WCHAR              szFileName[] = L"sample.exe";
	GUID               guidSubject;
	SIP_SUBJECTINFO    subejectInfo;
	DWORD              dwEncodingType;
	LPBYTE             lpSignedDataMsg;
	DWORD              dwSignedDataMsgSize;
	LPBYTE             lpContentData;
	DWORD              dwDataSize;
	PSIP_INDIRECT_DATA pIndirectData;
	DWORD              dwIndirectDataSize;

	if (!CryptSIPRetrieveSubjectGuidForCatalogFile(szFileName, NULL, &guidSubject))
		return 0;
	
	ZeroMemory(&subejectInfo, sizeof(SIP_SUBJECTINFO));
	subejectInfo.cbSize        = sizeof(SIP_SUBJECTINFO);
	subejectInfo.pgSubjectType = &guidSubject;
	subejectInfo.hFile         = INVALID_HANDLE_VALUE;
	subejectInfo.pwsFileName   = szFileName;

	if (!CryptSIPGetSignedDataMsg(&subejectInfo, &dwEncodingType, 0, &dwSignedDataMsgSize, NULL))
		dwSignedDataMsgSize = 10 * 1024;
	lpSignedDataMsg = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSignedDataMsgSize);

	if (!CryptSIPGetSignedDataMsg(&subejectInfo, &dwEncodingType, 0, &dwSignedDataMsgSize, lpSignedDataMsg)) {
		MessageBox(NULL, TEXT("署名の取得に失敗しました。"), NULL, MB_ICONWARNING);
		HeapFree(GetProcessHeap(), 0, lpSignedDataMsg);
		return 0;
	}

	if (!GetSpcIndirectDataContext(lpSignedDataMsg, dwSignedDataMsgSize, &lpContentData, &dwDataSize)) {
		HeapFree(GetProcessHeap(), 0, lpSignedDataMsg);
		return 0;
	}
	
	CryptDecodeObject(PKCS_7_ASN_ENCODING, SPC_INDIRECT_DATA_OBJID, lpContentData, dwDataSize, 0, NULL, &dwIndirectDataSize);
	pIndirectData = (PSIP_INDIRECT_DATA)HeapAlloc(GetProcessHeap(), 0, dwIndirectDataSize);
	CryptDecodeObject(PKCS_7_ASN_ENCODING, SPC_INDIRECT_DATA_OBJID, lpContentData, dwDataSize, 0, pIndirectData, &dwIndirectDataSize);

	if (CryptSIPVerifyIndirectData(&subejectInfo, pIndirectData))
		MessageBox(NULL, TEXT("検証に成功しました。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("検証に失敗しました。"), NULL, MB_ICONWARNING);

	HeapFree(GetProcessHeap(), 0, lpSignedDataMsg);
	HeapFree(GetProcessHeap(), 0, lpContentData);
	HeapFree(GetProcessHeap(), 0, pIndirectData);

	return 0;
}

BOOL GetSpcIndirectDataContext(LPBYTE lpSignedDataMsg, DWORD dwSignedDataMsgSize, LPBYTE *lplpContentData, LPDWORD lpdwDataSize)
{
	HCRYPTMSG hMsg;
	
	hMsg = CryptMsgOpenToDecode(PKCS_7_ASN_ENCODING | X509_ASN_ENCODING, 0, 0, 0, NULL, NULL);
	if (hMsg == NULL)
		return FALSE;
	
	if (!CryptMsgUpdate(hMsg, lpSignedDataMsg, dwSignedDataMsgSize, TRUE)) {
		CryptMsgClose(hMsg);
		return FALSE;
	}
	
	CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, NULL, lpdwDataSize);
	*lplpContentData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, *lpdwDataSize);
	CryptMsgGetParam(hMsg, CMSG_CONTENT_PARAM, 0, *lplpContentData, lpdwDataSize);

	CryptMsgClose(hMsg);

	return TRUE;
}

CryptSIPVerifyIndirectDataという関数は、第1引数に指定したSIP_SUBJECTINFO構造体を基に 内部でspcIndirectDataContextデータを生成し、 それに第2引数に指定されたspcIndirectDataContextデータと比較します。 この第2引数は、アプリケーションがファイルから明示的に取得する必要があるため、 GetSpcIndirectDataContextという自作関数でそれを行っています。 spcIndirectDataContextデータは、SignedData型のコンテンツとして格納されているため、 CMSG_CONTENT_PARAMを指定したCryptMsgGetParamで取得することができます。 実際にCryptSIPVerifyIndirectDataの前に、SIP_INDIRECT_DATA構造体の形にデコードしておく必要があります。 なお、多くのアプリケーションは、署名の検証をWinVerifyTrustで行いますが、 内部的には上記と似た処理が行われていると思われます。



戻る