記事 #1 からの続きです。免責事項とテスト環境は前の記事 #1 をよく確認してください。
BitVisor での検証のゴール
さっそく BitVisor での検証をはじめましょう。この記事の目的は BitVisor のID管理(idman)ライブラリだけを使ってマイナンバーカードから ATR (Answer To Reset) を取得することです。ATR については #1 を参照してください。簡単そうに思えますが意外と落とし穴が多いので、この記事ですこしでも時間短縮できる人が増えたら嬉しいです。
ICカードリーダのデバッグ(F11を使う)
まず、core/io_iohook.c の中で F11PANIC
と NTTCOM_TEST
が define されていたときに、カードリーダが初期化されるテストが書いてあります。とりあえずこの部分をそのまま使って IDMan_IPInitializeReader関数と IDMan_IPFinalizeReader関数の動作だけを確認することから始めます。
以下のように、NTTCOM_TEST を忘れずに define しておかないと F11 を押すとただ PANIC で止まるだけになりますので注意してください。最初は IDMan_IPInitializeReader関数とIDMan_IPFinalizeReader関数のあいだは全てコメントアウトしておきます。
--- a/master:bitvisor/core/io_iohook.c
+++ b/idman-debug:bitvisor/core/io_iohook.c
@@ -36,6 +36,8 @@
#include "panic.h"
#include "printf.h"
+#define NTTCOM_TEST
+
#ifdef DEBUG_IO_MONITOR
static enum ioact
kbdio_monitor (enum iotype type, u32 port, void *data)
@@ -104,15 +106,15 @@ kbdio_dbg_monitor (enum iotype type, u32 port, void *data)
printf ("IDMan_IPInitializeReader.\n");
i = IDMan_IPInitializeReader( );
printf ("IDMan_IPInitializeReader return = %d.\n", i);
- printf ("IDMan_IPInitialize.\n");
- i = IDMan_IPInitialize("123456789@ABCDEF", &session);
- printf ("IDMan_IPInitialize return = %d.\n", i);
- printf ("IDMan_generateSignatureByIndex.\n");
- i = IDMan_generateSignatureByIndex( session, 1, "1234567890abcdef", strlen("123
- printf ("IDMan_generateSignatureByIndex return = %d siglen=%d\n", i, siglen);
- printf ("IDMan_IPFinalize.\n");
- i = IDMan_IPFinalize(session);
- printf ("IDMan_IPFinalize return = %d.\n", i);
+// printf ("IDMan_IPInitialize.\n");
+// i = IDMan_IPInitialize("123456789@ABCDEF", &session);
+// printf ("IDMan_IPInitialize return = %d.\n", i);
+// printf ("IDMan_generateSignatureByIndex.\n");
+// i = IDMan_generateSignatureByIndex( session, 1, "1234567890abcdef", strlen("1
+// printf ("IDMan_generateSignatureByIndex return = %d siglen=%d\n", i, siglen);
+// printf ("IDMan_IPFinalize.\n");
+// i = IDMan_IPFinalize(session);
+// printf ("IDMan_IPFinalize return = %d.\n", i);
printf ("IDMan_IPFinalizeReader.\n");
i = IDMan_IPFinalizeReader( );
printf ("IDMan_IPFinalizeReader return = %d.\n", i);
bitvisor/.config も以下のように修正します。これで F11 をたたくと上記のテストコードを実行するようになります。
CONFIG_PS2KBD_F11PANIC=1
CONFIG_IDMAN=1
defconfig にも iccard.enable と iccard.status がありますが、この記事ではこれらの設定は 0 のままにしておいてください。
BitVisor を make して /boot/efi ディレクトリへコピーしマシンを再起動します。 grub メニューから BitVisor を選び、続いて CentOS を選びます。 CentOS が起動したら、F11 キーを押して上記の処理を実行させます。BitVisor の dbgsh で log の中身を見ます。(Vmware Fusion を使っている人はメニューから "キー送信" の "F11" を選びます)
F11 pressed.
IDMan_IPInitializeReader.
IDMan_IPInitializeReader return = -10.
IDMan_IPFinalizeReader.
IDMan_IPFinalizeReader return = -9.
カードリーダが認識されない?
エラーがでていますからソースコードを追って調べていきましょう。 IDMan_IPInitializeReader関数と IDMan_IPFinalizeReader関数は idman/lib/idman_pkcs11/IDMan_IPCommon.c にあります。前につくった doxygen のドキュメント からウェブブラウザでマウスをぽちぽちと押して IDMan_IPCommon.c をクリックしてみると「戻り値 -10:リーダ接続エラーの場合」と書いてありました。同じ html ページに以下のような CALL GRAPH も表示されているはずです。多少いやな予感はしなくもありませんが、とりあえずこのコールグラフ(地図)をたよりにソースコードの奥地へと進んでいきましょう。
まず IDMan_IPInitializeReader関数@{idman_pkcs11/IDMan_IPCommon.c} を読みます。ソースコードのなかに以下のような DEBUG_OutPut関数が含まれていました。開発時に使っていたものでしょうか?これをぜひ活用したいものです。
rv = C_Initialize(0x00);
/**PKCS#11ライブラリ初期化エラーの場合、 */
if (rv != CKR_OK)
{
/**−異常リターンする。 */
DEBUG_OutPut("IDMan_IPInitializeReader PKCS#11ライブラリ初期化 エラー C_Initialize\n");
ret = IDMan_IPRetJudgment(rv);
if (ret != RET_IPNG_INITIALIZE_REPEAT)
{
return RET_IPNG_NO_READER;
}
return ret;
}
デバッグ用プリントを有効にする
無理やりですが idman/lib/idman_pkcs11/IDMan_IPCommon.h を以下のように修正します。__VA_ARGS__
は関数マクロで可変引数を実現する書き方です。IDMan_*.c のオリジナル実装でもともと DEBUG_OutPut 関数を使っていたようなのですが、今のソースコードでは実体が定義されていないので、BitVisor の printf に置き換えて dbgsh から log で確認できるようにします。
--- a/bitvisor/idman/lib/idman_pkcs11/IDMan_IPCommon.h
+++ b/bitvisor/idman/lib/idman_pkcs11/IDMan_IPCommon.h
@@ -30,6 +30,8 @@
#include "IDMan_PKPkcs11.h"
#include "IDMan_StandardIo.h"
+#include <core/printf.h>
+
#define IDMAN_IPMAIN
#include "IDMan_IPLibrary.h"
@@ -78,8 +80,11 @@
#define ALGORITHM_IDLEN_SHA256 19
#define ALGORITHM_IDLEN_SHA512 19
+#define DEBUG
#ifdef DEBUG
-#define DEBUG_OutPut(message); IDMan_CmDebugOutPut(message);
+//#define DEBUG_OutPut(message); IDMan_CmDebugOutPut(message);
+#define DEBUG_OutPut(...); printf(__VA_ARGS__);
#else
#define DEBUG_OutPut(message);
#endif
ついでに、idman/lib/iccard//IDMan_ICSCard.h も修正します。
--- a/bitvisor/idman/lib/iccard/IDMan_ICSCard.h
+++ b/bitvisor/idman/lib/iccard/IDMan_ICSCard.h
@@ -34,6 +34,8 @@
/* 標準ライブラリ関数名 */
+#include <core/printf.h>
+#define DEBUG_OutPut(...); printf(__VA_ARGS__);
#define READBINSIZE 0xF3 // Read Binary 上限値:0xFF(非接触0xF3)
調べたところ、idman/iccard/, idman/pcsc/, idman/pkcs11/ ディレクトリ以下の .c ファイルはすべて standardio/IDMan_StandardIo.h をインクルードしているので、IDMan_StandardIo.h の先頭にまとめて以下を追加します。
--- a/bitvisor/idman/lib/standardio/IDMan_StandardIo.h
+++ b/bitvisor/idman/lib/standardio/IDMan_StandardIo.h
@@ -27,6 +27,9 @@
* POSSIBILITY OF SUCH DAMAGE.
*/
+#include <core/printf.h>
+#define DEBUG_OutPut(...); printf(__VA_ARGS__);
+
/** メンバ名 */
#define CRL "CRL"
#define TRUSTANCHORCERT "TrustAnchorCert"
もういちど make してマシンを再起動し BitVisor 上で CentOS を動かします。起動後に F11 を押して dbgsh で同じように log をみてみます。
F11 pressed.
IDMan_IPInitializeReader.
IDMan_IPInitializeReader start
IDMan_IPInitializeReader PKCS#11 <<本当はここに日本語が表示される>> C_Initialize
IDMan_IPInitializeReader return = -10.
IDMan_IPFinalizeReader.
IDMan_IPFinalizeReader start
IDMan_IPFinalizeReader PKCS#11 <<本当はここに日本語が表示される>> C_Finalize
IDMan_IPFinalizeReader return = -9.
>
上を見るとわかりますが日本語の部分が白く抜けちゃっています。多少下手でも英語でコメントを書いておけばよかったとおもう瞬間です(でも日本語のコメントは貴重)。これ以降では、関数の返り値(エラーコード)を見たりするときに DEBUG_OutPut 関数を使っていきます。これ以降は idman/lib/ 以下なら DEBUG_OutPut 関数でデバッグできるようになりました。
カードリーダの初期化処理を追う(PKCS#11 から PC/SC まで)
さきほどのエラーに話を戻しましょう。IDMan_IPInitializeReader関数@{idman_pkcs11/IDMan_IPCommon.c} から呼び出された C_Initialize 関数は pkcs11/IDMan_PKPkcs11.c にある PKCS#11 の関数でした。この関数の返り値 rv は IDMan_RetJudgement 関数@{idman_pkcs11/IDMan_IPCommon.c} で評価されています。この関数を読むと rv は CKR_DEVICE_ERROR (=0x30) であると予想できます。確認のため、CardEstablishContext(&hContext) の実行直後に DEBUG_OutPut("C_Initialize returns %X¥n", rv); と入れて実行したところ、返り値は 0x30 で間違いないことが確認できました。
さて、C_Initialize関数@{pkcs11/IDMan_PKPkcs11.c} を見ます(以下)。この関数は同じファイルの CardEstablishContext関数@{pkcs11/IDMan_PKPkcs11.c} を読んでいます。最初はここで失敗していると思いました。しかし後述するように CardEstablishContext関数は正常終了していて、次の CardListReader関数に問題があることが分かりました。
ただこのへんは PKCS#11 と PC/SC のつなぎのところで勉強になりますから、いったんCardEstablishContext関数に進んでみましょう。ちなみに、"C_Initialize" のように "C_" ではじまる関数名は PKCS#11(Cryptoki) と呼ばれるICカード(暗号トークン)の標準APIです。また、CardEstablishContext関数はPC/SC標準でカードリーダに接続する最初のAPIです。
/** スロットと接続する。*/
rv = CardEstablishContext(&hContext); <<= この中を見てみる
/** 失敗の場合、*/
if (rv != CKR_OK) <<== 実はここではひっかかっていない
{
/** −戻り値にデバイスエラー(CKR_DEVICE_ERROR)を設定し処理を抜ける。*/
IDMan_StFree(slotTable);
slotTable = CK_NULL_PTR;
IDMan_StFree(sessionTable);
sessionTable = CK_NULL_PTR;
return CKR_DEVICE_ERROR;
}
以下は、同じファイル中の CardEstablishContext関数@{pkcs11/IDMan_PKPkcs11.c} です。CardEstablishContext関数は単なるラッパーで、実際には IDMan_SCardEstablishContext関数@{iccard/IDMan_ICSCard.c} を読んでいます。これ以降は、PKCS#11 API から BitVisor のID管理層を経由して PC/SC ライブラリまで到達するまでのラッパー関数が続きます。
CK_RV CardEstablishContext(CK_ULONG_PTR phContext)
{
CK_LONG scRv = SCARD_S_SUCCESS;
#ifdef CARD_ACCESS
/** ICカード管理層のIDMan_SCardEstablishContextを呼び、スロットと接続する。*/
scRv = IDMan_SCardEstablishContext(phContext); <<= ここの中を見てみる
/** 失敗の場合、*/
if (scRv != SCARD_S_SUCCESS)
{
/** −戻り値に失敗(CKR_FUNCTION_FAILED)を設定し処理を抜ける。*/
return CKR_FUNCTION_FAILED;
}
#endif
/** 戻り値に成功(CKR_OK)を設定し処理を抜ける。*/
return CKR_OK;
}
以下は、IDMan_SCardEstablishContext関数@{iccard/IDMan_ICSCard.c}です。iccard/ ディレクトリは ICカード管理層 とよばれる BitVisor 独自の階層です。これも単なるラッパーになっています。このへんはまわりくどいですがセキュリティのフックポイントにするために入れたのでしょう(きっと)。
下記の sts の値が知りたいですね。下のコードの return の直前に DEBUG_OutPut("return=%d¥n", sts); とコードを入れて実行したところ、返り値は 0 になりました。このことから、IDMan_SCardEstablishContext関数は成功していることがわかりました。この先も正常に実行完了していそうですが、勉強のためさらに奥地(下層)に進みます。
long IDMan_SCardEstablishContext(unsigned long* phContext)
{
long sts=0 ;
/** PC/SCリソースとの接続コンテキストを生成する。 */
sts = SCardEstablishContext(SCARD_SCOPE_SYSTEM, 0x00, 0x00, phContext); << この先へ
/** 関数の戻り値を設定し終了する。 */
return ( sts ) ;
}
以下は SCardEstablishContext@{pcsc/IDMan_PcWinscard.c} の先頭です。やっと PC/SC ライブラリにたどり着きました。PC/SC(Personal Computer/Smart Card) はスマートカードをPCに接続して使う規格です。この呼び出し元でこの関数は正常終了していることを確認したので、これ以上は深入りせずに元のエラーを探す旅に戻りましょう。
LONG SCardEstablishContext(DWORD dwScope, LPCVOID pvReserved1,
LPCVOID pvReserved2, LPSCARDCONTEXT phContext)
{
///コンテキストバッファがNULLの場合、
if (phContext == 0) {
///−パラメータエラー(リターン)
return SCARD_E_INVALID_PARAMETER;
}
...
PKCS#11 の C_Initialize 関数をちゃんと読む
さきほど PKCS#11 API の C_Initialize関数@{pkcs11/IDMan_PKPkcs11.c} の中での CardEstablishContext関数の呼び出しは正常終了していることを確認したので、その先の CardListReaders関数の結果をチェックします。以下のように DEBUG_OutPut関数で値を表示させたところ、返り値は 0 でしたが、dwReaders が 0 になっていました。以下のコードを読むと dwReaders は 0以下だとエラー処理に落ちてしまうようです。dwReaders は利用可能なリーダリストのバッファサイズのようです。ソースコードを追っていきましょう。
--- a/bitvisor/idman/lib/pkcs11/IDMan_PKPkcs11.c
+++ b/bitvisor/idman/lib/pkcs11/IDMan_PKPkcs11.c
@@ -151,6 +152,7 @@ CK_RV C_Initialize(CK_VOID_PTR pReserved)
memset(mszReaders, 0x00, sizeof(mszReaders));
dwReaders = sizeof(mszReaders);
rv = CardListReaders(hContext, mszReaders, &dwReaders); <<= この先へ
+ DEBUG_OutPut("CardListReaders returns %d, dwReaders is %d\n", rv, dwReaders);
/** 失敗の場合、*/
if (rv != CKR_OK || 0 >= dwReaders )
{
以下が、CardListReaders関数@{pkcs11/IDMan_PKCardAccess.c} です。
CK_RV CardListReaders(CK_ULONG hContext, CK_BYTE_PTR pmszReaders, CK_ULONG_PTR pdwReaders)
{
CK_LONG scRv = SCARD_S_SUCCESS;
#ifdef CARD_ACCESS
/** ICカード管理層のIDMan_SCardListReadersを呼び、スロットリスト情報を取得する。*/
scRv = IDMan_SCardListReaders(hContext, (char*)pmszReaders, pdwReaders); <<= この先へ
/** 失敗の場合、*/
if (scRv != SCARD_S_SUCCESS)
{
/** −戻り値に失敗(CKR_FUNCTION_FAILED)を設定し処理を抜ける。*/
return CKR_FUNCTION_FAILED;
}
これもラッパー関数になっていて、さらに 以下の IDMan_SCardListReaders関数@{iccard/IDMan_ICSCard.c} を読んでいます。
long IDMan_SCardListReaders(unsigned long hContext, char* mszReaders, unsigned long* pcchReaders)
{
long sts=0 ;
/** 利用可能なカードリーダのリストを取得する。 */
UL_DW (pcchReaders,
sts = SCardListReaders( hContext, 0x00, mszReaders, pcchReaders ); <<= この先へ
);
/** 関数の戻りを設定し終了する。 */
return( sts ) ;
}
最後に、PC/SCライブラリの SCardListReaders@{pcsc/IDMan_PcWinscard.c} を読んでいます。このSCardListReaders関数は PC/SC でコンテキスト作成後に実行される2番目の API です。この関数呼び出しで、もとの dwReader も pcchReaders という名前の引数でそのまま渡っています。どこかで pcchReaders が 0 になっているはずです。いろいろ試した結果、HPSearchHotPluggables関数でカードリーダのリストを作っているところが怪しことがわかりました(ふぅ)。
LONG SCardListReaders(SCARDCONTEXT hContext, LPCSTR mszGroups,
LPSTR mszReaders, LPDWORD pcchReaders)
{
DWORD dwReadersLen;
///リーダリストのバッファサイズが設定されていない場合、
if (pcchReaders == NULL) {
///−パラメータエラー(リターン)
return SCARD_E_INVALID_PARAMETER;
}
///スマートカードコンテキストがオープンされていない場合、
if (hContext != psContext.hContext || hContext == 0) {
///−無効なハンドル(リターン)
return SCARD_E_INVALID_HANDLE;
}
/**
* リーダ端末接続状態検索 HPSearchHotPluggables() \n
* (このときシステムより現在利用可能なリーダ名を取得)
*/
HPSearchHotPluggables(); <<= ここって本当に実行されてる?
///リーダ名をマルチストリング形式にした場合のサイズを計算
dwReadersLen = IDMan_StStrlen(readerState.readerName);
if (dwReadersLen) dwReadersLen += 2;
以下は HPSearchHotPluggables関数@{pcsc/IDMan_PcHotplug_libusb.c} です。なんだか面白そうな処理にたどり着きましたね。ここで USB 接続のカードリーダを検出しているようです。さっそく DEBUG_OutPut関数を入れて調べてみたところ、USB BUS 検索か USBデバイス 検索で何も見つかっていないことが分かりました。
LONG HPSearchHotPluggables(void)
{
int i;
struct ID_USB_BUS *bus;
struct ID_USB_DEVICE *dev;
char bus_device[BUS_DEVICE_STRSIZE];
/// リーダ接続状態が初期化されていない場合、
if (!isUsbInit) {
///−リーダ接続監視構造体を初期化
readerTracker.driver = NULL;
readerTracker.status = READER_ABSENT; //リーダ接続状態:未接続
readerTracker.bus_device[0] = '\0'; //デバイス名消去
///−USBを初期化します。
IDMan_StUsbInit();
isUsbInit = 1;
}
/// USB BUS検索
IDMan_StUsbFindBusses(); <<= ここで検索できていない
/// USB デバイス検索
IDMan_StUsbFindDevices(); <<= ここでも検索できていない
...
この先は IDMan_StUsbFindBusses関数@{standardio/IDMan_StandardIo.c} がラッパー関数で実体は usb_find_busses関数@{drivers/usb/usb.c} を呼び出していることが分かりました。この usb_find_busses 関数の中で printf してデバッグしたところ、USBバスがひとつも見つかっていませんでした。さてこれはどうしたことでしょうか。
USBコントローラのフックを忘れてた...
この時点ですごく大事なことに気がつきました。BitVisor は準パススルー型のハイパーバイザです。要するに BitVisor側で指定していないデバイスはそのままゲストOSにスルーします。いま使っているカードリーダは USBデバイスですからUSBコントローラの準パススルードライバを指定しないと、そもそも BitVisor では認識されないのでした(ちゃんちゃん)。
ここで Google 先生に「BitVisor USB」とかで検索すると、mmi さんの記事 がヒット。意外とUSBをフックしている人は少なく貴重な情報でした。とにかく以下のように defconfig に入れる設定を探します。
まず ICカードリーダがどのコントローラを使っているかを調べます。以下の結果から XHCI (USB 3.0) ぽいですね。
$ lsusb -t
/: Bus 04.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 5000M
/: Bus 03.Port 1: Dev 1, Class=root_hub, Driver=xhci_hcd/4p, 480M <= XHCI ぽい
|__ Port 1: Dev 2, If 0, Class=Human Interface Device, Driver=usbhid, 12M
|__ Port 2: Dev 3, If 0, Class=Chip/SmartCard, Driver=, 12M
|__ Port 3: Dev 4, If 0, Class=Hub, Driver=hub/7p, 12M
|__ Port 4: Dev 5, If 0, Class=Hub, Driver=hub/7p, 480M
/: Bus 02.Port 1: Dev 1, Class=root_hub, Driver=uhci_hcd/2p, 12M
/: Bus 01.Port 1: Dev 1, Class=root_hub, Driver=ehci-pci/6p, 480M
次に、XHCI コントローラのベンダーIDとデバイスIDを調べます。lspci に -nn オプションをつけて実行します。
$ lspci -nn | grep USB
02:00.0 USB controller [0c03]: VMware USB1.1 UHCI Controller [15ad:0774]
02:02.0 USB controller [0c03]: VMware USB2 EHCI Controller [15ad:0770]
03:00.0 USB controller [0c03]: VMware USB3 xHCI 1.0 Controller [15ad:0779] <= これ
さいごに defconfig に以下を追加しましょう。ここで class_code の値が気になりますが、定義は drivers/usb/xhci.c にあったのでマジックナンバー(おまじない)として入れておきましょう。さて、これでうまくいくんでしょうか。
.pci = "class_code=0c0330, id=15ad:0779, number=0, driver=xhci",
ICカードの初期化(続き)
マシンを再起動してもういちど F11 を押してみます。処理が先にすすみました! ... ですが
HPSearchHotPluggables関数@{pcsc/IDMan_PcHotplug_libusb.c} でやはり止まります。(NTTCOM が定義されていないのになぜか ifndef NTTCOMブロックが実行されていないようですが、時間もあまりないのでここではあまり気にしないことにします)。以下のようにデバッグコードを入れて実行するとベンダーIDが 0x4E6 でプロダクトIDが 0x5116 のカードリーダが検出されていることを確認できました。
--- a/bitvisor/idman/lib/pcsc/IDMan_PcHotplug_libusb.c
+++ b/bitvisor/idman/lib/pcsc/IDMan_PcHotplug_libusb.c
@@ -159,10 +157,9 @@ LONG HPSearchHotPluggables(void)
if (dev->descriptor.idVendor != driverTracker[i].manuID ||
dev->descriptor.idProduct != driverTracker[i].productID) {
///−−−−サポートするUSBデバイス検索ループへ
continue;
}
+ DEBUG_OutPut("Supported device found: idVendor=%d, idProduct=%d\n", dev->descrip
//−−−検出したUSBデバイスをサポートしている場合、
///−−−−検出した対応デバイス名を取得 "dirname:filename"
以下のように調べたところ、今つかっているカードリーダ SCM SCR 3310 が正常に認識されていることが確認できました。とりあえず、ここまでで USBデバイスとしてカードリーダが認識されたところまでたどり着きました。
$ find idman -name \*.h -type f | xargs grep SCM
idman/lib/pcsc/IDMan_PcInfo_plist.h: { 0x04E6, 0x5116, "SCM SCR 3310" },
idman/lib/pcsc/IDMan_PcInfo_plist.h: { 0x04E6, 0x511A, "SCM SCR 3310 NTTCom" },
idman/lib/pcsc/IDMan_PcInfo_plist.h: { 0x04E6, 0x5111, "SCM SCR 331-DI" },
さて、やっとカードリーダが認識されているところまで辿りつきましたが、またまた長くなりすぎましたので #3 に続きます(続く)。
#2 のまとめ
BitVisor のID管理(idman)ライブラリでUSBタイプのカードリーダを認識できました。ここまでの処理は PC/SC API の SCardEstablishContext関数とCardListReaders関数の部分に相当します。また、これらの API は暗号トークンの標準APIである PKCS#11 の C_Initialize API から呼び出されていることを確認できました。