EternalWindows
スマートカード / セキュリティステータス

前節で問題となったのは、PINを格納するIEFがファイル構造上のどこに存在するか分らないという点でした。 これにより、IEFに対してPINの照合を行うことができず、 ファイルの作成や書き込みなどの操作を断念せざる得ない結果となってしまいました。 おそらく、こうした動作を行うためには、カードベンダー提供のツールを利用しなければならないのでしょう。 ただし、鍵ペアや証明書はCryptoAPIを通じて格納することができるため、 このような目的を達成する場合は専用のツールは不要です。

もし適切なIEFを予め知っていた場合、 実際にはどのようなコマンドを発行することになるのでしょうか。 このためには、IEFにどのような種類の鍵が格納されるかを理解しておく必要があります。

鍵の種類 該当する鍵
平文鍵 ・照合鍵
計算鍵 ・内部認証鍵
・署名生成用秘密鍵
認証鍵 ・外部認証鍵(制限回数)
・署名検証(認証)用公開鍵
・証明書検証用公開鍵

平文鍵にグループ化される照合鍵は、正確には鍵ではなくPINのことです。 つまり、照合鍵を使って暗号化や署名を行うようなことはありません。 照合鍵を格納するIEFに対して発行するコマンドは、VERIFYコマンドになります。 一方、計算鍵にグループ化される内部認証鍵は、暗号化に使用できる鍵であり、 これを格納するIEFに対して発行するコマンドは、INTERNAL AUTHETICATEコマンドになります。 また、計算鍵にグループ化される外部認証鍵も暗号化に使用できる鍵であり、 これを格納するIEFに対して発行するコマンドは、EXTERNAL AUTHETICATEコマンドになります。 署名生成用秘密鍵や署名検証用公開鍵は認証には利用されず、 COMPUTE DIGITAL SIGNATUREコマンドやVERIFY DIGITAL SIGNATUREで利用されます。 なお、ファイルがIEFであるかどうかはFCIから判断できますが、 それが実際にどのような種類の鍵を格納しているかを判断することはできないと思われます。

次に、VERIFYコマンドを送る例を示します。

BYTE commandVerify[] = {0x00, 0x20, 0x00, 0x80, 5, 1, 2, 3, 4, 5};

INSは0x20となり、P1は0x00となります。 P2の0x80という値は、現在選択されているIEFに対して認証を行うという意味になります。 このため、目的のIEFは事前にSELECT FILEコマンドで選択しておくことになります。 5バイト目の5はLcであり、指定するデータ(PIN)のサイズが5バイトであることを意味します。 上記の場合では、データとして12345を指定しているため、 この通りの値が照合鍵のIEFに格納されているならば、 成功を示すステータスワードが返ることになります。

指定したPINがIEFに格納されているそれと異なる場合、 ステータスワードに0x63cnが格納されることになります。 nには0からfまでの値が入り、認証を行うことのできる残りの回数を意味しています。 これをリトライカウンタと呼び、たとえば、0x63c2となっていれば、 残り2回しかVERIFYコマンドを送ることができません。 最終的に0x63c0が返った場合はIEFが閉塞(ロック)され、 VERIFYコマンドを送ることができなくなります。 ちなみに、リトライカウンタを知りたい場合は、 次のようにボディを除いてVERIFYコマンドを送ります。

BYTE commandVerify[] = {0x00, 0x20, 0x00, 0x80};

このコマンドはリトライカウンタを確認するのみですから、 これによりリトライカウンタが減るようなことはありません。 認証に成功した場合は、リトライカウンタは初期値にリセットされます。 リトライカウンタの初期値は、IEFの作成時に指定されます。

VERIFYコマンドが成功した場合、スマートカード内では具体的にどのようなことが起こるのでしょうか。 答えは、セキュリティステータスの獲得です。 セキュリティステータスとは、鍵に対しての照合または認証の結果であり、 これが成功を示しているかどうかで行えるセキュリティ操作が変化します。 ファイルにはセキュリティ属性があり、これには"xxxコマンドを実行するためには、 このIEFの照合が必要"というような情報が含まれています。 コマンドのレスポンスには、セキュリティステータスが満足されていないことを示す0x6982がありますが、 これは正にIEFに対しての照合または認証を終えていないことを意味しています。 ただし、コマンドのアクセスがフリーとして定義されている場合は、 このような操作は必要ありません。 ちなみに、セキュリティステータスは論理チャンネル毎に管理されています。 論理チャンネルはコマンドのCLAの下位2ビットに指定され、この他にカレントファイルも管理しています。

セキュリティステータスを獲得できる認証コマンドには、 VERIFYコマンド以外にEXTERNAL AUTHENTICATEコマンドがあります。 これは外部認証と呼ばれ、カードにアクセスするシステムを認証します。 具体的な手順としては、まずGET CHALLENGEコマンドでカードからチャレンジ(乱数)を取得し、 その乱数を外部認証鍵で暗号化します。 そして、この暗号化データをEXTERNAL AUTHENTICATEコマンドで送り、 カードはIEFに格納された外部認証鍵を使用して複合化します。 この結果が、チャレンジと一致する場合は、認証が成功したことになります。 次に、GET CHALLENGEコマンドを送る例を示します。

BYTE commandChallenge[] = {0x00, 0x84, 0x00, 0x00, 0x07};

GET CHALLENGEコマンドのINSは0x84であり、P1とP2は共に0を指定します。 ボディはLeのみであり、取得するチャレンジのサイズを指定します。 ここでは7と指定しているため、7バイトのチャレンジがレスポンスとして返ることになります。

GET CHALLENGEコマンドで取得したチャレンジは、 外部認証鍵で暗号化してEXTERNAL AUTHENTICATEコマンドで送ることになりますが、 ここでも問題が発生することになります。 1つはこのコマンドを送るIEF、つまり外部認証鍵を送るべきIEFが分からないという点と、 もう1つは暗号化に使う外部認証鍵を予め持ち合わせていないという点です。 おそらく、EXTERNAL AUTHENTICATEコマンドは次のように送ることになります。

BYTE commandExternal[] = {0x00, 0x82, 0x00, 0x00, n, ...};

INSは0x82であり、P1は0x00となります。 P2は現在選択されているIEFを表す0x80を指定することができますが、0x00を指定することもできます。 この場合、現在選択されているセキュリティ環境から、 外部認証鍵を格納するIEFが特定されるため、 目的のIEFを知っておく必要はなくなる可能性があります。 ボディについては、Lcに暗号化データのサイズを指定し、 データフィールドに暗号化データを指定します。

セキュリティステータスを獲得できるコマンドではありませんが、 INTERNAL AUTHENTICATEという内部認証を行うコマンドがあります。 内部認証というのは、システムがカードを認証するためのものであり、 データフィールドにはランダムなデータを指定します。 これを受け取ったカードは、内部認証鍵を使用して暗号化し、 この暗号化したデータをレスポンスとして返します。 これを受け取ったシステムは、その暗号化されたデータを内部認証鍵で複合化し、 その結果がランダムなデータと同一であるならば、 カードは不正なものではないと判断することができます。

セキュリティ環境について

計算鍵や認証鍵を使用するコマンドは、それを格納するIEFのファイル識別子を指定する代わりに、 現在のセキュリティ環境を参照することができます。 セキュリティ環境はDF毎に存在し、現在のセキュリティ環境(カレントSE)とは、 現在選択しているDFのセキュリティ環境になります。 セキュリティ環境の正体は、データオブジェクトです。 つまり、タグが分かっていればGET DATAコマンドでデータを取得することができます。 セキュリティ環境を表すデータオブジェクトは、次のようなタグを持ちます。

タグ 長さ 意味
0x803 暗号アルゴリズムID。
0x81可変 ファイルへの参照。ファイル識別子かDF名。JICSAP 2.0の仕様書にこの記述はない。
0x82可変 鍵を格納するDF名。
0x832 固定鍵または公開鍵を格納しているIEFのファイル識別子。
0x842 セッション鍵生成鍵または秘密鍵を格納しているIEFのファイル識別子。
0x891 鍵が格納されるIEFの階層レベル。
0x4D可変 拡張ヘッダリスト。

0x81と0x82で識別されるデータはDF名を格納しているため、 これを取得すればカードに存在するDFを列挙できる可能性があります。 以下に、そのプログラムを示します。

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

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

BOOL SendCommand(SCARDHANDLE hCard);
BOOL GetDfName(SCARDHANDLE hCard, WORD wTag, LPSTR lpszDfName);
BOOL SelectMf(SCARDHANDLE hCard);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	SCARDCONTEXT hContext;
	SCARDHANDLE  hCard;
	LPTSTR       lpszReaderName;
	LONG         lResult;
	DWORD        dwActiveProtocol;
	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 0;
	}

	lResult = SCardListReaders(hContext, NULL, (LPTSTR)&lpszReaderName, &dwAutoAllocate);
	if (lResult != SCARD_S_SUCCESS) {
		if (lResult == SCARD_E_NO_READERS_AVAILABLE)
			MessageBox(NULL, TEXT("カードリーダが接続されていません。"), NULL, MB_ICONWARNING);
		SCardReleaseContext(hContext);
		return 0;
	}

	lResult = SCardConnect(hContext, lpszReaderName, SCARD_SHARE_SHARED, SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, &hCard, &dwActiveProtocol);
	if (lResult != SCARD_S_SUCCESS) {
		if (lResult == SCARD_W_REMOVED_CARD)
			MessageBox(NULL, TEXT("カードがセットされていません。"), NULL, MB_ICONWARNING);
		SCardFreeMemory(hContext, lpszReaderName);
		SCardReleaseContext(hContext);
		return 0;
	}

	if (SelectMf(hCard))
		SendCommand(hCard);
		
	SCardDisconnect(hCard, SCARD_LEAVE_CARD);

	SCardFreeMemory(hContext, lpszReaderName);
	SCardReleaseContext(hContext);

	return 0;
}

BOOL SendCommand(SCARDHANDLE hCard)
{
	char  szDfName[16 + 1];
	BYTE  nDfSize;
	DWORD dwResponseSize;
	LONG  lResult;
	BYTE  response[2];
	BYTE  commandSelect[16 + 5] = {0x00, 0xa4, 0x04, 0x00};

	for (;;) {
		if (!GetDfName(hCard, 0x0181, szDfName)) {
			if (!GetDfName(hCard, 0x0182, szDfName))
				break;
		}

		MessageBoxA(NULL, szDfName, "OK", MB_OK);

		nDfSize = (BYTE)lstrlenA(szDfName);
		commandSelect[4] = nDfSize;
		CopyMemory(&commandSelect[5], szDfName, nDfSize);

		dwResponseSize = sizeof(response);
		lResult = SCardTransmit(hCard, SCARD_PCI_T1, commandSelect, nDfSize + 5, NULL, response, &dwResponseSize);
		if (lResult != SCARD_S_SUCCESS)
			break;
		if (response[dwResponseSize - 2] != 0x90 || response[dwResponseSize - 1] != 0x00)
			break;
	}

	return TRUE;
}

BOOL GetDfName(SCARDHANDLE hCard, WORD wTag, LPSTR lpszDfName)
{
	DWORD dwResponseSize;
	LONG  lResult;
	BYTE  response[16 + 2];
	BYTE  commandGet[] = {0x00, 0xca, HIBYTE(wTag), LOBYTE(wTag), 0};

	dwResponseSize = sizeof(response);
	lResult = SCardTransmit(hCard, SCARD_PCI_T1, commandGet, sizeof(commandGet), NULL, response, &dwResponseSize);
	if (lResult != SCARD_S_SUCCESS)
		return FALSE;
	if (response[dwResponseSize - 2] != 0x90 || response[dwResponseSize - 1] != 0x00)
		return FALSE;
	
	lResult = SCardTransmit(hCard, SCARD_PCI_T1, commandGet, sizeof(commandGet), NULL, response, &dwResponseSize);
	lstrcpynA(lpszDfName, (LPSTR)response, dwResponseSize - 1);

	return TRUE;
}

BOOL SelectMf(SCARDHANDLE hCard)
{
	DWORD dwResponseSize;
	LONG  lResult;
	BYTE  response[2];
	BYTE  command[] = {0x00, 0xa4, 0x00, 0x00};

	dwResponseSize = sizeof(response);
	lResult = SCardTransmit(hCard, SCARD_PCI_T1, command, sizeof(command), NULL, response, &dwResponseSize);
	if (lResult != SCARD_S_SUCCESS)
		return FALSE;

	return response[dwResponseSize - 2] == 0x90 && response[dwResponseSize - 1] == 0x00;
}

SendCommandという関数は、GetDfNameという関数を通じてDF名を取得します。 最初に取得を試みるのは0x81であり、それに失敗した場合は0x82のデータオブジェクトを対象とします。 上位バイトに関しては、0x01や0x02を指定することができますが、0x01で問題ないと思われます。 関数が成功した場合は取得したDF名を表示し、そのDFを選択するようにします。 このDF配下には最初のMF配下とは別のデータオブジェクトが存在すると思われるため、 選択することでその別のデータオブジェクトがGET DATAコマンドの検索対象となります。 具体的な手順としては、commandSelectという変数に格納されているヘッダの後にLcとしてDFのサイズを指定し、 その後に取得したDF名をコピーします。 これにより、commandSelectはLcとデータが含まれたことになり、 SELECT FILEコマンドに指定することができるようになります。



戻る