はじめに
PCSC-sharpとPaSoRiでマイナンバーカードの情報を読んでみたのでそのメモです。
概要
実行環境
| 項目 | 概要 |
|---|---|
| OS | Windows 11及びUbuntu 24.04 |
| リーダー | PaSoRi(RC-S380及びRC-S300) |
| 言語/ランタイム | C#/.NET 10 |
| ライブラリ | PCSC-sharp |
雑な解説
ソースを読むにあたって必要な知識の雑な解説を行います。
正確な情報については他サイトを参照してください。
PC/SCとは
PC(ホスト)とスマートカードリーダ/NFCリーダをつなぐための標準APIです。
APDUとは
ISO/IEC 7816-4で定義されるコマンド/レスポンスのフォーマットです。
コマンドAPDUの基本構造は以下です。
| 項目 | 概要 |
|---|---|
| CLA | クラスバイト(どのクラスの命令か等) |
| INS | 命令コード(SELECT、READ BINARY等) |
| P1 | パラメータ1 |
| P2 | パラメータ2 |
| Lc | 送信データ長(Dataフィールドのバイト数) |
| Data | カードに送りたいデータ |
| Le | 期待する応答データ長(受け取りたい最大バイト数) |
レスポンスAPDUの構造は以下です。
| 項目 | 長さ | 概要 |
|---|---|---|
| Data | 可変長 | 応答データ |
| SW1 SW2 | 2 | ステータスワード(成功/エラー) |
ステータスワードが90 00なら成功です。
マイナンバーカードの構造
今回アクセスするのはマイナンバーカードの以下の部分です。
MF:ルート
└─ DF:券面事項入力補助AP (AID = D3 92 10 00 31 00 01 01)
├─ EF:PIN (FID = 00 11)
├─ EF:個人番号 (FID = 00 01)
└─ EF:基本4業法 (FID = 00 02) TLV形式
プログラム
ライブラリ追加
dotnet package add PCSC
ソース
using System.Text;
using PCSC;
using PCSC.Monitoring;
Console.Write("PIN(4桁)を入力してください: ");
var pin = Console.ReadLine();
if (string.IsNullOrEmpty(pin) || (pin.Length != 4))
{
Console.WriteLine("PINは4桁の数字である必要があります");
return;
}
using var reader = new MynaReader(pin);
reader.Start();
Console.ReadLine();
reader.Stop();
// マイナンバーカード読取用PCSC-sharpのラッパー
internal sealed class MynaReader : IDisposable
{
private readonly ISCardMonitor monitor;
private readonly string pin;
public bool IsRunning { get; private set; }
public MynaReader(string pin)
{
this.pin = pin;
monitor = MonitorFactory.Instance.Create(SCardScope.System);
monitor.CardInserted += OnCardInserted;
}
public void Dispose()
{
MonitorFactory.Instance.Release(monitor);
}
private void OnCardInserted(object sender, CardStatusEventArgs e)
{
using var context = ContextFactory.Instance.Establish(SCardScope.System);
var reader = context.ConnectReader(e.ReaderName, SCardShareMode.Shared, SCardProtocol.Any);
try
{
// 券面事項入力補助APの選択: SELECT(0xA4) AIDによる選択(0x04) 券面事項入力補助AP[D3 92 10 00 31 00 01 01 01 01]
var response = SendCommand(reader, CreateCommand(0x00, 0xA4, 0x04, 0x0C, [0xD3, 0x92, 0x10, 0x00, 0x31, 0x00, 0x01, 0x01, 0x04, 0x08]));
if (!response.IsSuccess())
{
Console.WriteLine($"AP選択失敗: SW={response.SW1:X2}{response.SW2:X2}");
return;
}
// PINの選択: SELECT(0xA4) FIDによる選択(0x02)
response = SendCommand(reader, CreateCommand(0x00, 0xA4, 0x02, 0x0C, [0x00, 0x11]));
if (!response.IsSuccess())
{
Console.WriteLine($"暗証番号選択失敗: SW={response.SW1:X2}{response.SW2:X2}");
return;
}
// VERIFYコマンドでPIN認証: VERIFY(0x20)
response = SendCommand(reader, CreateCommand(0x00, 0x20, 0x00, 0x80, Encoding.ASCII.GetBytes(pin)));
if (!response.IsSuccess())
{
Console.WriteLine($"PIN認証失敗: SW={response.SW1:X2}{response.SW2:X2}");
return;
}
// 個人番号選択: SELECT(0xA4) FIDによる選択(0x02)
response = SendCommand(reader, CreateCommand(0x00, 0xA4, 0x02, 0x0C, [0x00, 0x01]));
if (!response.IsSuccess())
{
Console.WriteLine($"マイナンバー選択失敗: SW={response.SW1:X2}{response.SW2:X2}");
return;
}
// 個人番号読み取り: READ BINARY(0xB0)
response = SendCommand(reader, CreateCommand(0x00, 0xB0, 0x00, 0x00, 0x00));
if (!response.IsSuccess())
{
Console.WriteLine($"個人番号読み取り失敗: SW={response.SW1:X2}{response.SW2:X2}");
return;
}
var id = Encoding.ASCII.GetString(response.Data.Slice(3, 12));
Console.WriteLine($"個人番号: {id}");
// 基本4情報選択: SELECT(0xA4) FIDによる選択(0x02)
response = SendCommand(reader, CreateCommand(0x00, 0xA4, 0x02, 0x0C, [0x00, 0x02]));
if (!response.IsSuccess())
{
Console.WriteLine($"基本4情報選択失敗: SW={response.SW1:X2}{response.SW2:X2}");
return;
}
// TLVデータ長読み取り: READ BINARY(0xB0) offset=0x02, length=1
response = SendCommand(reader, CreateCommand(0x00, 0xB0, 0x00, 0x02, 0x01));
if (!response.IsSuccess())
{
Console.WriteLine($"データ長読み取り失敗: SW={response.SW1:X2}{response.SW2:X2}");
return;
}
var length = response.Data[0];
Console.WriteLine($"データ長: {length}");
// 基本4情報読み取り: READ BINARY(0xB0) offset=0x03, length=length
response = SendCommand(reader, CreateCommand(0x00, 0xB0, 0x00, 0x03, length));
if (!response.IsSuccess())
{
Console.WriteLine($"基本4情報読み取り読み取り失敗: SW={response.SW1:X2}{response.SW2:X2}");
return;
}
// パース
var map = ParseTlv(response.Data);
//Console.WriteLine($"制御情報(DF21): {Convert.ToHexString(map.GetValueOrDefault(0xDF21, []))}");
Console.WriteLine($"氏名(DF22): {Encoding.UTF8.GetString(map.GetValueOrDefault(0xDF22, []))}");
Console.WriteLine($"住所(DF23): {Encoding.UTF8.GetString(map.GetValueOrDefault(0xDF23, []))}");
Console.WriteLine($"生年月日(DF24): {Encoding.ASCII.GetString(map.GetValueOrDefault(0xDF24, []))}");
Console.WriteLine($"性別(DF25): {Encoding.ASCII.GetString(map.GetValueOrDefault(0xDF25, []))}");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
finally
{
reader.Disconnect(SCardReaderDisposition.Leave);
}
}
// 送信用コマンド作成
private static byte[] CreateCommand(byte cla, byte ins, byte p1, byte p2, byte[] data)
{
var command = new byte[4 + 1 + data.Length];
command[0] = cla;
command[1] = ins;
command[2] = p1;
command[3] = p2;
command[4] = (byte)data.Length; // Lc
data.CopyTo(command.AsSpan(5, data.Length));
return command;
}
// 受信用コマンド作成
private static byte[] CreateCommand(byte cla, byte ins, byte p1, byte p2, int le)
{
var command = new byte[5];
command[0] = cla;
command[1] = ins;
command[2] = p1;
command[3] = p2;
command[4] = (byte)le; // Le
return command;
}
private Response SendCommand(ICardReader reader, byte[] command)
{
Console.WriteLine($"送信: {BitConverter.ToString(command)}");
var receiveBuffer = new byte[258]; // SW1+SW2を含む
var bytesReceived = reader.Transmit(command, receiveBuffer);
Console.WriteLine($"受信: {BitConverter.ToString(receiveBuffer, 0, bytesReceived)}");
return new Response(receiveBuffer, bytesReceived);
}
private static Dictionary<int, byte[]> ParseTlv(ReadOnlySpan<byte> tlvData)
{
var map = new Dictionary<int, byte[]>();
var index = 0;
while (index < tlvData.Length)
{
if (index >= tlvData.Length)
{
break;
}
var tag1 = tlvData[index++];
int tag;
if (tag1 == 0xDF)
{
// 2バイトタグ)
if (index >= tlvData.Length)
{
break;
}
var tag2 = tlvData[index++];
tag = (tag1 << 8) | tag2;
}
else
{
// 1バイトタグ
tag = tag1;
}
if (index >= tlvData.Length)
{
break;
}
var length = tlvData[index++];
if (index + length > tlvData.Length)
{
break;
}
var value = tlvData.Slice(index, length).ToArray();
map[tag] = value;
index += length;
}
return map;
}
public bool Start()
{
if (IsRunning)
{
return false;
}
using var context = ContextFactory.Instance.Establish(SCardScope.System);
var readers = context.GetReaders();
if (readers.Length == 0)
{
return false;
}
monitor.Start(readers[0]);
IsRunning = true;
return true;
}
public void Stop()
{
if (!IsRunning)
{
return;
}
monitor.Cancel();
IsRunning = false;
}
private sealed class Response
{
private readonly byte[] buffer;
private readonly int length;
public ReadOnlySpan<byte> Data => buffer.AsSpan(0, length >= 2 ? length - 2 : 0);
public byte SW1 { get; }
public byte SW2 { get; }
public Response(byte[] buffer, int length)
{
this.buffer = buffer;
this.length = length;
if (length >= 2)
{
SW1 = buffer[length - 2];
SW2 = buffer[length - 1];
}
else
{
SW1 = 0x00;
SW2 = 0x00;
}
}
public bool IsSuccess() => SW1 == 0x90 && SW2 == 0x00;
}
}
実行例
XX部分は個人情報なのでマスク。
PIN(4桁)を入力してください: XXXX
送信: 00-A4-04-0C-0A-D3-92-10-00-31-00-01-01-04-08
受信: 90-00
送信: 00-A4-02-0C-02-00-11
受信: 90-00
送信: 00-20-00-80-04-XX-XX-XX-XX
受信: 90-00
送信: 00-A4-02-0C-02-00-01
受信: 90-00
送信: 00-B0-00-00-00
受信: FF-10-0C-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-FF-FF-90-00
個人番号: XXXXXXXXXXXX
送信: 00-A4-02-0C-02-00-02
受信: 90-00
送信: 00-B0-00-02-01
受信: 6B-90-00
データ長: 107
送信: 00-B0-00-03-6B
受信: DF-21-XX-XX-XX-XX-XX-XX-XX-XX-XX-DF-22-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-DF-23-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-XX-DF-24-XX-XX-XX-XX-XX-XX-XX-XX-XX-DF-25-XX-XX-90-00
氏名(DF22): XXXXX
住所(DF23): XXXXXXXXXXXXXXXXXXXX
生年月日(DF24): XXXXXXXX
性別(DF25): X
ソース解説
- 最初にPINコードを入力しておいてそれを認証時に使用します
- MynaReaderクラスがPCSC-sharpの機能を使った読み取り処理で、ISCardMonitorのインスタンスを取得し、CardInsertedイベントでカードがリーダーに置かれたときに処理を行うことができます
- MynaReaderクラスのStart/Stopでは、カードリーダーの監視の開始・停止をしています
- CardInsertedイベントで実行するOnCardInsertedがカード読み取りのメイン部分になります
- 最初にISCCardContext.ConnectReader()によりICardReaderを取得し、Transmit()によってAPDUコマンドの送信・レスポンスの受信を行う形となります
- 以降、以下のコマンドを実行して個人番号と基本4情報の読み取りを行っています
- 券面事項入力補助APのSELECT
- PINのSELECT
- PINのVERIFY
- 個人番号のSELECT
- 個人番号のREAD BINARY
- 基本4情報のSELECT
- 基本4情報のデータ長(3バイト目)をREAD BINARY
- 基本4情報のTLV部分をREAD BINARY
- 基本4情報はTLV形式なのでパースして結果を表示
LinuxでPC/SCを動かす
PCSC-sharpはLunuxでの動作にも対応していますが、LinuxでPC/SCを使えるようにするには、PC/SCデーモンを動作させる必要があります。
以下、Ubuntuでの方法について記述します。
パッケージインストール
sudo apt update
sudo apt install pcscd pcsc-tools libccid
pcscdサービスの起動設定
sudo systemctl enable pcscd
sudo systemctl start pcscd
リーダーの動作確認
sudo pcsc_scan
うさコメ
他の言語での情報なんかは出てくるけど、.NETでそのものずばりの記述がない気がしたので試してみました( ˙ω˙)