BitVisor の ID管理(idman)をつかう #3

記事 [#2](https://qiita.com/hrn/items/798abdde4c2843ee7a26) からの続きです。免責事項とテスト環境は前の記事 [#1](https://qiita.com/hrn/items/c11009060974cc286fcd) をよく確認してください。
 
# PC/SC API の SCardListReaders関数が正しく動かない

HPSearchHotPluggables関数＠{pcsc/IDMan_PcHotplug_libusb.c} の RFAddReader(readerTracker.fullName) が SCARD_S_SUCCESS を返さないためエラーになっていることを DEBUG_OutPut文を入れて突き止めました。ちなみに、この時点で readerTracker.fullName の値は "SCM SCR 3310 ()" と取得できています。リーダ名の文字列は pcsc/IDMan_PcInfo_plist.h で定義されていますが、カードリーダのベンダIDとプロダクトIDが取れていることから、最低限の USB 接続での通信はできているようです。

さらに追っていくと、RFAddReader関数＠{pcsc/IDMan_PcReaderfactory.c}の中の IFDHCreateChannel関数＠{ccid/IDMan_CcIfdhandler.c} でエラーが出ていることを突き止めました。

だんだん下層に降りてきましたね。CCID は Chip card interface device の略です。PC/SC はカードリーダに接続したり切断したりするAPIを提供します。CCID は PC/SC のさらに下で USB 接続とのあいだのドライバの役割を果たすようです。IFD というのは Interface Device の略です。

|  処理層   | 対応する BitVisor のディレクトリ 階層 |
|  ---   | ---                       | 
|PKCS#11 | idman/lib/pkcs11          | 
|PC/SC   | idman/lib/pcsc            |
|CCID    | idman/lib/ccid            |
|USB     | drivers/udb               |

さて、以下の IFDHCreateChannel関数＠{ccid/IDMan_CcIfdhandler.c} を確認してみます。これまでどおり、処理の途中に DEBUG_OutPut関数でデバッグプリントを入れて実行したところ、OpenUSB関数は TRUE を返してエラーにはなっていませんが、その後の IFDHICCPresence(Lun) のチェックでエラーになっていることが分かりました（なぜ同じ条件を３回検査しているかはちょっと分かりませんでしたが時間もないのであまり気にしないことにします）。次に進みましょう。

```
RESPONSECODE IFDHCreateChannel( DWORD Lun, DWORD Channel )
{
        int UnitNum = (Lun & 0xFFFF0000) >> 16;

        ///論理ユニット番号 \e Lun が間違っている場合、
        if (UnitNum >= READER_NUMBER_MAX) {
                ///−通信エラー（リターン）
                return IFD_COMMUNICATION_ERROR;
        }

        ///USBポートのオープン OpenUSB()
        if (OpenUSB(gReaderMng, &gReaderMng[UnitNum]) != TRUE) {
                ///−USBポートのオープンに失敗、通信エラー（リターン）
                return IFD_COMMUNICATION_ERROR;
        } else
        DEBUG_OutPut("Succeed to OpenUSB\n");
        if ((IFDHICCPresence(Lun) == IFD_COMMUNICATION_ERROR)
                && (IFDHICCPresence(Lun) == IFD_COMMUNICATION_ERROR)
                && (IFDHICCPresence(Lun) == IFD_COMMUNICATION_ERROR))　 <<=　ここ
        {
                CloseUSB(&gReaderMng[UnitNum]);
                return IFD_COMMUNICATION_ERROR; <<= ここに落ちる
        }

        ///正常終了（リターン）
        return IFD_SUCCESS;
}
```

さらに、同じファイル にある以下の IFDHICCPresence関数＠{ccid/IDMan_CcIfdhandler.c} を見てみます。この関数が IFD_COMMUNICATION_ERROR を返しています。この中に再び DEBUG_OutPut関数でどこまで進んでいるか確認したところ、今度は GetCardStatus関数＠{ccid/IDMan_CcReaderControl.c} がエラーを返していることが分かりました。

```
RESPONSECODE IFDHICCPresence( DWORD Lun )
{
        int                             iRet;
        int                             UnitNum = (Lun & 0xFFFF0000) >> 16;
        uchar                   SlotNum = Lun & 0x0000FFFF;
        unsigned int    ReadTimeOutBuffer;

        ///論理ユニット番号 \e Lun が間違っている場合、
        if (UnitNum >= READER_NUMBER_MAX || SlotNum >= SLOTS_NUMBER_MAX) {
                ///−通信エラー（リターン）
                return IFD_COMMUNICATION_ERROR;
        }
        ReadTimeOutBuffer = gReaderMng[UnitNum].ccid_mng.readTimeout;
        gReaderMng[UnitNum].ccid_mng.readTimeout = DEFAULT_READ_TIMEOUT;

        ///カード状態を取得します GetCardStatus() 
        iRet = GetCardStatus(&gReaderMng[UnitNum], SlotNum); <<= ここ

        gReaderMng[UnitNum].ccid_mng.readTimeout = ReadTimeOutBuffer;

        ///エラーの場合、
        if (iRet < 0) {
                ///通信エラー（リターン）
                return IFD_COMMUNICATION_ERROR;
        }
```

さらに、GetCardStatus関数＠{ccid/IDMan_CcReaderControl.c} の中の WriteUSB関数で書き込みエラーが発生していました。WriteUSB関数{ccid/IDMan_CcUsb.c} はラッパー関数で、
IDMan_StUsbBulkWrite関数を読んでいますが、これは standardio/IDMan_StandardIo.h で usb_bulk_write として定義されています。以下は usb_bulk_write関数＠{drivers/usb/usb.c} です。ここにも printf を入れてどこまで進んでいるか試したところ submit_bulk がエラーを返していました。

```
usb_bulk_write(usb_dev_handle *dev, int ep, char *bytes, int size,
               int timeout)
{
        struct usb_request_block *urb; 
        struct usb_endpoint_descriptor *epdesc;
        u64 start;
        int ret;

        start = get_time();

        epdesc = get_edesc_by_address(dev->device, ep);
        urb = dev->host->op->
                submit_bulk(dev->host, dev->device, epdesc, 
                            bytes, size, NULL, NULL, 0 /* no IOC */); <<= ここ
        if (!urb)
                return -1;

        ret = _usb_async_receive(dev, urb, NULL, 0, start, timeout * 1000);

        dev->host->op->deactivate_urb(dev->host, urb);

        return ret;
}
```

エラーが出ていた submit_bulk 関数は uhci (USB 1.x)、ehci (USB 2.0)、そして xhci (USB 3.0) の３種類のコントローラごとにドライバが分かれているようです。たとえば、以下のように drivers/usb/xhci_trans.c (USB 3.0) と drivers/usb/ehci_trans.c (USB 2.0) は return NULL を返すだけになっています（私が使っているブランチが古いだけだったらごめんなさい）。記事 [#2](https://qiita.com/hrn/items/798abdde4c2843ee7a26) では xhci コントローラ (USB 3.0) の準パススルードライバを設定したため、以下の xhci_submit_bulk関数が NULL を返していたことが確認できました。

```
struct usb_request_block *
xhci_submit_bulk (struct usb_host *host,
                  struct usb_device *device,
                  struct usb_endpoint_descriptor *epdesc,
                  void *data, u16 size,
                  int (*callback) (struct usb_host *,
                                   struct usb_request_block *, void *),
                  void *arg, int ioc)
{
        return NULL; <<= ここ
}
```

さらに調べてみると drivers/usb/uhci_trans.c (USB 1.x) にはちゃんと USBバルク転送処理が記述されていました。こっちを使わないと GetCardStatus関数＠{ccid/IDMan_CcReaderControl.c} を実行できませんから、UHCI (USB 1.x) の準パススルードライバを利用するように defconfig の設定を書き換えます。

# USBコントローラの準パススルードライバを UHCI に変更

ふたたび lspci コマンドを使い、今回は UHCI コントローラのベンダーIDとデバイスIDを調べます。

```
$ 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] 
```

class_code を drivers/usb で grep したところ、UHCI の class_code は 0c0300 であることがわかりました。

```
$ grep class_code drivers/usb/*.c
drivers/usb/ehci.c:	.device		= "class_code=0c0320",
drivers/usb/uhci.c:	.device		= "class_code=0c0300",
drivers/usb/xhci.c:	.device		= "class_code=0c0330",
```

さいごに defconfig に以下を追加しましょう。

```
.pci = "class_code=0c0300, id=15ad:0774, number=0, driver=uhci",
```

# やっとカードリーダの初期化に成功

そのまま再起動してもカードリーダが XHCI コントローラを使ってしまい、UHCI コントローラの準パススルードライバの処理が実行されませんでした。今回は VMware Fusion 11.5 を使っているので、以下のように無理やり USB 1.1 (UHCI) を使うように仮想ハードウェアの設定を変更します。

<img width="625" alt="vmware.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/550398/cbbf4144-ab5b-bd45-5e72-f32b2f5bf39c.png">

これでマシンを再起動してみたところ UHCI の準パススルードライバが動作し、これまで動作しなかった部分がすべて動きました。ちょっと無理やりですがとりあえず次に進みます（ふぅ...）。

# マイナンバーカードの ATR 取得

やっとマイナンバーカードの ATR (Answer To Reset) が正しく取れているかを確認できます。まず、ATR を使っているソースコードを探しましょう。このなかで特に IDMan_CcAtr.c にある SetATR関数に注目して処理を追います。SetATR関数 は ATR を解析する関数と書いてありますから ICカードを PowerOn したときに必ず呼ばれるはずです。

```
$ find idman -name \*.c | xargs grep ATR | sed -e 's/:.*$//' | uniq
idman/lib/ccid/IDMan_CcAtr.c
idman/lib/ccid/IDMan_CcProtocol.c
idman/lib/ccid/IDMan_CcIfdhandler.c
idman/lib/ccid/IDMan_CcReaderControl.c
idman/lib/pcsc/IDMan_PcEventhandler.c
idman/lib/pcsc/IDMan_PcProthandler.c
idman/lib/pcsc/IDMan_PcWinscard.c
idman/lib/pkcs11/IDMan_PKCardAccess.c
```

処理は以下のように呼ばれていることを調べました。SetATR関数＠{ccid/IDMan_CcAtr.c} -> IFDHSetProtocolParameters関数＠{pcsc/IDMan_PcProthandler.c:} -> PHSetProtocol関数＠{pcsc/IDMan_PcProthandler.c} ->  関数＠{pcsc/IDMan_PcWinscard.c} -> 最後に SCardConnect関数＠{pcsc/IDMan_PcWinscard.c} から呼ばれています。

以下は SCardConnect関数＠{pcsc/IDMan_PcWinscard.c} です。この SCardConnect関数は PC/SC API のうち、コネクションを作る大事な関数です。

```
LONG SCardConnect(SCARDCONTEXT hContext, LPCSTR szReader,
        DWORD dwShareMode, DWORD dwPreferredProtocols, LPSCARDHANDLE phCard,
        LPDWORD pdwActiveProtocol)
{
        PREADER_CONTEXT rContext = NULL;
        DWORD dwStatus;

        /*
         * Check for NULL parameters
         */
        ///ハンドル \e phCard またはプロトコル \e pdwActiveProtocol のバッファが指定されていない場合、
        if (phCard == NULL || pdwActiveProtocol == NULL) {
                ///−パラメータエラー（リターン）
                return SCARD_E_INVALID_PARAMETER;
        }

        ///ハンドル \e phCard を初期化
        *phCard = 0;

        ///リーダ名 \e szReader が指定されていない場合、
        if (szReader == NULL && *szReader==0) {
                ///−不明なリーダ エラー（リターン）
                return SCARD_E_UNKNOWN_READER;
        }
       ...
```

PC/SC の処理については日本語で gebo さんが [この記事](https://qiita.com/gebo/items/6a334b5453817a587683) にフローチャートでわかりやすくまとめてくださっています。この記事ではこれまでの段階で PC/SC API のうち、SCardEstablishContext関数 と SCardListReaders関数まで正常に実行できました。

もう忘れてしまったかもしれませんが、私たちは記事 [#1](https://qiita.com/hrn/items/c11009060974cc286fcd) の最初でこれ以降の処理をコメントアウトしていました。この後に、本来実行されるべき PC/SC API の SCardConnect関数が実行されるようにソースコードを直します。

長くなりすぎたので、急ぎます。以下のように [#1](https://qiita.com/hrn/items/c11009060974cc286fcd)  でコメントアウトした core/io_iohook.c のコードブロックのうち IDMan_IPInitialize関数のコメントアウトだけを外しこの関数を実行させるようにします。

```
--- a/bitvisor/core/io_iohook.c
+++ b/bitvisor/core/io_iohook.c
@@ -106,9 +106,9 @@ 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_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);
```

次に、idman_pkcs11/IDMan_IPCommon.c を以下のように削除します。コメントアウトでもいいんですが diff がわかりにくくなるので、以下では削除したものを示します。削除した部分には BitVisorAP依存の処理が含まれます。今回は BitVisorAP ではなくマイナンバーカードを使うのでその部分は削除します。

```
diff --git a/bitvisor/idman/lib/idman_pkcs11/IDMan_IPCommon.c b/bitvisor/idman/lib/idman_pkcs11/IDMan_IPCommon.c
index 124b3cc..10c48da 100644
--- a/bitvisor/idman/lib/idman_pkcs11/IDMan_IPCommon.c
+++ b/bitvisor/idman/lib/idman_pkcs11/IDMan_IPCommon.c
@@ -193,30 +193,6 @@ int IDMan_IPInitialize( char* PIN,unsigned long int* SessionHandle)
                return ret;
        }
 
-       /**セッションの確立を行う。 */
-       rv = C_OpenSession(slotID[0],CKS_RW_USER_FUNCTIONS,0x00,0x00,(CK_SESSION_HANDLE_PTR)SessionHandle);
-       /**セッション確立エラーの場合、 */
-       if (rv != CKR_OK) 
-       {
-               /**−異常リターンする。 */
-               DEBUG_OutPut("IDMan_IPInitialize セッションの確立 エラー C_OpenSession\n");
-               ret = IDMan_IPRetJudgment(rv);
-               return ret;
-       }
-
-       /**カードにログインを行う。 */
-       PINLen = (CK_ULONG) strlen((char*)PIN);
-       rv = C_Login((*SessionHandle),CKU_USER,(CK_UTF8CHAR_PTR)PIN,PINLen);
-       /**カードログインエラーの場合、 */
-       if (rv != CKR_OK) 
-       {
-               /**−異常リターンする。 */
-               DEBUG_OutPut("IDMan_IPInitialize カードログイン エラー C_Login\n");
-               C_CloseSession((*SessionHandle));
-               ret = IDMan_IPRetJudgment(rv);
-               return ret;
-       }
-
        /**正常リターンする。 */
        DEBUG_OutPut("IDMan_IPInitialize end\n");
        return RET_IPOK;
```

これで SCardConnect関数が呼ばれて ATR が取得できる状態になったはずです。さいごに GetCardStatus関数＠{pkcs11/IDMan_PKCardAccess.c} に以下のコードを追加して ATR をプリントさせましょう。

```
--- a/bitvisor/idman/lib/pkcs11/IDMan_PKCardAccess.c
+++ b/bitvisor/idman/lib/pkcs11/IDMan_PKCardAccess.c
@@ -237,6 +237,11 @@ CK_RV CardStatus(CK_ULONG hCard, CK_BYTE_PTR reader)
        dwAtrLen = sizeof(pbAtr);
        tmpReaderLen = MAX_READERNAME;
        scRv = IDMan_SCardStatus(hCard, (char*)reader, &tmpReaderLen, &dwState, &dwProt, pbAtr, &dwAtrLen);
+
+        int i;
+        printf("My number card ATR:  ");
+       for(i=0; i<dwAtrLen; i++) printf("%02X ", pbAtr[i]);
+
        /** 失敗の場合、*/
        if (scRv != SCARD_S_SUCCESS)
        {
```

最後に再起動して F11 を押し、dbgsh で log を確認しましょう。

```
My number card ATR:  3B DA 13 FF 81 31 FB 46 80 12 39 2F 31 C1 73 C6 01 C0 3B 
```

おめでとうございます！無事に BitVisor からID管理（idman）ライブラリだけをつかってマイナンバーカードにアクセスし、 ATR を取得できました。ここまで来れたらあとは PC/SC API の SCardTransmit関数で APDU を送受信する処理を書くだけでマイナンバーカードを使った認証や電子署名ができるはずです。

# さいごに

BitVisor （ハイパーバイザ）から OS の力を借りずにマイナンバーカードにアクセスして ATR を取得することができました。市販のカードリーダと誰でも持っているマイナンバーカードだけで実験したことで BitVisor のID管理（idman)ライブラリが汎用性の高いものであることを示せたと思います。

BitVisor の ID管理（idman）ライブラリ の良いと思ったところは以下の３点です。

- ICカードリーダとICカードにアクセスするためのオープンソースライブラリ（Linux の pcsc-lite, ccid, libusb ）が glibc などのライブラリなしでコンパイルできる。組み込みシステムでも使えそう。
- 親切な日本語のコメントが書いてある。 doxygen に対応している。日本人が PKCS#11, PC/SC, CCID の仕組みを学習するのに役立つ。
- ハイパーバイザ層でICカードが使える。OS の脆弱性に影響されにくい。セキュリティを強制する階層を、アプリケーション層とOS層より下にさらにひとつ増やすことができる。

National ID card としてのマイナンバーカードは e-Tax くらいしか活用されていなくて、その e-Tax ですら IDとパスワードだけでログインできるようになってきている現状です。そんな今こそマイナンバーカードのような暗号トークンの良さを実感できるサービスやソフトウェアが求められているんじゃないでしょうか（おしまい）。