マイナ免許証からNFCコマンドを叩いてデータを読み取ってみる

マイナ免許証からのデータ読み出しにトライしました。

2025/3/24 マイナンバーカードに免許情報を搭載するマイナ免許証がスタートしました。

https://www.npa.go.jp/bureau/traffic/r4kaisei_main.html

NFCコマンドが記載された公式仕様書が公開されていますから、早速NFCカードリーダを使って読み取りにトライしました。

カードリーダは SONY RC-S660/S を使用し、Arduino で制御しています。

本記事では、カードとのやり取り内容のみを記載しています。

## 公式仕様書V10で不明だった確認事項ポイント
仕様書通りコマンドを叩けば読み取っていけますが、不明点を実機確認したところがあります。
+ 通信確立したら インスタンス-AID をSELECT FILE する
実行モジュール系のAIDはそもそも SELECT FILE できない
+ VERIFY は IEF01(EFID = "0006", 短縮EFID = 6 ) を指定して実行する
**別添3-1 2章記載のサンプルコマンドから変更が必要** なため注意

## 公式仕様書V10の入手

マイナ免許証の仕様書は、『運転免許証及び運転免許証作成システム等仕様書(仕様書バージョン番号:010)』として警察庁webサイトで公開されています。

「免許証　仕様書V10」で検索し、「原議保存期間 5年(令和12年3月31日まで)」 menkyo20240719_145.pdf　を入手します。

## 読み出しの正常系手順と有効期限読み取り例

### 手順

正常系の手順例を示します。
正常系ですから、暗証番号間違い、ロックされている、ファイルが見つからない などは省略しています。

```mermaid
graph TD
    A(通信確立) --> B[1.免許証APの選択]
    B --> C[2.暗証番号設定有無の確認]
    C --> D{暗証番号設定あり？}
    D-->|設定あり| E[3.保有者設定PINで照合]
    D-->|未設定| F[3.DPINで照合]
    E-->G[4.免許情報 WEF の選択]
    F-->G[4.免許情報 WEF の選択]
    G-->H[5.目的のデータを探す::TAG,LEN読み取り]
    H-->I{目的のTAG？}
    I-->|目的のTAG| J[6.目的のデータを読む]
    I-->|目的のTAGではない| K[LEN分オフセット進める]
    K-->H
    J-->L(読取終了)
```

補足：1. 免許証APの選択
　インスタンス-AIDを SELECT FILE する
補足：2. 暗証番号設定有無の確認
　WEF01 を読むと、VALUE = "00"(未設定) or "01"(設定あり) が読める
補足：3. 暗証番号の照合
　 VERIFY は、短縮EFID = 6 を指定して実行する
 　手順2で暗証番号設定 あり の場合は、設定した暗証番号を JIS X 0201 文字コードでエンコードして渡す
　手順2で暗証番号未設定の場合は、DPIN ("2A, 2A, 2A, 2A" , "****"の JIS X 0201 文字コード) を渡す

### 読み出し例

下記のマイナ免許証(公式読み取りアプリのスクショ)から、有効期限(TAG = "C5")を読み取ってみます。

令和10年4月24日を読めることが期待値です。

![Screenshot_20250330-171542a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/210035/82380715-8182-4923-b5cb-6475412f4b25.png)

~~~有効期限読み出し.txt
// 1. 免許証APの選択
// SELECT FILE で インスタンス-AID を選択する

Tx : 00 A4 04 0C 10 A0 00 00 02 31 06 00 00 00 00 00 00 00 00 00 00 
Rx : 90 00  //正常終了

//2. 暗証番号設定有無の確認
//READ BINARY で WEF01(EFID = "001A") を読み取る
//長さ固定・一つのファイルしかないWEFのため、SELECT FILE でカレントEFにせず、
//短縮EF ("1A") を指定して READ BINARY 実行すると効率的

Tx : 00 B0 9A 00 03
Rx : C1 01 01 90 00
   //TAG "C1" , LEN "01" , VALUE "01"(設定あり) , "90 00"(正常終了)

//3. 暗証番号の照合
//VERIFY で IEF01(EFID = "0006") を指定して照合実行

Tx : 00 20 00 86 04 3X 3X 3X 3X  //X は指定した暗証番号 JIS X 0201 形式で渡す
Rx : 90 00  //正常終了

//4. 免許情報 WEF の選択 
//SELECT FILE で WEF02(EFID = "001B") を選択
//手順2とは異なり、不定長のデータを読んでいくためカレントEFにしておくと便利

Tx : 00 A4 02 0C 02 00 1B
Rx : 90 00  //正常終了

//5. 目的のデータを探す (例:有効期限 TAG = "C5" を探す)
//カレントファイル用の 15bit オフセット指定の READ BINARY で TAG, LEN を読み取り、
//目的の TAG でなければ LEN 分オフセットを進めて飛ばす操作を繰り返す
//読み出し長さ制限は Le を使う

Tx : 00 B0 00 00 02
Rx : C2 00 90 00
   //TAG "C2"(運転履歴情報記録年月日) , LEN "00" (Valueなし) , "90 00"(正常終了) : 飛ばす

Tx : 00 B0 00 02 02
Rx : C3 00 90 00
   //TAG "C3"(運転者区分(経歴情報)) , LEN "00" (Valueなし) , "90 00"(正常終了) : 飛ばす

Tx : 00 B0 00 04 02
Rx : C4 06 90 00
   //TAG "C4"(免許証の色区分) , LEN "06" (Value 6バイト) , "90 00"(正常終了) : 飛ばす

Tx : 00 B0 00 0C 02
Rx : C5 07 90 00
   //TAG "C5"(免許情報記録の有効期間の末日) , LEN "07" (Value 7バイト) , "90 00"(正常終了) : 読み取る

//6. 目的のデータを読む
//手順5で目的のTAGが見つかったら 取得した LEN を Le に指定した READ BINARY で目的のデータを読み取る

Tx : 00 B0 00 0E 07
Rx : 35 31 30 30 34 32 34 90 00
   //JIS X 0201 表記で 5(令和を意味する) 10 年 04 月 24 日 ,  "90 00"(正常終了) 
~~~

以上、有効期限を読み取ることができました。

## コマンドを色々試してみる

有効期限読み出しができましたから、仕様書を見ながらコマンドを色々試してみます。

### SELECT FILE :: インスタンスAID以外を選択するとどうなる？

アプリケーション識別子 AID は、ISOに従って国際的に登録されており、他のカードと被りません。

従来免許証では、タッチしたカードが免許証であるか確認するために、仕様書既定のAIDを試しに SELECT FILE してみて全て "90 00"(正常終了) が返ってくるか？で判定するプログラムを書いていました。

従来免許証では、仕様書V10 別紙2 p.5-2, 3(2)イ アプリケーション識別子(AID) に記載の3つのAID全てについて SELECT FILE すると "90 00"(正常終了) が返ってきました。

マイナ免許証でも同様に判定ロジックを書きたいため、どのような動作になるか確認しておきます。

マイナ免許証でも、仕様書V10 別紙3 p.3-2, 3(2)イ アプリケーション識別子(AID) には3つのAIDが載っています。
仕様書V10 別紙1 p.1-2, 2 略語・用語及び定義(概要) に説明があります。

> + ELF-AID : カードAPのバイナリファイルを管理するAID
> + 実行モジュール-AID : ELF-AIDの中にあるプログラムの開始点を指すAID
> + インスタンス-AID : 記録データの読出し時にSELECT-FILEコマンドを使用する時のAID

SELECT FILE を使用する際には、前提として インスタンス-AID を SELECT FILE しておく必要があり、残りは実行ファイルと分かります。

ELFはExecutable and Linkable Formatの略でその名の通り実行バイナリファイルのようです。

https://ja.wikipedia.org/wiki/Executable_and_Linkable_Format

インスタンス-AID の SELECT FILE は有効期限読み出しで確認しましたが、バイナリファイルを SELECT FILE するとどうなるでしょうか。
~~~3つのAIDをお試しSELECT FILE.txt
// ELF-AID を SELECT
Tx : 00 A4 04 0C 10 A0 00 00 02 31 04 00 00 00 00 00 00 00 00 00 00
Rx : 6A 82  //EF-IDが一致しない(別添3-1)・アクセス対象ファイルがない(別添2-1)

// 実行モジュール-AID を SELECT
Tx : 00 A4 04 0C 10 A0 00 00 02 31 05 00 00 00 00 00 00 00 00 00 00 
Rx : 6A 82  //EF-IDが一致しない(別添3-1)・アクセス対象ファイルがない(別添2-1)

// インスタンス-AID を SELECT
Tx : 00 A4 04 0C 10 A0 00 00 02 31 06 00 00 00 00 00 00 00 00 00 00 
Rx : 90 00  //正常終了
~~~

実行ファイルを SELECT FILE しようとする時は、応答として "6A 82" が返ってきました。

マイナ免許用のコマンド解説 別添3-1 では下記になります。

> EF-IDが一致しない

いまいちわからないため従来免許用のコマンド解説  別添2-1 を見てみます。

SELECT FILE の上位規格は従来もマイナも同じ ISO ですから、従来免許用のコマンド解説も参照できます。

> アクセス対象ファイルがない

ということで、インスタンス-AID しか SELECT FILE できなくなっているようです。

マイナ免許証ではタッチされたカードが免許証か判定するためのお試し SELECT FILE ロジックは、インスタンス-AID のみを対象にする必要があるとわかりました。

### VERIFY::残り照合許容回数確認 サンプルコマンドが動かない...

VERIFY コマンドは、ボディー(暗証番号データ)なしで実行すると残り照合許容回数を取得できる機能があるようです。

実行しても照合可能回数を消費しないため、お試しに最適です。

マイナ免許証を入手する前、事前に仕様書を読んでいて最も不明だったのがこの VERIFY です。
EFID = "0006" とサンプルコマンドが矛盾しています。

別紙3 3章 (2) オ では下記の記述があります。

> (ア) 暗証番号 (PIN)
>      格納ファイル名: 　IEF01(EFID="0006")

一方、別添3-1 2章 (5) には下記の記載があります。

> [再試行可能回数の取得]
> CLA:   00h
> INS:   20h
> P1 :   00h
> P2 :   82h

P2を読み解くと、EFID = "0002" が指定されています。

まずは 別添3-1 のサンプルコマンドを実行してみます。

期待値は "63 CX" で、X が残り照合回数です。

~~~別添3-1 のサンプルコマンドが動かない.txt
//前提 インスタンス-AIDが SELECT FILE され、VERIFY は未実行(初期状態)
Tx : 00 20 00 82 
Rx : 6A 82 
~~~

エラーコードが返ってきました。

別添3-1では解説のないコードですが、SELECT FILE同様、従来免許の方に記載があります。

別添2-1　4章 (2) を見ると

> 6A 82 : アクセス対象ファイルがない

とのことです。

従来免許側の仕様 別添 2-1 4章 (2) に、 VERIFY コマンドの詳しい解説があります。
EFID = "0006" にして再トライしてみます。

・クラスバイト CLA

下位ニブルに論理チャンネル番号を指定する必要があります。
従来免許では 別添2-1 2章(5) で チャンネル0 が指定されていますから "00" 固定です。
マイナ免許ではしていないですが、SELECT FILE のサンプルコマンド CLA = "00"で動作しましたから同様に チャンネル0 としてみます。

**CLA = "00"** (サンプルから変更なし)

・命令バイト INS

従来・マイナにかかわらず、上位規格の ISO で決まっており共通です。
VERIFY を命令しますから

**INS = "20"** (サンプルから変更なし)

・パラメタバイト P1-P2

P1については、別添 2-1 4章 (2) ウ にて

> "00"固定

とあります。

**P1 = "00"** (サンプルから変更なし)

とします。

P2については下記が指定されます。

> P2コーディング
> | b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | 意味 |
> |---|---|---|---|---|---|---|---|---|
> | 1 | 0 | 0 |   |   |   |   |   | 固定 |
> |   |   |   | 0 | 0 | 0 | 0 | 0 | カレントEF指定 |
> |   |   |   | 0 | 0 | 0 | 0 | 1 |  |
> |   |   |   |   |   | ～ |   |   | 短縮EF識別子指定 |
> |   |   |   | 1 | 1 | 1 | 1 | 0 |  |

"短縮EF" についても、従来免許側の仕様 別添2-1 2章 (3) イ に解説があります。

> 全てのEFは、2バイト( "0000" ～ "FFFF" )で構成されたEF識別子によって選択される。特に、EF識別子が "0001" から "001E" の場合には、5ビットで符号化した1から30までの短縮EF識別子によって選択される。

今回は EFID = "0006" を選択したいですから、短縮EFに対応しており、

短縮EFID = 6

5ビットのバイナリで表すと

短縮EFID = 0b0110 

上記の表に当てはめると

| b8 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | 意味 |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0 |   |   |   |   |   | 固定 |
|   |   |   | 0 | 0 | 1 | 1 | 0 | 短縮IFID = 6 |

よって

P2 = 0b10000110 

16進数で表すと

**P2 = "86"** (サンプルに対し短縮EFIDを2から6に変更) 

以上でコマンドの組み立てが完了しました。

組み立てたコマンドを実行してみます。

期待値は "63 CX" で、X が残り照合回数です。


~~~EFID = "0006"に修正.txt
//前提 インスタンス-AIDが SELECT FILE され、VERIFY は未実行(初期状態)
Tx : 00 20 00 86 
Rx : 63 CA 
~~~

"A" は 10進数にすると 10 ですから、「残り10回」です。

試行許容回数の仕様は 別紙3 3章 (2) オ (ア) に記述されており

> 試行許容回数は10回とする。


「10回」を読み取ることができました。成功です！

### READ BINARY::WEF02を全部読んでみる

続いて READ BINARY ってWEF02を全部読んでみます。

有効期限だけでは分からなかった日本語情報読み取りと、2バイトTAG・2バイトLENの動きを確認します。

カードリーダやマイコンプログラムのバッファの関係上、READ BINARY の Le = 0xC8 を設定して 200 バイトずつ区切って読みだしています。

読み出し結果は、Tag-Length-Value フォーマット1セットずつ改行します。

~~~WEF02をREAD BINARY.txt
//前提 VERIFY 成功状態 かつ WEF02 が カレントEFになっている

Tx : 00 B0 00 00 C8 //オフセット 0 バイトから開始 200 バイト READ BINARY

Rx : 
C2 00 //運転経歴情報記録年月日
C3 00 //運転者区分(経歴情報)
C4 06 4D 25 4E 49 00 00    //免許証の色区分
C5 07 35 31 30 30 34 32 34 //免許情報記録の有効期間の末日
C6 06 34 63 36 40 45 79    //免許の条件1
C7 32 3D 60 43 66 37 3F 24 47 31 3F 45 3E 24 47 24 2D 24 6B 3D 60 43 66 37 3F 3C 56 24 4F 3D 60 43 66 37 3F 3C 56 21 4A 23 35 23 74 21 4B 24 4B 38 42 24 6B //免許の条件2
C8 00 //免許の条件3
C9 00 //免許の条件4
CA 00 //欠字1
CB 00 //欠字2
CC 00 //免許の条件5
      // 省略:免許の条件6～12
D4 00 //欠字3
      // 省略:欠字4～5
D7 00 //備考1
      // 省略:備考2～8
DF 00 //予備1
      // 省略:予備2～8
E7 0C 3X 3X 3X 3X 3X 3X 3X 3X 3X 3X 3X 3X //免許情報記録番号
   //いわゆる12桁の免許証番号のため伏字 JIS X 0201 で保存
E8 00 //運転経歴情報記録番号
E9 00 //免許の年月日(二・小・原)
EA 07 34 32 34 30 38 30 33 //免許の年月日(他)
   //JIS X 0201 表記で 4(平成を意味する) 24 年 08 月 03 日 
EB 00    //免許の年月日(二種)
EC 01 00 //免許の種類(大型) HEX で 0x00 なし
ED 01 00 //免許の種類(普通) HEX で 0x00 なし
         //省略:バイクなど
F4 01 00 //免許の種類(大型二種) HEX で 0x00 なし
F5       //中途半端なところで切れた TAG "F5"
90 00    //正常終了

//カードリーダの内部バッファが足らないので分割

Tx : 00 B0 00 C7 C8 //オフセット 199 バイトから開始 200 バイト READ BINARY
Rx :
F5 01 00 //免許の種類(普通二種) HEX で 0x00:なし
F6 01 00 //免許の種類(大特二種) HEX で 0x00:なし 
F7 01 00 //免許の種類(けん引二種) HEX で 0x00:なし 
F8 01 00 //免許の種類(中型) HEX で 0x00:なし 
F9 01 00 //免許の種類(中型二種) HEX で 0x00:なし 
FA 01 01 //免許の種類(準中型) HEX で 0x01:有 
FB 00    //RFU1
         //省略:RFU2～4
FF 00    //RFU5
01 00 00 //RFU6 ※ここから2バイトタグ TAG "0100"
        //省略:RFU7～12
01 07 82 07 BB //写真 ※長いので2バイトLEN "82 07 BB"
               //"82"は識別フラグ 長さ "07 BB" (1979)バイト
   00 00 00 0C 6A 50 20 20 0D //後略　写真データ 
90 00   //正常終了
~~~


#### 日本語を表示する

TAG "C4" (免許証の色区分)、"C6" (免許の条件1)、 "C7" (免許の条件2) が JIS X 0208 データとして読み取れましたので日本語表示をしてみました。

読み取ったJIS X 0208 をそのまま txt ファイルなどに保管しても、Windowsで使う文字コードと違うため文字化けして読めません。

UTF-8 に変換しようなどとすると大変なため、全バイトに 0x80 を足すだけで済むお手軽な EUC-JP を変換先としました。

ちょっとお試しですから、Stringをスペース区切りで・・・といったコードは書かず、サクッとエディタの置換で スペース を ", 0x" に置換して配列にし、簡単なコードで変換しました。

~~~toEucJp.cpp
using namespace std;

#include <iostream>
#include <fstream>

int main()
{
    ofstream outputfile("EucJp.txt", std::ios::out | std::ios::binary);

    //マイナ免許証から読み取った値を入れていく
    char str_C4[] = { 0x4D, 0x25, 0x4E, 0x49 };
    char str_C6[] = { 0x34, 0x63, 0x36, 0x40, 0x45, 0x79 };
    char str_C7[] = { 0x3D, 0x60, 0x43, 0x66, 0x37, 0x3F, 0x24, 0x47, 0x31, 0x3F, 0x45, 0x3E, 0x24, 0x47, 0x24, 0x2D, 0x24, 0x6B, 0x3D, 0x60, 0x43, 0x66, 0x37, 0x3F, 0x3C, 0x56, 0x24, 0x4F, 0x3D, 0x60, 0x43, 0x66, 0x37, 0x3F, 0x3C, 0x56, 0x21, 0x4A, 0x23, 0x35, 0x23, 0x74, 0x21, 0x4B, 0x24, 0x4B, 0x38, 0x42, 0x24, 0x6B };

    // 1バイトずつEUC-JPに変換しファイル出力
    for (int i = 0; i < sizeof(str_C4) / sizeof(str_C4[0]); i++) {
        outputfile.put(str_C4[i] + 0x80);  
    }
    outputfile.put('\n');

    for (int i = 0; i < sizeof(str_C6) / sizeof(str_C6[0]); i++) {
        outputfile.put(str_C6[i] + 0x80);
    }
    outputfile.put('\n');

    for (int i = 0; i < sizeof(str_C7) / sizeof(str_C7[0]); i++) {
        outputfile.put(str_C7[i] + 0x80);
    }

    outputfile.close();

    return 0;
}
~~~

出力されたテキストファイルを EUC-JP を指定して開くと日本語が表示されます。

~~~EucJp.txt
優良
眼鏡等
準中型で運転できる準中型車は準中型車（５ｔ）に限る
~~~

従来免許証で印刷されている文字が、そのままテキストデータとしてマイナ免許証に入れられているようです。