EternalWindows
自己署名入り証明書 / 発行者と発行先

証明書の発行者と発行先が何を意味するのかは、実際に証明書を手にいれる際のことを 考えてみると分かりやすいでしょう。 証明書を必要とする開発者は、自身の目的に適った証明書を発行するCAを選択し、 そのCAに対して証明書申請の手続きを行います。 そして、最終的にCAより証明書が発行されるわけですが、 この証明書の発行者(Issuer)にCAの名前が格納されます。 そして、CAからすれば、申請者に対して証明書を発行しているわけですから、 証明書の発行先(Subject)は申請者の名前となります。 ただし、Root CAの証明書や自己署名入り証明書などは、 自分で自分の証明書を発行していることになるため、 発行者と発行先は同一の名前となります。

発行者と発行先の名前は、X.500の規格に準拠した形で決定することになっています。 X.500はディレクトリサービスに関する規格であり、 証明書の規格であるX.509とは、また別の規格です。 ただし、X.509はX.500シリーズの1つであり、 発行者と発行先の名前においてX.500の規格を利用することになっているので、 X.500の規格についても理解しておく必要があります。 次に、X.500の識別名の一部を示します。

識別名 説明
CN(CommonName) 一般名。主にユーザーの名前を指定する。
OU(OrganizationalUnitName) 組織内単位。組織や会社の部門を区別する場合に指定する。
O(OrganizationName) 組織名。組織または会社の法律上の名前を指定する。
C(CountryName) 国。組織または個人が存在する国のコードを指定する。

識別名について理解するために、1つの例を挙げて考えてみましょう。 社内で運営しているCAから社員に対して証明書を発行するとものとします。 この場合、証明書の発行先は社員名になるということで、 CN=社員名という文字列を発行先として指定することができます。 識別名の指定は、常に"識別名=名前"というようになります。 ただし、発行先をCN=社員名とした場合、 その社員が組織内でどのような位置づけにいるのかまでは判断することができません。 組織には部門や課などの単位があるため、 このような情報も発行先に指定することができるのです。 つまり、"CN=社員名, OU=部門名"というように複数の識別名で構成しても問題ありません。 実際にはこれに加えて、その社員と部門がどの組織のものかを 示すためにO=組織名を含める場合や、社員が複数の部門に属する場合は、OUが 2つ以上になることもあります。

発行者と発行先は、それぞれCERT_INFO構造体のIssuerメンバとSubjectメンバに相当します。 これらは、それぞれCERT_NAME_BLOB構造体として定義されていますが、 次に示すように同類の構造体が数多く定義されています。

typedef struct _CRYPTOAPI_BLOB {
  DWORD    cbData;
  BYTE*    pbData;
} CRYPT_INTEGER_BLOB,    *PCRYPT_INTEGER_BLOB, 
  CRYPT_UINT_BLOB,     *PCRYPT_UINT_BLOB, 
  CRYPT_OBJID_BLOB,    *PCRYPT_OBJID_BLOB, 
  CERT_NAME_BLOB,      *PCERT_NAME_BLOB, 
  CERT_RDN_VALUE_BLOB, *PCERT_RDN_VALUE_BLOB, 
  CERT_BLOB,           *PCERT_BLOB, 
  CRL_BLOB,            *PCRL_BLOB, 
  DATA_BLOB,           *PDATA_BLOB, 
  CRYPT_DATA_BLOB,     *PCRYPT_DATA_BLOB, 
  CRYPT_HASH_BLOB,     *PCRYPT_HASH_BLOB, 
  CRYPT_DIGEST_BLOB,   *PCRYPT_DIGEST_BLOB, 
  CRYPT_DER_BLOB,      *PCRYPT_DER_BLOB, 
  CRYPT_ATTR_BLOB,     *PCRYPT_ATTR_BLOB;

このように複数の名前で構造体を定義するのは、 名前から格納されているデータを識別できるようにするためです。 CERT_NAME_BLOBであれば、pbDataがX.500名をエンコードしたバイトデータであると推測でき、 CRYPT_HASH_BLOBならば、pbDataがハッシュ値のバイナリデータであることが推測できます。 cbDataがデータのサイズとなります。

文字列のX.500名からCERT_NAME_BLOB構造体を初期化するには、 X.500形式にエンコードされたバイトデータとそのサイズが必要です。 これは、CertStrToNameを呼び出すことで取得できます。

BOOL WINAPI CertStrToName(
  DWORD dwCertEncodingType,
  LPCTSTR pszX500,
  DWORD dwStrType,
  void *pvReserved,
  BYTE *pbEncoded,
  DWORD *pcbEncoded,
  LPCTSTR *ppszError
);

dwCertEncodingTypeは、指定した文字列のエンコード方法であるエンコーディングタイプを指定します。 エンコーディングタイプは原則として、X509_ASN_ENCODINGとなります。 pszX500は、エンコードしたいX.500形式の文字列を指定します。 dwStrTypeは、pszX500の形式を指定しますが、基本的にCERT_X500_NAME_STRを指定します。 pvReservedは、予約されているためNULLを指定します。 pbEncodedは、エンコードされたバイトデータを受け取るバッファを指定します。 pcbEncodedは、pbEncodedのサイズを格納した変数のアドレスを指定します。 ppszErrorは、pszX500の内容がエラーである箇所を指すための変数のアドレスを指定します。 エラーの箇所を必要としない場合は、NULLを指定しても問題ありません。

CERT_INFO構造体のSubjectメンバとIssuerメンバを初期化する例を次に示します。

TCHAR szSubjectName[] = TEXT("CN=name");

if (!CertStrToName(X509_ASN_ENCODING, szSubjectName, CERT_X500_NAME_STR, NULL, NULL, &dwSize, NULL))
	return FALSE;
lpData = (LPBYTE)HeapAlloc(GetProcessHeap(), 0, dwSize);
CertStrToName(X509_ASN_ENCODING, szSubjectName, CERT_X500_NAME_STR, NULL, lpData, &dwSize, NULL);
certInfo.Subject.cbData = dwSize;
certInfo.Subject.pbData = lpData;
certInfo.Issuer = certInfo.Subject;

CertStrToNameの第2引数に指定する文字列は、X.500形式になっている必要があります。 ここでは識別名としてCNを選択し、名前の部分はnameとしていますが、 識別名=名前という形ならば、どのような値を指定しても問題ありません。 1回目のCertStrToNameでは、エンコードされたバイトデータのサイズを取得すると共に、 関数の戻り値を調べています。 これは、X.500形式でない文字列を指定した場合に関数が失敗するようになっているためです。 2回目の呼び出しで実際にデータを取得したら、 後はそのデータとサイズをCERT_INFO構造体のSubjectメンバを代入するだけです。 今回作成する証明書は自己署名入り証明書であり、 発行先とと発行者は同一になるため、初期化したSubjectメンバをそのままIssuerメンバに代入しています。

RDNの取得

エンコードされたIssuerメンバやSubjectメンバをデコードしたい場合は、 CertNameToStrを呼び出すことができます。 この関数の呼び出し方は、次のようになります。

CertNameToStr(X509_ASN_ENCODING, &pContext->pCertInfo->Subject, CERT_X500_NAME_STR, szBuf, sizeof(szBuf));

この関数の第3引数にCERT_X500_NAME_STRを指定した場合、 返される文字列は"CN=XXX, OU=ZZZ"のような識別名と名前の組が複数並ぶ形になりますが、 この形式を正確にはDNと呼びます。 一方、CN=XXXのように単一の組をRDNと呼びます。 個々のRDNを分割して取得したいような場合は、CertNameToStrではなく、 次のような手順を用いてRDNを取得することができます。

#include <windows.h>

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HCERTSTORE       hStore;
	PCCERT_CONTEXT   pContext;
	PCERT_NAME_INFO  pNameInfo;
	DWORD            dwSize;
	DWORD            i;
	PCCRYPT_OID_INFO pOidInfo;
	WCHAR            szRdn[512];
	
	hStore = CertOpenSystemStore(0, TEXT("CA"));
	if (hStore == NULL)
		return 0;

	pContext = CertEnumCertificatesInStore(hStore, NULL);
	
	CryptDecodeObject(X509_ASN_ENCODING, X509_NAME, pContext->pCertInfo->Subject.pbData, pContext->pCertInfo->Subject.cbData, 0, NULL, &dwSize);
	pNameInfo = (PCERT_NAME_INFO)HeapAlloc(GetProcessHeap(), 0, dwSize);
	CryptDecodeObject(X509_ASN_ENCODING, X509_NAME, pContext->pCertInfo->Subject.pbData, pContext->pCertInfo->Subject.cbData, 0, pNameInfo, &dwSize);

	for (i = 0; i < pNameInfo->cRDN; i++) {
		pOidInfo = CryptFindOIDInfo(CRYPT_OID_INFO_OID_KEY, pNameInfo->rgRDN[i].rgRDNAttr->pszObjId, CRYPT_RDN_ATTR_OID_GROUP_ID);
		CertRDNValueToStrW(CERT_RDN_ANY_TYPE, &pNameInfo->rgRDN[i].rgRDNAttr->Value, szRdn, sizeof(szRdn));
		MessageBoxW(NULL, szRdn, pOidInfo->pwszName, MB_OK);	
	}

	HeapFree(GetProcessHeap(), 0, pNameInfo);
	CertFreeCertificateContext(pContext);
	CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);

	return 0;
}

CryptDecodeObjectは、何らかの形式でエンコードされたデータをデコードする関数で、 そのデコードの形式を第2引数に指定することになっています。 X509_NAMEを指定した場合、第5引数のバッファにCERT_NAME_INFO構造体で表現できる データが返ることになっているため、これを取得するようにします。 CERT_NAME_INFO構造体にはCERT_RDN構造体が含まれており、 この数だけRDNが存在することが分かります。 RDNの構成する属性型は、rgRDNAttr->pszObjIdにOIDとして格納されているため、 指定したOIDに関する情報を取得するCryptFindOIDInfoに指定することができます。 ここで取得できるCRYPT_OID_INFO構造体のpwszNameメンバには、OIDに関連する名前が格納されています。 一方、RDNの属性値はrgRDNAttr->Valueに格納されており、 CertRDNValueToStrWを呼び出して文字列に変換することができます。 明示的にUNICODE版の関数を呼び出しているのは、先に取得した属性型の文字列との型を一致させるためであり、 それ以上の意味はありません。



戻る