EternalWindows
CSP / 公開鍵暗号と証明書

公開鍵暗号における通信では、通信相手が誰であるかをより明確にするために、 証明書が利用されることがよくあります。 証明書には公開鍵と所有者が記述されており、 これを受け取った相手は送信側が自分の想定している相手であるかを確認できると共に、 証明書に含まれる公開鍵で暗号化を行うことができます。 基本的にCryptoAPIでは、証明書を利用した暗号化にCryptEncryptMessageとCryptDecryptMessageを 使いますが、決してCryptEncryptやCryptDecryptを利用できないわけではありません。 しかし、このためには証明書から公開鍵を取得する方法と、 関連する秘密鍵を取得する方法を理解しておかなければなりません。

証明書を作成するコンピュータでは、最初に鍵ペアを作成しておくことになります。 この鍵ペアの公開鍵を証明書に含むようにし、自身の情報を含めるようにします。 また、作成元のコンピュータ上に存在する証明書には、CSP情報を関連付けておくようにします。 こうすることで、証明書を表すPCCERT_CONTEXT型から、 CSPのハンドルを取得することができ、最終的にCryptGetUserKeyで鍵ペアのハンドルを取得することができます。 証明書から、関連するCSPを取得するにはCryptAcquireCertificatePrivateKeyを呼び出します。

BOOL WINAPI CryptAcquireCertificatePrivateKey(
  PCCERT_CONTEXT pCert,
  DWORD dwFlags,
  void* pvReserved,
  HCRYPTPROV_OR_NCRYPT_KEY_HANDLE* phCryptProvOrNCryptKey,
  DWORD* pdwKeySpec,
  BOOL* pfCallerFreeProvOrNCryptKey
);

pCertは、CSP情報が関連付けられた証明書の証明書コンテキストを指定します。 dwFlagsは、専用の定数を指定します。 pvReservedは、予約されているためNULLを指定します。 phCryptProvOrNCryptKeyは、HCRYPTPROV型、もしくはNCRYPT_KEY_HANDLE型の 変数のアドレスを指定します。 証明書には、Windows Vistaより登場したCNGキーを関連付けることもできるため、 このような場合は、NCRYPT_KEY_HANDLE型を指定します。 pdwKeySpecは、鍵の用途を受け取る変数のアドレスを指定します。 証明書に関連付けられているのがCSPの情報である場合は、 返される値がAT_KEYEXCHANGEかAT_SIGNATUREのいずれかになりますが、 CNGキーの場合はCERT_NCRYPT_KEY_SPECになります。 pfCallerFreeProvOrNCryptKeyは、phCryptProvOrNCryptKeyのハンドルを開放する必要があるかどうかです。 TRUEの場合は、HCRYPTPROV型の際にはCryptReleaseContextを、 NCRYPT_KEY_HANDLE型の場合はNCryptFreeObjectで開放することになります。

dwFlagsに指定できる定数の一部を次に示します。

定数 意味
CRYPT_ACQUIRE_CACHE_FLAG ハンドルをキャッシュする。 pfCallerFreeProvOrNCryptKeyには、FALSEが格納される。
CRYPT_ACQUIRE_COMPARE_KEY_FLAG 証明書に含まれる公開鍵とCSPの鍵コンテに格納されている鍵が一致しているかどうかを調べる。
CRYPT_ACQUIRE_NO_HEALING 証明書の拡張プロパティにCERT_KEY_PROV_INFO_PROP_IDが存在しなくても、それを作成しようとしない。
CRYPT_ACQUIRE_SILENT_FLAG CryptAcquireCertificatePrivateKeyの呼び出しによってUIが表示される可能性がある場合、 関数を失敗させるようにする。

CryptAcquireCertificatePrivateKeyは、証明書に関連する情報を基にCSPのハンドルを取得するわけですが、 証明書には正規の情報としてCSPは含まれていません。 ただし、CERT_KEY_PROV_INFO_PROP_IDという拡張プロパティを設定すると、 CSPの情報を関連付けられることになっているため、 恐らくCryptAcquireCertificatePrivateKeyもこの点に注目していると思われます。 あくまで推測ですが、次のようなコードが実行されているのではないでしょうか。

PCRYPT_KEY_PROV_INFO pCryptKeyProvInfo;

CertGetCertificateContextProperty(pContext, CERT_KEY_PROV_INFO_PROP_ID, NULL, &dwSize);
pCryptKeyProvInfo = (PCRYPT_KEY_PROV_INFO)HeapAlloc(GetProcessHeap(), 0, dwSize);
CertGetCertificateContextProperty(pContext, CERT_KEY_PROV_INFO_PROP_ID, pCryptKeyProvInfo, &dwSize);

CryptAcquireContextW(phCryptProv, pCryptKeyProvInfo->pwszContainerName, pCryptKeyProvInfo->pwszProvName, pCryptKeyProvInfo->dwProvType, 0);
*pdwKeySpec = pCryptKeyProvInfo->dwKeySpec;

HeapFree(GetProcessHeap(), 0, pCryptKeyProvInfo);

証明書にCERT_KEY_PROV_INFO_PROP_IDの拡張プロパティが設定されている場合、 CertGetCertificateContextPropertyでCRYPT_KEY_PROV_INFO構造体を取得することができます。 この構造体には、そのCSPの名前や証明書に含まれる公開鍵を格納した鍵コンテの名前、及び鍵の用途などが格納されており、 CSPの名前と鍵コンテの名前をCryptAcquireContextに指定することができます。 これにより、CSPのハンドルを取得できることになります。 使い分けとしては、CSPの情報を取得したい場合にはCertGetCertificateContextPropertyを、 CSPのハンドルを取得したい場合はCryptAcquireCertificatePrivateKeyを呼び出すことになるでしょう。

先の説明により、証明書の所有者が証明書から秘密鍵を取得する方法が分かりました。 次は、証明書を受信した側が証明書から公開鍵を取得する方法を検討します。 これには、CryptImportPublicKeyInfoを呼び出します。

BOOL WINAPI CryptImportPublicKeyInfo(
  HCRYPTPROV hCryptProv,
  DWORD dwCertEncodingType,
  PCERT_PUBLIC_KEY_INFO pInfo,
  HCRYPTKEY *phKey
);

hCryptProvは、CSPのハンドルを指定します。 dwCertEncodingTypeは、X509_ASN_ENCODINGを指定します。 pInfoは、公開鍵の情報を格納したCERT_PUBLIC_KEY_INFO構造体のアドレスを指定します。 phKeyは、公開鍵を受け取るHCRYPTKEY型の変数を指定します。

次に、証明書を利用したセッション鍵の交換の例を示します。

BOOL ImportSessionKey(HANDLE hPipe, HCRYPTPROV *phProv, HCRYPTKEY *phSessionKey)
{
	BYTE           keyData[1024];
	DWORD          dwWriteByte;
	DWORD          dwReadByte;
	HCRYPTKEY      hKey;
	HCERTSTORE     hStore;
	PCCERT_CONTEXT pContext;
	DWORD          dwKeySpec;
	HCRYPTPROV     hProv;

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

	pContext = CertFindCertificateInStore(hStore, X509_ASN_ENCODING, 0, CERT_FIND_SUBJECT_STR, L"MyCert Publisher", NULL);
	if (pContext == NULL) {
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);	
		return 0;
	}

	if (!CryptAcquireCertificatePrivateKey(pContext, 0, NULL, &hProv, &dwKeySpec, NULL)) {
		CertFreeCertificateContext(pContext);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return FALSE;
	}

	if (!CryptGetUserKey(hProv, dwKeySpec, &hKey)) {
		CryptReleaseContext(hProv, 0);
		CertFreeCertificateContext(pContext);
		CertCloseStore(hStore, CERT_CLOSE_STORE_CHECK_FLAG);
		return FALSE;
	}

	WriteFile(hPipe, pContext->pbCertEncoded, pContext->cbCertEncoded, &dwWriteByte, NULL);
	
	ReadFile(hPipe, keyData, sizeof(keyData), &dwReadByte, NULL);
	CryptImportKey(hProv, keyData, dwReadByte, hKey, 0, phSessionKey);

	*phProv = hProv;

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

	return TRUE;
}

BOOL CreateAndExportSessionKey(HANDLE hPipe, HCRYPTPROV hProv, ALG_ID algid, HCRYPTKEY *phPubKey, HCRYPTKEY *phSessionKey)
{
	BYTE           certData[1024];
	BYTE           keyData[1024];
	DWORD          dwKeySize;
	DWORD          dwWriteByte;
	DWORD          dwReadByte;
	HCRYPTKEY      hPubKey;
	HCRYPTKEY      hSessionKey;
	PCCERT_CONTEXT pContext;
	
	ReadFile(hPipe, certData, sizeof(certData), &dwReadByte, NULL);
	pContext = CertCreateCertificateContext(X509_ASN_ENCODING, certData, dwReadByte);

	if (!CryptImportPublicKeyInfo(hProv, X509_ASN_ENCODING, &pContext->pCertInfo->SubjectPublicKeyInfo, &hPubKey)) {
		CertFreeCertificateContext(pContext);
		return FALSE;
	}
	
	if (!CryptGenKey(hProv, algid, CRYPT_EXPORTABLE, &hSessionKey))
		return FALSE;

	CryptExportKey(hSessionKey, hPubKey, SIMPLEBLOB, 0, keyData, &dwKeySize);
	WriteFile(hPipe, keyData, dwKeySize, &dwWriteByte, NULL);
	
	*phPubKey = hPubKey;
	*phSessionKey = hSessionKey;	

	return TRUE;
}

前節と同様に、サーバー側の関数をImportSessionKeyとし、 クライアント側の関数をCreateAndExportSessionKeyとします。 まず、サーバーは証明書をクライアントに公開しなければならないため、 MY証明書ストアから証明書を取得しようとしています。 これらの処理は、CertOpenSystemStoreとCertFindCertificatesInStoreに該当します。 この時点で直ぐに証明書をWriteFileに指定してもよいのですが、 後でセッション鍵を秘密鍵で複合化することを考え、先に鍵ペアのハンドルを取得することにしています。 CryptAcquireCertificatePrivateKeyに証明書を指定すれば、CSPのハンドルが取得できるため、 そのCSPに対してCryptGetUserKeyを呼び出せば、鍵ペアのハンドルを取得できます。 以上の作業が終われば、WriteFileでクライアントに証明書を公開し、 ReadFileでセッション鍵が送られてくるまで待機します。

クライアント側のReadFileが制御を返したということは、サーバーからの証明書を受信したということです。 送られてきたバイトデータは、CertCreateCertificateContextで証明書コンテキストに変換できるため、 これをCryptImportPublicKeyInfoに指定し、公開鍵のハンドルを取得することができます。 CryptExportKeyの第2引数にその公開鍵を指定することにより、セッション鍵は公開鍵で暗号化され、 WriteFileの呼び出しで、サーバーに送られることになります。 サーバーはReadFileでこれを受信し、CryptImportKeyに鍵ペアのハンドルを指定することにより、 暗号化されたセッション鍵を複合化できることになります。


戻る