EternalWindows
スマートカード / コマンドとレスポンス

カードへの接続を終えたら、いよいよカードに対してコマンドを送ることになります。 コマンドというのは、いわばカードに対する命令とそれに付随するデータであり、 たとえばファイルへの書き込みを行うコマンドは、 書き込みを示す値と書き込みたいデータで構成されます。 また、コマンドを受け取ったカードはその応答としてレスポンスを返し、 アプリケーションはこのレスポンスを確認することで、 コマンドが成功したかどうかなどを特定することができます。 コマンドやレスポンスといったカードと送受信されるデータは、 APDU(Application Protocol Data Unit)と呼ばれることもあります。

カードに送るコマンドがカード毎に異なる設計になっている場合、 アプリケーションはカードの数だけコマンドを理解する必要が生じ、 互換性の維持が難しくなります。 こうした問題を解消し、カードの設計に共通の規約を設ける意味で、 ISO 7816という国際規約が制定されています。 この規約は、カードの性質を15のパートに区分けして規定し、 その中のISO 7816-4/8/9がカードに送るべきコマンドの構造を規定しています。 したがって、このコマンドを理解しておくことで、 ISO 7816準拠の各カードを共通のコードで取り扱うことができるようになります。 次に、ISO 7816におけるコマンドのフォーマットを示します。

ヘッダ
CLAINSP1P2

表から分かるように、コマンドを構成するデータはCLAからP2までの4つとなります。 これら4つのデータにどのような値を指定するかは次節で説明するため、 今回は各データのサイズがそれぞれ1バイトであることを理解してください。 つまり、コマンドのサイズは最低でも4バイト必要になります。 実は、これら4バイトはコマンドに必ず含めなければならないヘッダであり、 それ以外のデータ(ボディ)を追加するかどうかはオプションになっています。 上記のフォーマットをcase 1と呼ぶことにして、 次にボディを含むcase 2のフォーマットを示します。

ヘッダボディ
CLAINSP1P2Le

このフォーマットはヘッダに加えて、Leというボディが含まれています。 Leのサイズは1または3バイトにすることが可能で、 指定する値はレスポンスの最高サイズです。 たとえば、50を指定した場合は、コマンドの応答データとして最高50バイトが返ることになります。 一方、Leが含まれない場合は、応答データとして2バイトが返ります。 次に、これとはまた別のフォーマットであるcase 3のフォーマットを示します。

ヘッダボディ
CLAINSP1P2Lcデータ

このフォーマットはヘッダに加えて、Lcとデータというボディが含まれています。 Lcのサイズは1または3バイトであり、指定する値はデータのサイズです。 一方、データのサイズはLcに指定した値となります。 データにどのような値を指定するかは、コマンドのINS(命令内容)によって異なりますが、 たとえば、ファイルに書き込みを行う場合は書き込みたいデータを指定することになります。 最後に示すフォーマットは、Lcとデータ及びLeを含むcase 4となります。

ヘッダボディ
CLAINSP1P2LcデータLe

それぞれ意味については、先に示したフォーマットの同一になりますが、 データの順序には注意してください。 Leを指定する場合は、常にそれがコマンドの最後に位置します。 任意のデータを送信し、任意のサイズのデータを受信したい場合は、 コマンドをこのフォーマットにすることになります。

続いて、レスポンスのフォーマットを確認します。 コマンドのフォーマットにLeが含まれない場合、 つまり、case 1と3のフォーマットでは次のフォーマットを持ったレスポンスが返ります。

ステータスワード
SW1SW2

SW1とSW2はそれぞれ1バイトであり、ステータスワードと呼ばれています。 ステータスワードには、コマンドが成功したかどうかを示す値と、 コマンドが失敗した理由を示す値が含まれているため、 必ず確認する必要があるといえます。 次に、Leを指定したコマンドであるcase 2と4のレスポンスを確認します。

ボディ ステータスワード
データSW1SW2

このフォーマットでは、ステータスワードの前にデータが含まれています。 そのサイズはLeに指定した値以下であり、 たとえば、Leに50を指定していた場合は、 データのサイズは50以下であると考えられます。

ステータスワードとして設定される値を次に示します。

SW1 SW2 意味
0x900x00正常終了。
0x62n警告処理。不揮発性メモリの状態が変化していない。
0x81出力データに異常がある。
0x83DFが閉塞(へいそく)している。
0x63n警告処理。不揮発性メモリの状態が変化している。
0x00照合不一致である。
0x81ファイルが今回の書き込みによっていっぱいになった。
0xcn照合不一致である。'n'によって、残りの再試行回数(1〜15)を示す。
0x64n実行誤り。不揮発性メモリの状態が変化していない。
0x00ファイル制御情報に異常がある。
0x65n実行誤り。不揮発性メモリの状態が変化している。
0x00メモリへの書き込みが失敗した。
0x6700Lc/Leフィールドが間違っている。
0x68n検査誤り。CLAの機能が提供されない。
0x81指定された論理チャンネル番号によるアクセス機能を提供しない。
0x82CLAバイトで指定されたセキュアメッセージング機能を提供しない。
0x69n検査誤り。コマンドは許されない。
0x81ファイル構造と矛盾したコマンドである。
0x82セキュリティステータスが満足されない。
0x84認証方法を受け付けない。
0x84参照されたIEFが閉塞している。
0x85コマンドの使用条件が満足されない。
0x86ファイルが存在しない。
0x87セキュアメッセージングに必要なデータオブジェクトが存在しない。
0x88セキュアメッセージング関連エラー。
0x6an検査誤り。間違ったパラメータP1,P2。
0x80データフィールドのタグが正しくない。
0x81機能が提供されていない。
0x82ファイルが存在しない。
0x83アクセス対象のレコードがない。
0x84ファイル内に十分なメモリ容量がない。
0x85Lcの値がTLV構造に矛盾している。
0x86P1-P2の値が正しくない。
0x87Lcの値がP1-P2に矛盾している。
0x88参照された鍵が正しく設定されていない。
0x6b0x00EF範囲外にオフセット指定した(検査誤り)。
0x6d0x00INSが提供されていない(検査誤り)。
0x6e0x00CLAが提供されていない(検査誤り)。
0x6f0x00自己診断異常(検査誤り)。

成功を示すのは、SW1が0x90でSW2が0x00のときのみです。 それ以外の場合は、コマンドの内容を実行できなかったことを意味します。 nには、任意の16進数値が格納されます。

コマンドを適切にフォーマットすることができれば、 SCardTransmitでカードにコマンドを送ることができます。

LONG SCardTransmit(
  SCARDHANDLE hCard,
  LPCSCARD_IO_REQUEST pioSendPci,
  LPCBYTE pbSendBuffer,
  DWORD cbSendLength,
  LPSCARD_IO_REQUEST pioRecvPci,
  LPBYTE pbRecvBuffer,
  LPDWORD pcbRecvLength
);

hCardは、カードのハンドルを指定します。 pioSendPciは、送信に使用するプロトコルを指定します。 SCARD_PCI_T0がキャラクタ転送であり、SCARD_PCI_T1がブロック転送になります。 基本的に、SCardConnectの第6引数の値を考慮して、どちらを指定するかを判断することになると思われます。 pbSendBufferは、送信したいコマンドを格納したバッファを指定します。 cbSendLengthは、pbSendBufferのサイズを指定します。 pioRecvPciは、NULLで問題ありません。 pbRecvBufferは、コマンドに対するレスポンスを受け取るバッファを指定します。 pcbRecvLengthは、pbRecvBufferのサイズを格納した変数のアドレスを指定します。 関数から制御が返ると、受信したレスポンスのサイズが格納されます。

SCardTransmitが成功するかどうかは、コマンドを正しく送信できたかどうかに関係し、 コマンドの実行結果は考慮されません。 たとえば、ファイルを選択するコマンドを送信し、 実際に指定したファイルが存在しない場合でも、関数自体は成功します。 このようなコマンドの成否は、レスポンスのステータスワードで確認するべきものだからです。 SCardTransmitが失敗する主な原因は、コマンドのフォーマットが正しくないことによるものであり、 たとえば、Lcの値とデータのサイズが矛盾している場合や、 Leの値とpcbRecvLengthに指定したバッファのサイズが矛盾する場合などが挙げられます。

今回のプログラムは、コマンドをカードに送信し、そのレスポンスを確認します。 簡単のため、SCardTransmitの第2引数にSCARD_PCI_T1を指定し、T=1のカードを扱うようにしています。

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

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

BOOL SendCommand(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;
	}
	
	SendCommand(hCard);

	SCardDisconnect(hCard, SCARD_LEAVE_CARD);

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

	return 0;
}

BOOL SendCommand(SCARDHANDLE hCard)
{
	TCHAR szBuf[256];
	DWORD dwBufferSize;
	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;

	dwBufferSize = sizeof(szBuf);
	CryptBinaryToString(response, dwResponseSize, CRYPT_STRING_HEX, szBuf, &dwBufferSize);
	MessageBox(NULL, szBuf, TEXT("レスポンス"), MB_OK);

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

SendCommandという自作関数が、カードにコマンドを送る関数となります。 コマンドは、commandという変数で初期化されています。

BYTE command[] = {0x00, 0xa4, 0x00, 0x00};

コマンドに必ず4バイトのヘッダが含まれることになります。 この例では、CLAが0x00、INSが0xa4、P1が0x00、P2が0x00であり、 ボディが一切存在しないことを意味しています。 つまり、case 1のフォーマットということになります。 このコマンドがどのような命令をカードに与えているかは次節で説明します。 SCardTransmitの呼び出しは、次のようになっています。

dwResponseSize = sizeof(response);
lResult = SCardTransmit(hCard, SCARD_PCI_T1, command, sizeof(command), NULL, response, &dwResponseSize);

レスポンスを受け取るバッファのサイズを変数に格納してから、SCardTransmitを呼び出します。 関数が成功した場合は、第6引数のバッファがレスポンスのデータで初期化され、 第七引数にレスポンスのサイズが返ります。 第2引数の レスポンスのデータを表示するのは、次のコードです。

dwBufferSize = sizeof(szBuf);
CryptBinaryToString(response, dwResponseSize, CRYPT_STRING_HEX, szBuf, &dwBufferSize);
MessageBox(NULL, szBuf, TEXT("レスポンス"), MB_OK);

CryptBinaryToStringは、第1引数のバイトデータを文字列に変換して、 第4引数に返す関数です。 レスポンスのデータの最後は必ずSW1とSW2が存在し、 その値が0x90と0x00であれば関数が成功していることが分かります。

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

コマンドが成功したかどうかは、レスポンスのSW1とSW2を確認することになります。 今回は、コマンドにLeを指定していないため、 レスポンスにはSW1とSW2の2バイトのみが設定され、 response[0]がSW1の値となり、response[1]がSW2の値になります。 また、レスポンスのサイズを表すdwResponseSizeも2となるため、 dwResponseSize - 2は0となり、SW1を参照することができます。 添え字に明示的に0を指定しないのは、 レスポンスのサイズが可変になる場合にも対応するためです。 たとえば、次のようにコマンドをフォーマットしたとします。

BYTE response[100 + 2];
BYTE command[] = {0x00, 0xa4, 0x00, 0x00, 100};

このcommandは、ヘッダに加えて1バイトのボディが含まれています。 この1バイトLeであり、case 2のフォーマットになります。 Leは、レスポンスとして返されるデータのサイズを意味するため、 ここで100を指定した場合は、レスポンスに100サイズ以下のデータが格納されることになります。 レスポンスを受け取るバッファのサイズを100 + 2とするのは、 SW1とSW2の値でデータを上書きしてしまわないためです。 SW1とSW2は必ずレスポンスに含まれるため、バッファのサイズを100としたならば、 98バイトがデータとなり、残り2バイトがSW1とSW2になります。 つまり、2バイト分データが失われることになるため、 予めSW1とSW2の2バイトを考慮しておく必要があります。

Smart Card Service Providerについて

カードにコマンドを送信するアプリケーションは、 主としてWinSCard APIを呼び出していますが、 SCSP(Smart Card Service Provider)を利用する方法もあります。 これは、COMインターフェースを実装するDLLであり、 内部でWinSCard APIを呼び出す設計になっています。 つまり、他の言語からの利用を想定すると共に、 コマンドについての詳細をアプリケーションから隠蔽します。 カードベンダーがこのようなDLLを公開しているかどうかは、 SCardGetProviderIdを呼び出すことで確認することができます。

GUID  guid;
TCHAR szCardName[] = TEXT("CardName");

SCardGetProviderId(0, szCardName, &guid);

スマートカードデータベースに存在するカード用のサブキーには、 Primary Providerというエントリが存在することがあります。 ここには、SCSPが実装するCOMオブジェクトのGUIDが格納されるため、 SCardGetProviderIdはこれを取得します。 関数が成功した場合は、このGUIDをCoCreateInstanceに指定することで、 オブジェクトを作成することができるようになるはずです。 そのようなオブジェクトは、ISCardFileAccessやISCardVerifyなどを実装し、 それらのIIDはSCardListInterfacesで取得できるのではないかと思われます。

Windows Vista以前のWindowsには、システム提供のSCSPとしてscardssp.dllが存在しています。 これは、Base Service Providerとも呼ばれ、 基本的にはWinSCard APIで行えることをCOMで行えるようにしたものと考えることができます。 今回のプログラムとほぼ同様の動作をBase Service Providerで実現すると、 次のようになります。

#include <windows.h>

#define _LPHSCARDCONTEXT_DEFINED
#include <scardssp.h>

const IID   IID_ISCard = {0x1461AAC3, 0x6810, 0x11D0, {0x91, 0x8F, 0x00, 0xAA, 0x00, 0xC1, 0x80, 0x68}};
const CLSID CLSID_CSCard = {0x1461AAC7, 0x6810, 0x11D0, {0x91, 0x8F, 0x00, 0xAA, 0x00, 0xC1, 0x80, 0x68}};

const IID   IID_ISCardCmd = {0xD5778AE3, 0x43DE, 0x11D0, {0x91, 0x71, 0x00, 0xAA, 0x00, 0xC1, 0x80, 0x68}};
const CLSID CLSID_CSCardCmd = {0xD5778AE7, 0x43DE, 0x11D0, {0x91, 0x71, 0x00, 0xAA, 0x00, 0xC1, 0x80, 0x68}};

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	ISCard    *pSCard;
	ISCardCmd *pSCardCmd;
	HRESULT   hr;
	WORD      wResponse;
	TCHAR     szBuf[256];
	WCHAR     szReaderName[] = L"ReaderName";
	
	CoInitialize(NULL);

	hr = CoCreateInstance(CLSID_CSCard, NULL, CLSCTX_ALL, IID_ISCard, (LPVOID *)&pSCard);
	if (FAILED(hr)) {
		if (hr == REGDB_E_CLASSNOTREG)
			MessageBox(NULL, TEXT("指定されたオブジェクトが存在しません。"), NULL, MB_ICONWARNING);
		CoUninitialize();
		return 0;
	}
	
	hr = CoCreateInstance(CLSID_CSCardCmd, NULL, CLSCTX_ALL, IID_ISCardCmd, (LPVOID *)&pSCardCmd);
	if (FAILED(hr)) {
		if (hr == REGDB_E_CLASSNOTREG)
			MessageBox(NULL, TEXT("指定されたオブジェクトが存在しません。"), NULL, MB_ICONWARNING);
		pSCard->Release();
		CoUninitialize();
		return 0;
	}

	hr = pSCard->AttachByReader(szReaderName, SHARED, T1);
	if (FAILED(hr)) {
		if (hr == SCARD_E_NO_SERVICE)
			MessageBox(NULL, TEXT("Smart Cardサービスが起動されていません。"), NULL, MB_ICONWARNING);
		else if (hr == SCARD_E_UNKNOWN_READER)
			MessageBox(NULL, TEXT("カードリーダが接続されていません。"), NULL, MB_ICONWARNING);
		else if (hr == SCARD_W_REMOVED_CARD)
			MessageBox(NULL, TEXT("カードがセットされていません。"), NULL, MB_ICONWARNING);
		else
			;
		pSCardCmd->Release();
		pSCard->Release();
		CoUninitialize();
		return 0;
	}

	hr = pSCardCmd->BuildCmd(0x00, 0xa4, 0x00, 0x00, NULL, NULL);
	if (SUCCEEDED(hr)) {
		pSCard->Transaction(&pSCardCmd);
		pSCardCmd->get_ReplyStatus(&wResponse);
		wsprintf(szBuf, TEXT("%02x %02x"), HIBYTE(wResponse), LOBYTE(wResponse));
		MessageBox(NULL, szBuf, TEXT("OK"), MB_OK);
	}
	else
		MessageBox(NULL, TEXT("コマンドの作成に失敗しました。"), NULL, MB_ICONWARNING);
	
	pSCard->Detach(LEAVE);

	pSCardCmd->Release();
	pSCard->Release();
	CoUninitialize();

	return 0;
}

Base Service Providerの機能を利用する場合は、scardssp.hをインクルードすることになります。 事前に_LPHSCARDCONTEXT_DEFINEDを定義しているのは、 VC 2005や2008などでコンパイルエラーを出さないためです。 CoCreateInstanceを呼び出す際に必要な各種GUIDは、 scardssp.hに定義されていないため、コード上で明示的に定義するようにしています。

Base Service Providerを扱う上で必要となるインターフェースは、ISCardとISCardCmdです。 ISCardは、カードへの接続やATRの取得、コマンドの送信などをサポートし、 ISCardCmdはコマンドの作成をサポートします。 最終的な目的は、ISCardCmdで作成したコマンドをISCardでカードに送信することです。 ISCardの取得には、CoCreateInstanceの第1引数にCLSID_CSCardを指定し、 第4引数にIID_ISCardを指定します。 一方、ISCardCmdを取得するには、第1引数にCLSID_CSCardCmdを指定し、 第4引数にIID_ISCardCmdを指定します。 Base Service Providerが実装するクラスオブジェクトとインターフェースは、 この他にもいくつか存在します。

ISCardを取得したら、AttachByReaderでカードに接続することができます。 これが成功すれば、ISCardCmdのBuildCmdでコマンドを内部的に作成し、 ISCardのTransactionでコマンドをカードに送信します。 カードから返されたレスポンスのステータスバイトは、 ISCardCmdのget_ReplyStatusで取得することができるため、 この上位バイトと下位バイトを表示するようにしています。 カードへの接続が不要になったら、ISCardのDetachを呼び出します。



戻る