EternalWindows
自己署名入り証明書 / 拡張情報のエンコード

証明書の拡張情報を初期化するとは、CERT_INFO構造体のCERT_EXTENSION構造体を初期化することです。 その具体的な方法は、pszObjIdメンバに初期化したい拡張情報を表すOIDを指定し、 Valueメンバにその拡張情報を表す構造体を指定します。 ただし、この構造体はエンコードされていなければならず、 また、使用する構造体も拡張情報によって異なります。 各種構造体を使用した具体的な手順は次節以降に取り上げることにし、 今回はエンコードを行う方法について説明します。 次に示すCryptEncodeObjectは、指定された構造体をエンコードします。

BOOL WINAPI CryptEncodeObject(
  DWORD dwCertEncodingType, 
  LPCSTR lpszStructType, 
  const void *pvStructInfo, 
  BYTE *pbEncoded, 
  DWORD *pcbEncoded 
);

dwCertEncodingTypeは、エンコーディングタイプを指定します。 lpszStructTypeは、エンコードする構造体を表すOIDを指定します。 pvStructInfoは、エンコードしたい構造体のアドレスを指定します。 pbEncodedは、エンコードされたデータを受け取るバッファを指定します。 pcbEncodedは、バッファのサイズを格納する変数のアドレスを指定します。

次に、CryptEncodeObjectを呼び出して拡張情報を初期化する例を示します。 構造体とOID以外は、どの拡張情報を初期化する場合でも基本的に同一となります。

void AddExtension(PCERT_EXTENSION pCertExtension)
{
	CRYPT_DATA_BLOB dataStruct;
	DWORD           dwSize;
	LPBYTE          lpData;
	
	// dataStruct(拡張情報を表す構造体)を初期化

	CryptEncodeObject(X509_ASN_ENCODING, szOID_XXX, &dataStruct, NULL, &dwSize);
	lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
	CryptEncodeObject(X509_ASN_ENCODING, szOID_XXX, &dataStruct, lpData, &dwSize);

	pCertExtension->pszObjId     = szOID_XXX;
	pCertExtension->fCritical    = FALSE;
	pCertExtension->Value.cbData = dwSize;
	pCertExtension->Value.pbData = lpData;
}

このコードでは、構造体をdataStructとして定義し、それを表すOIDがszOID_XXXであると仮定しています。 手順としては、まず専用の構造体を初期化し、 次にその構造体をCryptEncodeObjectでエンコードします。 このとき、1回目の呼び出しではデータのサイズを受け取るようにし、 2回目の呼び出しで実際のデータを受け取ります。 CERT_EXTENSION構造体の初期化では、pszObjIdメンバにCryptEncodeObjectで指定したOIDを指定し、 Value.cbDataにはエンコードされたデータのサイズ、Value.pbDataはエンコードされたデータを指定します。 fCriticalは、個々の拡張情報によって値が異なるため必要に応じて補足します。

拡張情報を証明書に追加する場合は、その情報をエンコードすることになりますが、 証明書から拡張情報を取得したい場合は、その情報をデコードすることになります。 これには、CryptDecodeObjectを呼び出します。

BOOL WINAPI CryptDecodeObject(
  DWORD dwCertEncodingType, 
  LPCSTR lpszStructType, 
  const BYTE *pbEncoded, 
  DWORD cbEncoded, 
  DWORD dwFlags, 
  void *pvStructInfo, 
  DWORD *pcbStructInfo 
);

dwCertEncodingTypeは、エンコーディングタイプを指定します。 lpszStructTypeは、エンコードされたデータをデコードした際に得られる構造体を表すOIDを指定します。 pbEncodedは、エンコードされているデータを格納するバッファを指定します。 cbEncodedは、エンコードされているデータのサイズを指定します。 dwFlagsは、0を指定して問題ありません。 pvStructInfoは、デコードしたデータを受け取るバッファを指定します。 pcbStructInfoは、バッファのサイズを受け取ります。

実際にデコードを行うとなった場合、 目的のOIDを含む拡張情報をCERT_EXTENSION構造体の配列から取得することになります。 このような場合、個々のCERT_EXTENSION構造体のpszObjIdと目的のOIDが一致するかを 調べるループ文を記述することもできますが、CertFindExtensionを呼び出した方が効率的といえます。

PCERT_EXTENSION WINAPI CertFindExtension(
  LPCSTR pszObjId,
  DWORD cExtensions, 
  CERT_EXTENSION rgExtensions[]
);

pszObjIdは、取得したい拡張情報のOIDを指定します。 cExtensionsは、拡張情報の配列の数を指定します。 rgExtensionsは、拡張情報の配列を指定します。 戻り値は、pszObjIdと一致した拡張情報のアドレスとなります。 次に、CryptDecodeObjectとCertFindExtensionの使用例を示します。

PCERT_EXTENSION pCertExtension;
DWORD           dwSize;
LPBYTE          lpData;

pCertExtension = CertFindExtension(szOID_XXX, pContext->pCertInfo->cExtension, pContext->pCertInfo->rgExtension);
if (pCertExtension == NULL)
	return 0;

CryptDecodeObject(X509_ASN_ENCODING, szOID_XXX, pCertExtension->Value.pbData, pCertExtension->Value.cbData, 0, NULL, &dwSize);
lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
CryptDecodeObject(X509_ASN_ENCODING, szOID_XXX, pCertExtension->Value.pbData, pCertExtension->Value.cbData, 0, lpData, &dwSize);

CERT_INFO構造体のcExtensionとrgExtensionは、それぞれ拡張情報の数と配列になっているため、 それをCertFindExtensionに指定することができます。 CertFindExtensionがNULLを返さなかったということは、指定したOIDを持った拡張情報が取得できたことを意味しますから、 その拡張情報のValue.pbDataをCryptDecodeObjectに指定します。

エンコーディングタイプの詳細

データのエンコードやデコードを行う関数などは、その方法を示すエンコーディングタイプを 指定できるようになっています。 これまでは、この引数に関して特に意識せず、常にX509_ASN_ENCODINGという定数を指定してきましたが、 ここには独自に定義したエンコーディングタイプを指定することも可能となっています。 これはつまり、データのエンコードやデコードを独自のコードで行うということですが、 そのコードはどのような手順を経由して参照されることになるでしょうか。 まず、ポイントとなるのは指定したエンコーディングタイプの値そのものです。 X509_ASN_ENCODINGは1と定義されているため、この1という値を持ったレジストリキーが 次のレジストリキーにて検索されることになります。

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\OID

このキーには、EncodingType 0(これは少し特殊です)やEncodingType 1という名前を持った サブキーが既定で存在しています。 EncodingType 1のサブキーを確認してみると、CryptDllEncodeObjectやCryptDllDecodeObjectという 名前を持ったサブキーが存在することが分かりますが、 これらのどのキーが参照されるかは、実際に呼び出した関数によって異なります。 想像できるように、CryptEncodeObjectを呼び出したならば、CryptDllEncodeObjectのキーが参照され、 CryptDecodeObjectを呼び出した場合はCryptDllDecodeObjectのキーが参照されるという具合になります。 これらのキーは、OIDの名前を持ったサブキーを含んでおり、 それはCryptEncodeObjectやCryptDecodeObjectに指定したOIDと一致するか比較されます。 一致した場合、そのサブキーのエントリとして存在するDLLがロードされ、 エントリに記述されている関数名を持った関数が呼び出されることになります。

上記の内容を踏まえ、独自のエンコーディングタイプを実装するアプリケーションが検討すべきことは、 まず、エンコーディングタイプそのものの値、次に呼び出しに対応する関数、 さらにエンコードの対象とするOID、そして実際にエンコードを行う関数の実装です。 これらの作業が終了すれば、アプリケーションはCryptRegisterOIDFunctionを呼び出して、 エンコーディングタイプ及びそれに付随する情報を登録することになります。

#include <windows.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	BOOL  bRegister = TRUE;
	DWORD dwEncodingType = 10;
	LPSTR lpszOid = "1.2.34";

	if (bRegister)
		CryptRegisterOIDFunction(dwEncodingType, CRYPT_OID_ENCODE_OBJECT_FUNC, lpszOid, L"mydll.dll", "EncodeData");
	else
		CryptUnregisterOIDFunction(dwEncodingType, CRYPT_OID_ENCODE_OBJECT_FUNC, lpszOid);
	
	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);
	
	return 0;
}

CryptRegisterOIDFunctionの第1引数は、登録するエンコーディングタイプの値です。 第2引数は、呼び出しに対応する関数名で、CRYPT_OID_ENCODE_OBJECT_FUNCはCryptDllEncodeObjectとして定義されています。 つまり、CryptEncodeObjectの呼び出しを受け取る意思を示しています。 第3引数は、処理対象とするOIDを指定します。 第4引数は処理(この場合、エンコード)を行う関数を実装するDLLの名前、第5引数はその関数の名前となります。 このプログラムを実行した場合、次のような結果を確認することができます。

図から分かるように、CryptRegisterOIDFunctionを指定した値が反映されています。 これにより、エンコーディングタイプを10、OIDを"1.2.34"に指定したCryptEncodeObjectの呼び出しは、 mydll.dllのEncodeDataへの呼び出しにつながることになります。 EncodeDataのプロトタイプは、CryptEncodeObjectのそれと同一にしておくことになります。



戻る