EternalWindows
スマートカード / MS_SCARD_PROV

スマートカードへのアクセスにベンダー提供のCSPを利用することは、 CSP間による実装の違いというある種の問題を抱えることになります。 たとえば、CryptAcquireContextで例えるなら、 カード内に鍵コンテナを作成するという行為はどのCSPでも実装されているはずですが、 存在しない鍵コンテナを指定した場合のエラー値というような細かい部分については、 CSPによって異なる可能性があります。 こうした問題を解消するためにカードベンダーがすべきことは、 アプリケーションにMS_SCARD_PROVというスマートカード用のCSPを利用できる環境を提供することです。 このようにすれば、CryptAcquireContextのCSP名を受け取る引数はMS_SCARD_PROVで固定になりますし、 理解すべき動作や戻り値はMS_SCARD_PROVのみとなります。 たとえば、MS_SCARD_PROVにおけるPIN入力のダイアログは、Windows XP環境下では次のように表示されることになります。

ベンダー提供のカードの仕様を理解できるのは、そのベンダー以外に他なりませんから、 MS_SCARD_PROVがカードに正しくアクセスできるように、 ベンダーはCard MiniDriverと呼ばれるDLLを提供しておかなければなりません。 このDLLは、MS_SCARD_PROVが理解できる関数をエクスポートしており、 MS_SCARD_PROVは必要に応じてこれらの関数を呼び出すことになります。 当然ながら、アプリケーションが扱うのはMS_SCARD_PROVですから、 アプリケーションがCard MiniDriverの詳細を理解しておく必要はありません。 MS_SCARD_PROVがどうやってCard MiniDriverのDLL名を特定しているかについては、 リーダにセットされたカードのATRから、スマートカードデータベースを参照していると思われます。

さて、実際にMS_SCARD_PROVを利用したプログラムを作成するには、 Card MiniDriverを提供しているカードベンダーのカードが必要になります。 たとえば、iCanal社は、自社のスマートカード用のCard MiniDriverを無料で公開しており、 この企業のスマートカードとリーダ(これはHitachi社製)を購入すれば、 MS_SCARD_PROVを利用したアプリケーションを作成することができます。

http://www.icanal.co.jp/myutoken/index.html

上記サイトより、myuTokenという名前のファイルをダウンロードして実行すると、 ツールやCard MiniDriverなどがインストールされます。 後は、ドキュメントに記述されているフォーマットなどの処理を終えれば、 アプリケーションを実行する準備は整ったことになります。 なお、Windows Vista以前のWindowsではMS_SCARD_PROVが既定でインストールされていないため、 Windows Updateを通じて別途インストールする必要があります。 ただし幸いなことに、上記したファイルのインストーラーは、 MS_SCARD_PROVをインストールする機能を備えています。

既に述べたように、MS_SCARD_PROVを利用するには、 この定数をCryptAcquireContextの第3引数に指定するだけです。 しかしながら、このCSPには少し癖のある動作が含まれるため、 まずはその部分について確認しておきます。

CryptAcquireContext(&hProv, szContainerName, MS_SCARD_PROV, PROV_RSA_FULL, 0);

CryptAcquireContextの第5引数に0を指定した場合、第2引数に指定した鍵コンテナをオープンする意味となります。 この鍵コンテナが既に存在する場合においては、特に不具合が生じるようなことはありませんが、 鍵コンテナが存在しない場合は、何故かダイアログが表示されることになっています。 このダイアログは、SCardUIDlgSelectCardで表示されるダイアログと同一ですが、 決して正常に終了することができないと思われるため、 このようなダイアログを表示させない方法を考える必要があります。

CryptAcquireContext(&hProv, szContainerName, MS_SCARD_PROV, PROV_RSA_FULL, CRYPT_SILENT);

CryptAcquireContextの第5引数にCRYPT_SILENTを指定した場合、 この関数及び、以降のCSP関数の呼び出しでダイアログが表示されることはなくなります。 よって、鍵コンテナが存在しない場合は、NTE_BAD_KEYSETを返して関数が失敗するため、 鍵コンテナの作成に入ることができます。 ただし、既に鍵コンテナが存在する場合において、このコードはある1つの問題を抱えています。 それは、CRYPT_SILENTを指定してCSPのハンドルを取得してしまうと、 CryptSignHashなどの秘密鍵を利用する関数を呼び出せなくなるという点です。 このような関数では、PIN入力を促すダイアログが表示されますが、 CRYPT_SILENTを指定している場合はダイアログを表示することができず、 関数はNTE_SILENT_CONTEXTを返した失敗してしまうのです(ただし、PINをキャッシュしている場合は別です)。 そもそも、CRYPT_SILENTを指定したのは、鍵コンテナが存在しない場合のための対処法であって、 それ以上の意味が含まれることは望みませんから、 しかるべき対策が必要となります。

BOOL IsExistContainer(LPTSTR lpszProvName, LPTSTR lpszContainerName)
{
	BOOL       bResult;
	HCRYPTPROV hProv;

	bResult = CryptAcquireContext(&hProv, lpszContainerName, lpszProvName, PROV_RSA_FULL, CRYPT_SILENT);
	if (bResult)
		CryptReleaseContext(hProv, 0);

	return bResult;
}

この関数は、引数に指定された鍵コンテナが既に存在するかを調べます。 このため、鍵コンテナが存在しない場合にダイアログが表示されないよう、第5引数に0を指定します。 重要なのは、鍵コンテナが既に存在する場合、つまり関数が成功した場合にCSPのハンドルを開放しているところであり、 CRYPT_SILENTのハンドルを使用して何らかの関数を呼び出すことはありません。 あくまで、CryptAcquireContextの第5引数に0を指定してCSPのハンドルを取得したいわけです。 IsExistContainerが成功した場合における、CryptAcquireContextの呼び出しは、 鍵コンテナが既に存在することが証明されているので、 0を指定してもダイアログが表示されることはありません。

今回のプログラムは、CryptAcquireContextにMS_SCARD_PROVを指定して鍵コンテナを作成します。 szPinに指定する文字列は、ベンダー提供のドキュメントより確認してください。

#include <windows.h>

BOOL IsExistContainer(LPTSTR lpszProvName, LPTSTR lpszContainerName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HCRYPTPROV hProv;
	HCRYPTKEY  hKey;
	TCHAR      szProvName[] = MS_SCARD_PROV;
	TCHAR      szContainerName[] = TEXT("MyContainer");
	CHAR       szPin[] = "";
	DWORD      dwKeySpec = AT_KEYEXCHANGE;
	BOOL       bAutoInputPin = FALSE;

	if (!CryptAcquireContext(&hProv, NULL, szProvName, PROV_RSA_FULL, CRYPT_DEFAULT_CONTAINER_OPTIONAL)) {
		if (GetLastError() == SCARD_E_NO_SERVICE)
			MessageBox(NULL, TEXT("Smart Cardサービスが起動されていません。"), NULL, MB_ICONWARNING);
		else if (GetLastError() == SCARD_E_NO_READERS_AVAILABLE)
			MessageBox(NULL, TEXT("カードリーダが接続されていません。"), NULL, MB_ICONWARNING);
		else if (GetLastError() == SCARD_W_CANCELLED_BY_USER)
			MessageBox(NULL, TEXT("カードがセットされませんでした。"), NULL, MB_ICONWARNING);
		else
			;
		return 0;
	}
	
	CryptReleaseContext(hProv, 0);

	if (!IsExistContainer(szProvName, szContainerName)) {
		if (bAutoInputPin) {
			if (CryptAcquireContext(&hProv, NULL, szProvName, PROV_RSA_FULL, CRYPT_DEFAULT_CONTAINER_OPTIONAL)) {
				if (!CryptSetProvParam(hProv, PP_KEYEXCHANGE_PIN, (LPBYTE)szPin, 0)) {
					if (GetLastError() == SCARD_W_WRONG_CHV)
						MessageBox(NULL, TEXT("指定されたPINが正しくありません。"), NULL, MB_ICONWARNING);
					CryptReleaseContext(hProv, 0);
					return 0;
				}
				CryptReleaseContext(hProv, 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);
	}
	else {
		if (!CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, 0)) {
			MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}
	}

	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 IsExistContainer(LPTSTR lpszProvName, LPTSTR lpszContainerName)
{
	BOOL       bResult;
	HCRYPTPROV hProv;

	bResult = CryptAcquireContext(&hProv, lpszContainerName, lpszProvName, PROV_RSA_FULL, CRYPT_SILENT);
	if (bResult)
		CryptReleaseContext(hProv, 0);

	return bResult;
}

カードベンダーがCard MiniDriverを提供していることを想定して、 szProvNameにはMS_SCARD_PROVを指定しています。 また、MS_SCARD_PROVでは、リーダ名を含んだ鍵コンテナのパスを作成する必要はなく、 単純に鍵コンテナの名前を指定するだけで鍵コンテナを作成することができるため、 szContainerNameにリーダ名は含まれていません。 最初に実行される処理は、次のようになっています。

if (!CryptAcquireContext(&hProv, NULL, szProvName, PROV_RSA_FULL, CRYPT_DEFAULT_CONTAINER_OPTIONAL)) {
	if (GetLastError() == SCARD_E_NO_SERVICE)
		MessageBox(NULL, TEXT("Smart Cardサービスが起動されていません。"), NULL, MB_ICONWARNING);
	else if (GetLastError() == SCARD_E_NO_READERS_AVAILABLE)
		MessageBox(NULL, TEXT("カードリーダが接続されていません。"), NULL, MB_ICONWARNING);
	else if (GetLastError() == SCARD_W_CANCELLED_BY_USER)
		MessageBox(NULL, TEXT("カードがセットされませんでした。"), NULL, MB_ICONWARNING);
	else
		;
	return 0;
}

CryptReleaseContext(hProv, 0);

このコードは、スマートカードが利用できる状態であるかを確認するためのものです。 CRYPT_DEFAULT_CONTAINER_OPTIONALは、そうした確認作業やCryptGet(Set)ProvParamの呼び出しを行う場合だけに利用する定数であり、 この定数を指定した場合は鍵コンテナにアクセスが生じるようなことはありません。 そのため、第2引数はNULLを指定することになります。 鍵コンテナにアクセスしない定数には、CRYPT_VERIFYCONTEXTもありますが、 この定数はセッション鍵を利用した暗号化を行う場合などに指定することが多いため、 確認作業にはCRYPT_DEFAULT_CONTAINER_OPTIONALの方が適切であるといえます。 確認用に取得したCSPのハンドルは、確認に問題がなければ開放することになります。

先に示した確認コードは、前々節のベンダーCSPのプログラムで行われていません。 理由は、CSPによってGetLastErrorが返す値が異なるためです。 また、リーダにカードがセットされていない場合の動作も、CSPによって異なるといえるでしょう。 しかし、ベンダーがCard MiniDriverを用意している場合は、 アプリケーションが利用するCSPはMS_SCARD_PROVになりますから、 常に同じ動作を期待することができます。 そのため、どのような場合にどの戻り値が返るかも分かりますし、 リーダにカードがセットされていない場合は、ダイアログが表示されることも既知の事実となります。

鍵コンテナのオープンと作成に関するコードは、次のようになっています。

if (!IsExistContainer(szProvName, szContainerName)) {
	if (bAutoInputPin) {
		if (CryptAcquireContext(&hProv, NULL, szProvName, PROV_RSA_FULL, CRYPT_DEFAULT_CONTAINER_OPTIONAL)) {
			if (!CryptSetProvParam(hProv, PP_KEYEXCHANGE_PIN, (LPBYTE)szPin, 0)) {
				if (GetLastError() == SCARD_W_WRONG_CHV)
					MessageBox(NULL, TEXT("指定されたPINが正しくありません。"), NULL, MB_ICONWARNING);
				CryptReleaseContext(hProv, 0);
				return 0;
			}
			CryptReleaseContext(hProv, 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);
}
else {
	if (!CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, 0)) {
		MessageBox(NULL, TEXT("CSPのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
}

既に述べたように、IsExistContainerが失敗したということは、 指定した鍵コンテナが存在しないということですから、 鍵コンテナ名の作成に入ることになります。 このとき、bAutoInputPinという変数がTRUEである場合は、 CryptSetProvParamを呼び出してPINをキャッシュしようとします。 このようにすれば、鍵コンテナの作成時、つまりCRYPT_NEWKEYSETを指定したCryptAcquireContextの呼び出し時に、 PINの入力を促すダイアログが表示されることはなくなります。 PINの入力時に利用するCSPのハンドルは、CRYPT_DEFAULT_CONTAINER_OPTIONALを指定して取得していますが、 ここではCRYPT_VERIFYCONTEXTを指定することはできません。

MS_SCARD_PROVを利用していて分ったことですが、CryptAcquireContextにCRYPT_NEWKEYSETを指定した場合、 NTE_TOKEN_KEYSET_STORAGE_FULLというエラーが返ることがあるようです。 これは、カードに鍵コンテナを作成できるスペースがないことを意味するため、 新しい鍵コンテナを作成するためには、既存の鍵コンテナを作成する必要があります。

if (IsExistContainer(szProvName, szContainerName)) {
	if (CryptAcquireContext(&hProv, szContainerName, szProvName, PROV_RSA_FULL, CRYPT_DELETEKEYSET))
		MessageBox(NULL, TEXT("鍵コンテナを削除しました。"), TEXT("OK"), MB_OK);
	return 0;
}

鍵コンテナを削除する場合は、第5引数にCRYPT_DELETEKEYSETを指定します。 これによりPINの入力を求められ、 入力したPINが正しい場合は鍵コンテナが削除されることになります。

CryptoAPIによるリーダ名の取得

MS_SCARD_PROVでは、鍵コンテナの名前を単一で指定することができますが、 できればリーダ名を含めたコンテナ名を指定した方がよいといえます。 システムに接続されているリーダ名を取得するには、SCardListReadersを呼び出しますが、 コードの統一感を出すためにも、できればCryptoAPIで取得したいものです。 実はMS_SCARD_PROVを利用している場合は、これが可能です。

BOOL GetFullPathContainerName(LPTSTR lpszProvName, LPTSTR lpszContainerName, LPTSTR lpszFullPathContainerName)
{
	HCRYPTPROV hProv;
	LPTSTR     lpszReaderName;
	LPSTR      lpszReaderNameA;
	DWORD      dwSize;

	if (!CryptAcquireContext(&hProv, NULL, lpszProvName, PROV_RSA_FULL, CRYPT_DEFAULT_CONTAINER_OPTIONAL))
		return FALSE;

	if (!CryptGetProvParam(hProv, PP_SMARTCARD_READER, NULL, &dwSize, 0)) {
		CryptReleaseContext(hProv, 0);
		return FALSE;
	}
	lpszReaderNameA = (LPSTR)HeapAlloc(GetProcessHeap(), 0, dwSize);
	CryptGetProvParam(hProv, PP_SMARTCARD_READER, (LPBYTE)lpszReaderNameA, &dwSize, 0);

#if UNICODE
	dwSize = (lstrlenA(lpszReaderNameA) + 1) * sizeof(WCHAR);
	lpszReaderName = (LPWSTR)HeapAlloc(GetProcessHeap(), 0, dwSize);
	MultiByteToWideChar(CP_ACP, 0, lpszReaderNameA, -1, lpszReaderName, dwSize);
#else
	lpszReaderName = lpszReaderNameA;
#endif

	lstrcpy(lpszFullPathContainerName, TEXT("\\\\.\\"));
	lstrcat(lpszFullPathContainerName, lpszReaderName);
	lstrcat(lpszFullPathContainerName, TEXT("\\"));
	lstrcat(lpszFullPathContainerName, lpszContainerName);

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

	return TRUE;
}

この関数は、第1引数にCSP名、第2引数にリーダ名を含まないコンテナ名を受け取り、 第3引数にリーダ名を含めたフルパスのコンテナ名を返します。 まず、CryptAcquireContextにCRYPT_DEFAULT_CONTAINER_OPTIONALを指定してCSPのハンドルを取得しますが、 これはCRYPT_VERIFYCONTEXTでも問題はありません。 続いて、CryptGetProvParamにPP_SMARTCARD_READERを指定し、 ANSI文字列で表されたリーダ名を取得します。 MS_SCARD_PROVでは、この処理に成功するのが大きいといえます。 UNICODEが定義されている場合は、ANSI文字列をUNICODE文字列に変換するためにMultiByteToWideCharを呼び出します。 後は、lpszFullPathContainerNameにフルパスのコンテナ名を構成する各要素を順に連結していくことになります。 GetFullPathContainerNameの呼び出しは、次のようになります。

TCHAR szContainerName[256];
GetFullPathContainerName(MS_SCARD_PROV, TEXT("MyContainer"), szContainerName);

関数が成功した場合、リーダ名を含んだフルパスの鍵コンテナの名前がszContainerNameに格納されます。



戻る