EternalWindows
自己署名入り証明書 / 証明書作成サンプル

これまでで、証明書を作成する手順は一通り示しましたが、 実際に証明書が利用される場合の事を踏まえた場合、まだ考慮すべき問題があります。 それは、証明書とCSPの関連付けです。 証明書に含まれる公開鍵は、元は特定のCSPの鍵コンテナからエクスポートされたものであり、 そのCSPの鍵コンテナには公開鍵の対となる秘密鍵が含まれています。 証明書から、この秘密鍵の所在を解決することができなければ、 アプリケーションは証明書を通じて署名を行うことができないため、 証明書にCSPに関する情報を設定しておく必要があるのです。

上図は、MY証明書ストアに証明書を追加してそれを表示させたものです。 CSP情報が関連付けられた証明書は、「この証明書に対応する秘密キーを持っています。」という メッセージが表示されるため、この証明書が署名に使用できることが分ります。 IEのインターネットオプションから表示できるダイアログでは、 MY証明書ストアに格納された証明書のうち、CSPの情報を含まないものは表示されないので、 この点は注意しておく必要があります。 ところで、上図ではルートCAに関する警告が表示されていますが、 自己署名入り証明書の場合においては、これは当然のことであるといえます。 証明書をエクスポートして、ルートCAにインポートすれば警告は解消されますが、 あくまでローカルコンピュータ上における回避策となります。

CERT_INFO構造体の定義からも想像できることですが、 証明書の形式を規格するX.509では、 証明書にCSPの情報を格納するフィールドは存在していません。 これでは、証明書にCSPの情報を格納できないように思えますが、 Windowsでは証明書に独自の情報を含めるための拡張プロパティを設定することができます。 この拡張プロパティは、Windowsのみで解釈される情報ですが、 そこにはCSPの情報だけなく、発行者のフレンドリ名などを含めることができるので、 多くの証明書で拡張プロパティは利用されています。 証明書に拡張プロパティを設定するには、CertSetCertificateContextPropertyを呼び出します。

BOOL WINAPI CertSetCertificateContextProperty(
  PCCERT_CONTEXT pCertContext, 
  DWORD dwPropId, 
  DWORD dwFlags, 
  const void *pvData 
);

pCertContextは、拡張プロパティを設定したい証明書コンテキストを指定します。 dwPropIdは、設定する拡張プロパティの種類を表す定数を指定します。 この種類は非常に多いため、必要に応じて取り上げます。 dwFlagsは、基本的に0を指定します。 pvDataは、dwPropIdに関連する構造体のアドレスを指定します。

今回のプログラムは、これまで示してきたコードを1つにまとめたもので、 実際に証明書を作成するプログラムとして機能します。 また、証明書を証明書ストアに追加する際には、拡張プロパティを設定するようにしています。

#include <windows.h>

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

BOOL CreateCertificate(HCRYPTPROV hProv, DWORD dwKeySpec, LPTSTR lpszSubjectName, LPBYTE *lplpCertData, LPDWORD lpdwCertSize);
BOOL AddCertificateToStore(PCRYPT_KEY_PROV_INFO pCryptKeyProvInfo, LPBYTE lpCertData, DWORD dwCertSize);
BOOL SaveCertificate(LPTSTR lpszFileName, LPBYTE lpCertData, DWORD dwCertSize);
BOOL GetCryptProvider(HCRYPTPROV *hProv, LPTSTR lpszContainerName, LPTSTR lpszProviderName, DWORD dwProvType, DWORD dwKeySpec);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WCHAR               szContainerName[] = L"MyContainer";
	LPWSTR              lpszProviderName = MS_DEF_PROV_W;
	DWORD               dwProvType = PROV_RSA_FULL;
	DWORD               dwKeySpec = AT_KEYEXCHANGE;
	HCRYPTPROV          hProv;
	TCHAR               szSubjectName[] = TEXT("CN=MyCert Publisher");
	BOOL                bSaveStore = TRUE;
	LPBYTE              lpCertData;
	DWORD               dwCertSize;
	CRYPT_KEY_PROV_INFO cryptKeyProvInfo;

	if (!GetCryptProvider(&hProv, szContainerName, lpszProviderName, dwProvType, dwKeySpec))
		return 0;

	if (!CreateCertificate(hProv, dwKeySpec, szSubjectName, &lpCertData, &dwCertSize)) {
		MessageBox(NULL, TEXT("証明書データの作成に失敗しました。"), NULL, MB_ICONWARNING);
		CryptReleaseContext(hProv, 0);
		return 0;
	}
	
	if (bSaveStore) {
		ZeroMemory(&cryptKeyProvInfo, sizeof(cryptKeyProvInfo));
		cryptKeyProvInfo.pwszContainerName = szContainerName;
		cryptKeyProvInfo.pwszProvName      = lpszProviderName;
		cryptKeyProvInfo.dwProvType        = dwProvType;
		cryptKeyProvInfo.dwKeySpec         = dwKeySpec;

		if (AddCertificateToStore(&cryptKeyProvInfo, lpCertData, dwCertSize))
			MessageBox(NULL, TEXT("証明書を証明書ストアに追加しました。"), TEXT("OK"), MB_OK);
		else
			MessageBox(NULL, TEXT("証明書を証明書ストアに追加できませんでした。"), NULL, MB_ICONWARNING);
	}
	else {
		SaveCertificate(TEXT("sample.cer"), lpCertData, dwCertSize);
		MessageBox(NULL, TEXT("証明書をファイルに保存しました。"), TEXT("OK"), MB_OK);
	}

	HeapFree(GetProcessHeap(), 0, lpCertData);
	CryptReleaseContext(hProv, 0);

	return 0;
}

BOOL CreateCertificate(HCRYPTPROV hProv, DWORD dwKeySpec, LPTSTR lpszSubjectName, LPBYTE *lplpCertData, LPDWORD lpdwCertSize)
{
	CERT_INFO             certInfo;
	BYTE                  serialNumber[4];
	SYSTEMTIME            systemTime;
	FILETIME              fileTime;
	PCERT_PUBLIC_KEY_INFO pPublicKeyInfo;
	DWORD                 dwSize;
	LPBYTE                lpData;

	ZeroMemory(&certInfo, sizeof(CERT_INFO));

	certInfo.dwVersion = CERT_V3;
	
	CryptGenRandom(hProv, sizeof(serialNumber), serialNumber);
	serialNumber[sizeof(serialNumber) - 1] &= 0x7f;
	certInfo.SerialNumber.cbData = sizeof(serialNumber);
	certInfo.SerialNumber.pbData = serialNumber;

	if (!CertStrToName(X509_ASN_ENCODING, lpszSubjectName, CERT_X500_NAME_STR, NULL, NULL, &dwSize, NULL))
		return FALSE;

	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
	CertStrToName(X509_ASN_ENCODING, lpszSubjectName, CERT_X500_NAME_STR, NULL, lpData, &dwSize, NULL);
	certInfo.Subject.cbData = dwSize;
	certInfo.Subject.pbData = lpData;
	certInfo.Issuer = certInfo.Subject;

	GetSystemTime(&systemTime);
	SystemTimeToFileTime(&systemTime, &fileTime);
	certInfo.NotBefore = fileTime;
	systemTime.wYear += 1;
	SystemTimeToFileTime(&systemTime, &fileTime);
	certInfo.NotAfter = fileTime;
	
	if (!CryptExportPublicKeyInfo(hProv, dwKeySpec, X509_ASN_ENCODING, NULL, &dwSize)) {
		HeapFree(GetProcessHeap(), 0, lpData);
		return FALSE;
	}
	pPublicKeyInfo = (PCERT_PUBLIC_KEY_INFO)HeapAlloc(GetProcessHeap(), 0, dwSize);
	CryptExportPublicKeyInfo(hProv, dwKeySpec, X509_ASN_ENCODING, pPublicKeyInfo, &dwSize);
	certInfo.SubjectPublicKeyInfo = *pPublicKeyInfo;
	
	certInfo.SignatureAlgorithm.pszObjId = szOID_RSA_SHA1RSA;
	CryptSignAndEncodeCertificate(hProv, dwKeySpec, X509_ASN_ENCODING, (LPSTR)X509_CERT_TO_BE_SIGNED, &certInfo, &certInfo.SignatureAlgorithm, NULL, NULL, lpdwCertSize);
	*lplpCertData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, *lpdwCertSize);
	CryptSignAndEncodeCertificate(hProv, dwKeySpec, X509_ASN_ENCODING, (LPSTR)X509_CERT_TO_BE_SIGNED, &certInfo, &certInfo.SignatureAlgorithm, NULL, *lplpCertData, lpdwCertSize);

	HeapFree(GetProcessHeap(), 0, lpData);
	HeapFree(GetProcessHeap(), 0, pPublicKeyInfo);
	
	return TRUE;
}

BOOL AddCertificateToStore(PCRYPT_KEY_PROV_INFO pCryptKeyProvInfo, LPBYTE lpCertData, DWORD dwCertSize)
{
	HCERTSTORE     hStore;
	PCCERT_CONTEXT pContext;

	hStore = CertOpenSystemStore(0, TEXT("MY"));
	if (hStore == NULL)
		return FALSE;

	if (!CertAddEncodedCertificateToStore(hStore, X509_ASN_ENCODING, lpCertData, dwCertSize, CERT_STORE_ADD_REPLACE_EXISTING, &pContext)) {
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return FALSE;
	}

	CertSetCertificateContextProperty(pContext, CERT_KEY_PROV_INFO_PROP_ID, 0, pCryptKeyProvInfo);

	CertFreeCertificateContext(pContext);
	CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);

	return TRUE;
}

BOOL SaveCertificate(LPTSTR lpszFileName, LPBYTE lpCertData, DWORD dwCertSize)
{
	HANDLE hFile;
	DWORD  dwWriteByte;
	
	hFile = CreateFile(lpszFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE)
		return FALSE;

	WriteFile(hFile, lpCertData, dwCertSize, &dwWriteByte, NULL);
	
	CloseHandle(hFile);

	return TRUE;
}

BOOL GetCryptProvider(HCRYPTPROV *phProv, LPTSTR lpszContainerName, LPTSTR lpszProviderName, DWORD dwProvType, DWORD dwKeySpec)
{
	HCRYPTKEY hKey;

	if (!CryptAcquireContext(phProv, lpszContainerName, lpszProviderName, dwProvType, 0)) {
		if (GetLastError() != NTE_BAD_KEYSET) {
			MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			return FALSE;
		}
		if (!CryptAcquireContext(phProv, lpszContainerName, lpszProviderName, dwProvType, CRYPT_NEWKEYSET)) {
			MessageBox(NULL, TEXT("鍵コンテナの作成に失敗しました。"), NULL, MB_ICONWARNING);
			return FALSE;
		}
	}
	
	if (!CryptGetUserKey(*phProv, dwKeySpec, &hKey)) {
		if (GetLastError() != NTE_NO_KEY) {
			MessageBox(NULL, TEXT("鍵ペアのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			CryptReleaseContext(*phProv, 0);
			return FALSE;
		}
		if (!CryptGenKey(*phProv, dwKeySpec, 0, &hKey)) {
			MessageBox(NULL, TEXT("鍵ペアの作成に失敗しました。"), NULL, MB_ICONWARNING);
			CryptReleaseContext(*phProv, 0);
			return FALSE;
		}
	}
	
	CryptDestroyKey(hKey);

	return TRUE;
}

自作関数のGetCryptProviderはCSPのハンドルを取得することが目的ですが、 指定された鍵コンテナが存在していない場合は、それを作成します。 また、鍵コンテナに指定された用途の鍵ペアが存在しない場合は、それも作成しています。 証明書にCSPの情報を関連付けるには、これらが存在していなければなりませんから、 必ず確認するようにします。

プログラムが扱っている3つの自作関数について簡単に説明します。 CreateCertificateは、証明書を作成するために必要な引数を要求し、証明書データを作成します。 この手順は、これまで示したCERT_INFO構造体の必要なメンバを順に初期化していき、 最後にCryptSignAndEncodeCertificateを呼び出して証明書データを作成するというものです。 CreateCertificateの呼び出した後は、bSaveStoreの値によって データを証明書ストアに追加するAddCertificateToStoreか、 データをファイルに保存するSaveCertificateを呼び出すことになります。 bSaveStoreはFALSEで初期化されているので、 デフォルトではSaveCertificateを呼び出すことになります。

CreateCertificateの内部については既に述べてきた通りですが、 この関数がどのような引数を要求しているかは確認しておきます。 まず、CERT_INFO構造体のSubjectとIssuerの初期化には、 発行先及び発行者とする名前が必要になるので、lpszSubjectNameという引数が用意されています。 また、CryptSignAndEncodeCertificateでデータに署名を行う場合は、 CSPのハンドルと署名に利用する鍵ペアの用途が必要になるため、 このような引数を用意することになります。 CreateCertificateは、作成した証明書データを戻り値として返し、 データのサイズは引数として返します。

証明書データを証明書ストアに追加する場合は、AddCertificateToStoreを呼び出すことになります。 今回、説明したように、追加される証明書にはCSPの情報を表す拡張プロパティを設定しておく必要があるため、 そのCSPの情報となる構造体を事前に初期化しておくことになります。

ZeroMemory(&cryptKeyProvInfo, sizeof(cryptKeyProvInfo));
cryptKeyProvInfo.pwszContainerName = szContainerName;
cryptKeyProvInfo.pwszProvName      = lpszProviderName;
cryptKeyProvInfo.dwProvType        = dwProvType;
cryptKeyProvInfo.dwKeySpec         = dwKeySpec;

cryptKeyProvInfoはCRYPT_KEY_PROV_INFO構造体として定義され、 この構造体がCSPの情報を表すことになっています。 初期化については、CryptAcquireContextに指定するようなCSP自体の情報と鍵コンテナの名前、 そして、鍵ペアの用途を指定するだけで特に問題ありません。 少し注意しなければならないのは、この構造体の文字列を要求するメンバが、 UNICODEの型として定義されていることです。 このため、WinMainではszContainerNameとlpszProviderNameを それぞれUNICODEとして定義し、CryptAcquireContextの呼び出しにも 明示的にUNICODE版の関数を指定しています。 次に、AddCertificateToStoreの内部を確認します。

if (!CertAddEncodedCertificateToStore(hStore, X509_ASN_ENCODING, lpCertData, dwCertSize, CERT_STORE_ADD_REPLACE_EXISTING, &pContext)) {
	CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
	return FALSE;
}

CertSetCertificateContextProperty(pContext, CERT_KEY_PROV_INFO_PROP_ID, 0, pCryptKeyProvInfo);

このコードで少し違和感を抱いてしまうのは、CertAddEncodedCertificateToStoreが返した証明書コンテキストに対して、 拡張プロパティを設定しているところです。 この拡張プロパティの設定は、証明書ストアに証明書を追加する前に行っておく処理のように思えますが、 CertAddEncodedCertificateToStoreが返す証明書コンテキストは、 追加された証明書にリンクするような形になっているようです。 つまり、それに対する拡張プロパティの設定が、 証明書ストアに存在する証明書に反映することになります。 CertSetCertificateContextPropertyの第2引数では、CSPの情報を 表すCERT_KEY_PROV_INFO_PROP_IDを指定し、 第4引数にはWinMainで初期化したCRYPT_KEY_PROV_INFO構造体を指定しています。

CAによる署名について

今回のような自己署名入り証明書を作成するコードをよく検討してみると、 それをCAとして機能するコードに簡単に変化させられることが分かります。 たとえば、今回のプログラムのCERT_INFO構造体のIssuerに、 既存の自己署名入り証明書の発行先を指定し、CryptSignAndEncodeCertificateのCSPに関する引数を、 同じく既存の自己署名入り証明書のものにしたらどうなるでしょうか。 作成される証明書は自己署名ではなく、CAの署名が入った証明書ということになるでしょう。 次に、コード例を示します。

BOOL CreateCertificate(HCRYPTPROV hProv, DWORD dwKeySpec, LPTSTR lpszSubjectName, LPBYTE *lplpCertData, LPDWORD lpdwCertSize, LPTSTR lpszIssuerName)
{
	CERT_INFO             certInfo;
	BYTE                  serialNumber[4];
	SYSTEMTIME            systemTime;
	FILETIME              fileTime;
	PCERT_PUBLIC_KEY_INFO pPublicKeyInfo;
	DWORD                 dwSize;
	LPBYTE                lpData;
	HCERTSTORE            hStore;
	PCCERT_CONTEXT        pRootContext;

	ZeroMemory(&certInfo, sizeof(CERT_INFO));

	certInfo.dwVersion = CERT_V3;
	
	CryptGenRandom(hProv, sizeof(serialNumber), serialNumber);
	serialNumber[sizeof(serialNumber) - 1] &= 0x7f;
	certInfo.SerialNumber.cbData = sizeof(serialNumber);
	certInfo.SerialNumber.pbData = serialNumber;

	if (!CertStrToName(X509_ASN_ENCODING, lpszSubjectName, CERT_X500_NAME_STR, NULL, NULL, &dwSize, NULL))
		return FALSE;

	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
	CertStrToName(X509_ASN_ENCODING, lpszSubjectName, CERT_X500_NAME_STR, NULL, lpData, &dwSize, NULL);
	certInfo.Subject.cbData = dwSize;
	certInfo.Subject.pbData = lpData;

	GetSystemTime(&systemTime);
	SystemTimeToFileTime(&systemTime, &fileTime);
	certInfo.NotBefore = fileTime;
	systemTime.wYear += 1;
	SystemTimeToFileTime(&systemTime, &fileTime);
	certInfo.NotAfter = fileTime;
	
	if (!CryptExportPublicKeyInfo(hProv, dwKeySpec, X509_ASN_ENCODING, NULL, &dwSize)) {
		HeapFree(GetProcessHeap(), 0, lpData);
		return FALSE;
	}
	pPublicKeyInfo = (PCERT_PUBLIC_KEY_INFO)HeapAlloc(GetProcessHeap(), 0, dwSize);
	CryptExportPublicKeyInfo(hProv, dwKeySpec, X509_ASN_ENCODING, pPublicKeyInfo, &dwSize);
	certInfo.SubjectPublicKeyInfo = *pPublicKeyInfo;
	CryptReleaseContext(hProv, 0);

	hStore = CertOpenSystemStore(0, TEXT("Root"));
	if (hStore == NULL) {
		HeapFree(GetProcessHeap(), 0, lpData);
		HeapFree(GetProcessHeap(), 0, pPublicKeyInfo);
		return FALSE;
	}
	
	pRootContext = CertFindCertificateInStore(hStore, X509_ASN_ENCODING, 0, CERT_FIND_SUBJECT_STR, lpszIssuerName, NULL);
	if (pRootContext == NULL) {
		HeapFree(GetProcessHeap(), 0, lpData);
		HeapFree(GetProcessHeap(), 0, pPublicKeyInfo);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);	
		return FALSE;
	}
	
	if (!CryptAcquireCertificatePrivateKey(pRootContext, 0, NULL, &hProv, &dwKeySpec, NULL)) {
		HeapFree(GetProcessHeap(), 0, lpData);
		HeapFree(GetProcessHeap(), 0, pPublicKeyInfo);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);	
		return FALSE;
	}

	certInfo.Issuer = pRootContext->pCertInfo->Subject;	
	certInfo.SignatureAlgorithm.pszObjId = szOID_RSA_SHA1RSA;
	CryptSignAndEncodeCertificate(hProv, dwKeySpec, X509_ASN_ENCODING, (LPSTR)X509_CERT_TO_BE_SIGNED, &certInfo, &certInfo.SignatureAlgorithm, NULL, NULL, lpdwCertSize);
	*lplpCertData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, *lpdwCertSize);
	CryptSignAndEncodeCertificate(hProv, dwKeySpec, X509_ASN_ENCODING, (LPSTR)X509_CERT_TO_BE_SIGNED, &certInfo, &certInfo.SignatureAlgorithm, NULL, *lplpCertData, lpdwCertSize);

	HeapFree(GetProcessHeap(), 0, lpData);
	HeapFree(GetProcessHeap(), 0, pPublicKeyInfo);
	CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
	CryptReleaseContext(hProv, 0);

	return TRUE;
}

CAに関わるコードは、CertOpenSystemStoreでRoot証明書ストアをオープンする箇所から始まります。 このCreateCertificateには、lpszIssuerNameという発行先CAの名前を受け取る文字列があるため、 これを基にCertFindCertificateInStoreを呼び出せば、CAの証明書を取得することができます。 この証明書を使って署名を行うためには、証明書に関連付けられているCSP情報が必要になりますから、 CryptAcquireCertificatePrivateKeyを呼び出して、CSPのハンドルと鍵用途を取得します。 後は、CERT_INFO構造体のIssuerをCA証明書の発行者と同一にし、 取得したCSP情報を基にCryptSignAndEncodeCertificateを呼び出せば、 CAによる署名が行われることになります。 これにより、署名された証明書の「証明のパス」には、その上位にCA証明書が存在することになります。

上記からも分るようにCAが署名を行うためには、 そのCA証明書がルート証明書ストアに存在している必要があります。 これにより、そのCA証明書は信頼され、署名された証明書も信頼できることになります。 手順としては、今回のプログラムのszSubjectNameをCAの名前として適切なものに変更し、 AddCertificateToStoreで呼ばれているCertOpenSystemStoreの引数をRootに変更して実行します。 その後、今回のプログラムのCreateCertificateを先に示したものに変更して実行すればよいでしょう。 その際、CreateCertificateに指定するlpszIssuerNameは、 CA証明書の発行先名(CN=は含めない)となります。



戻る