EternalWindows
スマートカード / スマートカードと鍵ペア

CryptoAPIを用いてスマートカードにアクセスする場合は、 次の2点について理解しておく必要あります。 まず1つは、前節で述べたようにCryptAcquireContextに指定するCSPの引数に、 カードベンダーのCSPを指定する点です。 そして2つ目は、鍵コンテナとして指定する文字列を適切にフォーマットする点です。

\\\\.\\ReaderName\\ContainerName

ReaderNameの個所にカードリーダの名前を記述し、ContainerNameの個所に鍵コンテナの名前を指定します。 このようなフォーマットにしなければならないのは、 カードリーダが複数個システムに接続されている場合に対処するためです。 単純に鍵コンテナの名前を指定するだけでは、 どのカードリーダのカードに鍵コンテナを作成すればよいかをCSPが判断することができません。 もちろん、CSPを実装しているのはベンダーですから、 必ずしも鍵コンテナをフォーマットする必要があるとは限りません。

今回のプログラムは、実際にカードの中に鍵コンテナを作成し、 鍵交換用の鍵ペアを作成します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HCRYPTPROV hProv;
	HCRYPTKEY  hKey;
	TCHAR      szProvName[] = TEXT("Vendor CSP Name");
	TCHAR      szContainerName[] = TEXT("\\\\.\\ReaderName\\MyContainer);
	DWORD      dwKeySpec = AT_KEYEXCHANGE;

	if (!CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, 0)) {
		if (GetLastError() != NTE_BAD_KEYSET && GetLastError() != NTE_KEYSET_NOT_DEF) {
			MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}

		if (!CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, CRYPT_NEWKEYSET)) {
			MessageBox(NULL, TEXT("鍵コンテナの作成に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}
		else
			MessageBox(NULL, TEXT("鍵コンテナを作成しました。"), TEXT("OK"), MB_OK);
	}

	if (!CryptGetUserKey(hProv, dwKeySpec, &hKey)) {
		if (GetLastError() != NTE_NO_KEY) {
			MessageBox(NULL, TEXT("鍵ペアのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			CryptReleaseContext(hProv, 0);
			return 0;
		}
		if (!CryptGenKey(hProv, dwKeySpec, 0, &hKey)) {
			MessageBox(NULL, TEXT("鍵ペアの作成に失敗しました。"), NULL, MB_ICONWARNING);
			CryptReleaseContext(hProv, 0);
			return 0;
		}
		else
			MessageBox(NULL, TEXT("鍵ペアを作成しました。"), TEXT("OK"), MB_OK);
	}
	else
		MessageBox(NULL, TEXT("鍵ペアのハンドルを取得しました。"), TEXT("OK"), MB_OK);
	
	CryptDestroyKey(hKey);
	CryptReleaseContext(hProv, 0);

	return 0;
}

szProvNameにベンダーのCSPを指定し、szContainerNameにリーダ名を含んだ鍵コンテナの名前を指定します。 ContainerNameの部分は自由に決めることができますが、 ReaderNameの部分にはシステムに接続されているリーダ名を指定する必要があります。 主要な処理を1つずつ見ていきます。

if (!CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, 0)) {
	if (GetLastError() != NTE_BAD_KEYSET && GetLastError() != NTE_KEYSET_NOT_DEF) {
		MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

第5引数に0を指定したCryptAcquireContextでは、CSPのハンドルを取得すると共に、 指定された鍵コンテナがカードに存在するかを確認します。 ここで、GetLastErrorがNTE_BAD_KEYSETやNTE_KEYSET_NOT_DEFを返した場合は、 鍵コンテナが存在しないことによるエラーであると判断できるため、 2回目のCryptAcquireContextに入る必要があります。

if (!CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, CRYPT_NEWKEYSET)) {
	MessageBox(NULL, TEXT("鍵コンテナの作成に失敗しました。"), NULL, MB_ICONWARNING);
	return 0;
}

CRYPT_NEWKEYSETを指定したCryptAcquireContextの呼び出しでは、 カードの中に鍵コンテナを作成する関係上、PINを入力するダイアログが表示されるはずです。 おそらく、これはどのようなCSPにも共通して言えることでしょう。 カードベンダーは、マニュアル等にユーザーPINと管理者PINを記述していますが、 ここで入力するのはユーザーPINです。 このユーザーPINを正しく入力すれば、鍵コンテナが作成されます。 管理者PINは、ユーザーPINのリトライカウンタをリセットするために存在し、 ベンダー付属のツールで使用するものと思われます。

if (!CryptGetUserKey(hProv, dwKeySpec, &hKey)) {
	if (GetLastError() != NTE_NO_KEY) {
		MessageBox(NULL, TEXT("鍵ペアのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		CryptReleaseContext(hProv, 0);
		return 0;
	}
	if (!CryptGenKey(hProv, dwKeySpec, 0, &hKey)) {
		MessageBox(NULL, TEXT("鍵ペアの作成に失敗しました。"), NULL, MB_ICONWARNING);
		CryptReleaseContext(hProv, 0);
		return 0;
	}
	else
		MessageBox(NULL, TEXT("鍵ペアを作成しました。"), TEXT("OK"), MB_OK);
}

これについては、特に重要な点はありません。 指定した用途を持った鍵ペアが既に作成されているかをCryptGetUserKeyで確認し、 作成されていない場合はCryptGenKeyで作成するだけです。 スマートカードを使用する場合であっても、基本的にはCryptoAPIを 利用した通常のアプリケーションと同じコードを使用することができます。

今回のプログラムでは、ベンダーのCSP名とリーダ名をハードコードするよう設計されていますが、 これらは動的に取得した方が柔軟性は増すのではないでしょうか。 確かに特定のCSPだけを対象にするのは好ましくないかもしれませんが、 CSPにはベンダー毎に多少の実装の違いがみられるため、 あらゆるCSPで問題なく機能するためのコードを記述するのは難しいところがあります。 たとえば、リーダにカードがセットされていない場合、 CryptAcquireContextの呼び出し時において、 ダイアログを表示するかどうかはCSPによって異なるといえるでしょう。 ベンダーがCSPではなくCard MiniDriverを用意している場合は、 こうした問題は解消されます。

WinSCard APIの併用

CryptAcquireContextに指定するCSP名やリーダ名を動的に取得したい場合は、 WinSCard APIを呼び出す必要があります。 次に、その例を示します。

#include <windows.h>
#include <winscard.h>

#pragma comment (lib, "winscard.lib")
#pragma comment (lib, "scarddlg.lib")

BOOL GetProvAndReaderName(LPTSTR lpszProvName, LPTSTR lpszReaderName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HCRYPTPROV hProv;
	HCRYPTKEY  hKey;
	TCHAR      szProvName[256];
	TCHAR      szReaderName[256];
	TCHAR      szContainerName[256];
	DWORD      dwKeySpec = AT_KEYEXCHANGE;

	if (!GetProvAndReaderName(szProvName, szReaderName))
		return 0;

	lstrcpy(szContainerName, TEXT("\\\\.\\"));
	lstrcat(szContainerName, szReaderName);
	lstrcat(szContainerName, TEXT("\\"));
	lstrcat(szContainerName, TEXT("MyContainer"));

	if (!CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, 0)) {
		if (GetLastError() != NTE_BAD_KEYSET && GetLastError() != NTE_KEYSET_NOT_DEF) {
			MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}

		if (!CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, CRYPT_NEWKEYSET)) {
			MessageBox(NULL, TEXT("鍵コンテナの作成に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}
		else
			MessageBox(NULL, TEXT("鍵コンテナを作成しました。"), TEXT("OK"), MB_OK);
	}

	if (!CryptGetUserKey(hProv, dwKeySpec, &hKey)) {
		if (GetLastError() != NTE_NO_KEY) {
			MessageBox(NULL, TEXT("鍵ペアのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			CryptReleaseContext(hProv, 0);
			return 0;
		}
		if (!CryptGenKey(hProv, dwKeySpec, 0, &hKey)) {
			MessageBox(NULL, TEXT("鍵ペアの作成に失敗しました。"), NULL, MB_ICONWARNING);
			CryptReleaseContext(hProv, 0);
			return 0;
		}
		else
			MessageBox(NULL, TEXT("鍵ペアを作成しました。"), TEXT("OK"), MB_OK);
	}
	else
		MessageBox(NULL, TEXT("鍵ペアのハンドルを取得しました。"), TEXT("OK"), MB_OK);
	
	CryptDestroyKey(hKey);
	CryptReleaseContext(hProv, 0);

	return 0;
}

BOOL GetProvAndReaderName(LPTSTR lpszProvName, LPTSTR lpszReaderName)
{
	SCARDCONTEXT    hContext;
	LONG            lResult;
	OPENCARDNAME_EX openCard;
	TCHAR           szReaderName[256];
	TCHAR           szCardName[256];
	LPTSTR          lpszProvNameTemp;
	DWORD           dwAutoAllocate = SCARD_AUTOALLOCATE;

	lResult = SCardEstablishContext(SCARD_SCOPE_USER, NULL, NULL, &hContext);
	if (lResult != SCARD_S_SUCCESS) {
		if (lResult == SCARD_E_NO_SERVICE)
			MessageBox(NULL, TEXT("Smart Cardサービスが起動されていません。"), NULL, MB_ICONWARNING);
		return FALSE;
	}

	ZeroMemory(&openCard, sizeof(OPENCARDNAME_EX));
	openCard.dwStructSize  = sizeof(OPENCARDNAME_EX);
	openCard.hSCardContext = hContext;
	openCard.dwFlags       = SC_DLG_MINIMAL_UI;
	openCard.lpstrRdr      = szReaderName;
	openCard.nMaxRdr       = sizeof(szReaderName);
	openCard.lpstrCard     = szCardName;
	openCard.nMaxCard      = sizeof(szCardName);

	lResult = SCardUIDlgSelectCard(&openCard);
	if (lResult != SCARD_S_SUCCESS) {
		if (lResult == SCARD_E_NO_READERS_AVAILABLE)
			MessageBox(NULL, TEXT("カードリーダが接続されていません。"), NULL, MB_ICONWARNING);
		return FALSE;
	}
	
	lResult = SCardGetCardTypeProviderName(hContext, szCardName, SCARD_PROVIDER_CSP, (LPTSTR)&lpszProvNameTemp, &dwAutoAllocate);
	if (lResult != SCARD_S_SUCCESS) {
		SCardReleaseContext(hContext);
		return FALSE;
	}
	
	lstrcpy(lpszProvName, lpszProvNameTemp);
	lstrcpy(lpszReaderName, szReaderName);
	
	SCardFreeMemory(hContext, lpszProvNameTemp);
	SCardReleaseContext(hContext);
	
	return TRUE;
}

GetProvAndReaderNameという関数は、呼び出し側にCSP名とリーダ名を返します。 まず、この関数はSCardUIDlgSelectCardを呼び出して、 システムに接続されているリーダ名とリーダにセットされているカード名を取得します。 dwFlagsにSC_DLG_MINIMAL_UIを指定しているため、 リーダにカードがセットされていない場合はダイアログが表示されることになります。 カード名を取得すれば、SCardGetCardTypeProviderNameにSCARD_PROVIDER_CSPを指定し、 カード名に対応するCSP名を取得することができます。



戻る