3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

.NETアプリからマイナンバーカードの情報を読み取る(Linuxでも動くよ)

Posted at

:star: はじめに

PCSC-sharpとPaSoRiでマイナンバーカードの情報を読んでみたのでそのメモです。

:pencil: 概要

実行環境

項目 概要
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形式

:hammer_pick: プログラム

ライブラリ追加

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形式なのでパースして結果を表示

:penguin: 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

:rabbit: うさコメ

他の言語での情報なんかは出てくるけど、.NETでそのものずばりの記述がない気がしたので試してみました( ˙ω˙)

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?