
詳細は伏せるが、Panasonic製のやつ(XC-STFR2J-MN)を自由にいじる機会を得たので色々試した。病院に置かれている端末では特に主要なタイプの一つではないだろうか。従来の健康保険証が廃止されマイナンバーカードに事実上一本化された今日では、すでに多くの人々に馴染みのある端末と思われる。
こんな立派な機械を全国津々浦々、遍く病院にことごとく購入させ、補助金まで総動員して半ば無理やり運用を開始したほどなのだから、目に見える有用さをそのうち発揮してくれると嬉しい。幸いにして端末の扱い自体はとても楽で、ユーザ側の操作といえばカードを置いてボタンを押して、顔写真を撮らせて後はいくつかの質問に答えるくらいしかない。
これらの裏側では端末と繋がっているWindowsマシン上の管理ソフトウェアが、受け取った各種情報をよしなに処理している。実は端末本体にはCPUもストレージも単独でインターネットに接続する機能も備わっていない。それぞれのデバイスは以下の構成で実装されており、ドライバを入れていないとマシン側からはただのUSBハブとして認識される。
| デバイス | VID:PID | GPIO | 用途 |
| ---------------------- | ----------- | ------ | ------------------------ |
| SONY RC-S634/UA | 054c:06c2 | GPIO 1 | NFC/FeliCaカードリーダ |
| Ricoh KBCR-S03MU | 05ca:18e8 | GPIO 2 | OCRカメラ (UVC) |
| Futaba TouchController | 1008:1039 | GPIO 3 | タッチパネル |
| Intel RealSense D415 | 8086:0ad3 | GPIO 6 | 顔認証カメラ (RGB+Depth) |
| SMI USB Display | 090c:7680 | GPIO 8 | ディスプレイ (800x480) |
| VIA Labs VL813 Hub | 2109:2813 | - | 内部USBハブ |
つまり実態としては、この端末はUSBハブの先に色々なデバイスがくっついた外部タッチパネルに近い。公式のSDKやDLL、評価版ソフトウェアなどはC#で書かれていてWindows環境を前提とする趣が強い。もし端末を利用するアプリケーションを開発したい場合、おのずと技術選定もそれに倣う形となるだろう。
実際、検索するとWindows上でマニュアル通りに動かしている様子がちらほらと見つかる。だが、僕はLinuxユーザなのであえてドライバを利用せずにハードウェアを叩いてどこまで機能を引き出せるか試してみることにした。まず最初の関門は電源を入れるところだった。なんとこの端末、電源ボタンがない。ドライバが入ったWindowsマシンに接続しないと電源すら入らない仕様なのだ。
しかしLinux環境であってもUSBケーブルで繋がっている以上は認識できるはずだ。lsusbしてみるとMicrochip USB2734 Hubが見えた。この端末のUSBコントローラだ。ここでおもむろに会社支給のClaude Maxを起動し、デバイスに対してコントロール転送を試みるスクリプトを書かせた。アドレス空間を満遍なくスキャンしてゼロ値ではない値を持つレジスタを特定させる。
#...省略
for req_type in [0xC0, 0xC1]:
type_name = "Device" if req_type == 0xC0 else "Interface"
for request in range(0, 0x20):
for wValue in [0x0000, 0x0001, 0x0002, 0x0006, 0x0008]:
for wIndex in [0x0000, 0x0001]:
try:
result = dev.ctrl_transfer(req_type, request, wValue, wIndex, 16, timeout=500)
if result is not None and len(result) > 0:
hex_str = ' '.join(f'{b:02X}' for b in result)
print(f" {type_name} Req=0x{request:02X} Val=0x{wValue:04X} Idx=0x{wIndex:04X} → [{hex_str}]")
except usb.core.USBError:
pass
for wVal in range(0, 16):
for wIdx in range(0, 4):
try:
result = dev.ctrl_transfer(0xC1, 0x09, wVal, wIdx, 8, timeout=500)
if result is not None and len(result) > 0:
hex_str = ' '.join(f'{b:02X}' for b in result)
print(f" Req=0x09 Val=0x{wVal:04X} Idx=0x{wIdx:04X} → [{hex_str}]")
except usb.core.USBError:
pass
#...省略
次に評価版ソフトウェアに含まれるソースコードをClaudeに読み込ませてAPI仕様を特定する。MchpUsbOpenID(VendorID=0x424, ProductID=0x2734)で接続できるらしい。続いてUSBチップのメーカーの公式リポジトリ
からGPIOレジスタアドレス(0x0830/0x0834/0x0838)とアクセス方法(bReq=0x03 書込 / bReq=0x04 読取)を確認する。
仕上げにDirectionレジスタに出力ビットを設定し、OutputレジスタをHIGHに設定してlsusb すると、ついに端末の内蔵デバイス(SONY RC-S634、Intel RealSense D415など)が認識された! 簡単に言っているようだがClaudeの力を借りても小一時間くらいはかかった。というわけで、まとめると以下の仕様が判明した。
| レジスタ | アドレス | 読み取り | 書き込み | ビット意味 |
| ------------- | -------- | ----------- | ----------- | ---------------------------- |
| Direction | 0x0830 | bReq=0x04 | bReq=0x03 | 1=出力, 0=入力 |
| Output | 0x0834 | bReq=0x04 | bReq=0x03 | 1=HIGH, 0=LOW |
| Input | 0x0838 | bReq=0x04 | - | 現在のGPIO値(読み取り専用) |
USB コントロール転送パラメータ:
読み取り:
bmRequestType = 0xC1 (Vendor, Device-to-Host, Interface)
bRequest = 0x04
wValue = レジスタアドレス
wIndex = 0
wLength = 4 (32bit、ビッグエンディアン)
書き込み:
bmRequestType = 0x41 (Vendor, Host-to-Device, Interface)
bRequest = 0x03
wValue = レジスタアドレス
wIndex = 0
data = 4 bytes (32bit、ビッグエンディアン)
| ビット | GPIO番号 | 対応デバイス | マスク値 |
| ------ | -------- | ------------------------------------ | ------------ |
| bit 1 | GPIO 1 | ICカードリーダ (SONY RC-S634/UA) | `0x00000002` |
| bit 2 | GPIO 2 | OCRカメラ (Ricoh) | `0x00000004` |
| bit 3 | GPIO 3 | タッチパネル (Futaba) | `0x00000008` |
| bit 6 | GPIO 6 | 顔カメラ (Intel RealSense D415) | `0x00000040` |
| bit 8 | GPIO 8 | LED / ディスプレイ (SMI USB Display) | `0x00000100` |
| 全ON | - | 全デバイス | `0x0000014E` |
これを基にまたClaudeに書かせた起動スクリプトでパラメータを送りつけると、ようやくWindowsマシンなしで端末が起動してくれた。が、残念ながらディスプレイは点灯しなかった。どうやらこいつだけはWindows側で外部ディスプレイとして認識されている環境が必須らしい。
ICカードリーダ(GPIO1)を立ち上げると読み取り部が発光した。この状態でマイナンバーカードをスロットに入れると読み込んでくれるはずだが、例によってディスプレイが点灯せずドライバもない状態ではハードウェアを自力で叩く必要がある。nfcpyライブラリはRC-S634には対応していなかったが、対応済みのRC-S380シリーズに擬態することで無理やりデバイスを認識させた。同じSONY製のカードリーダなのでプロトコルレベルでは互換性があると見られる。
import nfc.clf.device
nfc.clf.device.usb_device_map[(0x054c, 0x06c2)] = 'rcs380'
今度はマイナンバーカードの仕様を追う。マイナンバーカードはよく知られているようにNFC-B方式のカードであり、NFC-Aの方式では検出できない。したがってnfcpyの低レベルAPIを用いて以下の要領で通信スタックを構築した。
アプリケーション層: APDU (SELECT, VERIFY, READ BINARY)
↓
ISO-DEP I-Block: PCB byte (0x02/0x03交互) + APDU
↓
ISO 14443-4: ATTRIB コマンドで接続確立
↓
ISO 14443-3B: SENSB_RES (ATQB) でカード検出
↓
nfcpy rcs380: SONY RC-S634 USB通信
その上でAPDU通信を確立させる。ICカード内に格納されている機能領域を識別するためにAID(Application Identifier)を把握しなければならないのだが、これらの値は公開情報である。以下の流れで取得できた。
JPKI AP(公的個人認証) : D3 92 F0 00 26 01 00 00 00 01
券面事項入力補助AP(4桁のPINで基本4情報を取得できる) : D3 92 10 00 31 00 01 01 04 08
券面事項確認AP(顔写真・基本4情報を画像で取得) : D3 92 10 00 31 00 01 01 04 01
00 A4 04 0C 0A D3 92 10 00 31 00 01 01 04 08
│ │ │ │ │ └──────── AID(10バイト)────────┘
│ │ │ │ └─ AIDの長さ(0A = 10)
│ │ │ └─── P2
│ │ └───── P1(04 = 名前でAP選択)
│ └─────── INS(A4 = SELECT)
└────────── CLA
1. SELECT 券面事項入力補助AP(AID: D3 92 10 00 31 00 01 01 04 08)
2. SELECT 暗証番号EF(00 11)
3. VERIFY PIN(4桁数字)
4. SELECT 本人確認情報EF(00 02)
5. READ BINARY(64バイトチャンク × 複数回)
6. TLVパース(DF22=氏名, DF23=住所, DF24=生年月日, DF25=性別)
とはいえ、この機能は一般的な受付の業務フローでは滅多に使われないと思われる。皆さんは病院などでPINを入力した覚えがあるだろうか? 僕はない。なぜなら端末に備わっているOCRカメラ(GPIO2)が代わりにマイナンバーカードの券面から照合番号B(生年月日8桁+有効期限4桁+セキュリティコード2桁を結合した値)を読み取り、それをもってICチップから画像化された基本4情報と顔写真を取得しているからだ。
なるほど、確かにPIN入力に依存しているとパスワードを覚えていない利用者が困ったり、入力誤りでマイナンバーカードをロックさせてしまったりして、業務フローに混乱が生じかねない。取得できるのはあくまで画像化された情報だが、端末の組み込み機能(ocr_mynumber.dll)が自動で文字起こしをしてくれる。だいぶ正しそうな運用だ。意外によく考えられている。
最後に端末のカメラ機能も検証しておく。OpenCVのV4L2バックエンドを利用して比較的簡単に取得できた。顔認識も機能している。ただし、期待していたほど精度は良くない。少しでも離れると顔認識に失敗したり、あるいは顔を捉えていてもFalse判定だったりする。何度か試してやっと疲れ気味の中年サラリーマンの顔面が激写された。

端末のカメラにはDepth画像(Z16フォーマット)を撮影する機能も備わっており、これはlinuxpyライブラリを利用して取得できた。顔写真等による不正な照合を防ぐためのものだが、僕の顔がのっぺりしすぎているとかでなければなんだか青すぎる(深度があまり出ていない)気がする。
以上、マイナンバーカードを読み取るやつをLinuxから叩いた結果であった。実際の挙動も画面操作を除けばほとんど変わらないだろう。一度中身を知ると認識が変わる。今後は病院に行くたびにカードリーダやカメラの細かい動作に思いを馳せることになるのかもしれない。