83
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Android でマイナンバーカードにアクセスしてみた

Posted at

まえがき

マイナンバーカードを作ったもののイマイチこのカードで何ができるのか、そもそもこのカードは一体ナニモノなのか分からない。でもまぁマイナポイントもらえるから別にいっか!・・・ちょっと前まで私はそんな人間でした。ごめんなさい反省してます許して!
でも確定申告の電子申告をやってみたら、なんと手持ちの Android スマホ+マイナンバーカードで個人認証できるじゃないですか。え、このカードってそういうものなの!?実はめっちゃ便利なものなんじゃない??となったわけです。

というきっかけから、そもそもマイナンバーカードとは何なのか、どんなことができるのかを技術的な観点から調べてみました。また実際に Android 端末からアクセスする方法も調べてデモアプリも作ってみました。この記事ではそれら調べたことをまとめています。デモアプリも併せて公開します。

サンプルアプリ

app_ss.png

免責

この記事ではマイナンバーカードの仕様の一部を説明していますがその正しさは保証できません。
そもそも私はマイナンバーカードの公式ないし正式な仕様をまったく知りません。ネット上で公開されている(誰でも見ることができる)情報をまとめ、その情報をベースに Android でのアクセス方法を探り、そこから得られた知見を書いています。
私個人のマイナンバーカードと私物の Android 端末で動作確認をしていますが、すべてのマイナンバーカードと Android 端末で同様の結果が得られることは保証できません。
実際にマイナンバーカードにアクセスする場合はくれぐれも自己責任でお願いします。

マイナンバーカードざっくり FAQ

マイナンバーカードってそもそも何なの?という人が多いと思いますので、まずはざっくりとした FAQ を書いてみようと思います。

Q1: マイナンバーカードって、マイナンバーが記録されてあるだけのカードでしょ?

マイナンバー以外に個人情報(氏名、住所、生年月日、性別、顔写真)、認証用キーペア、署名用キーペア等いろんな情報が記録されています。また機能を拡張できるようになっていて、今後その他のデータも記録されるようになると思われます。

Q2: それらのデータを取り出せるだけでしょ?

マイナンバーカードは単純な記憶媒体ではありません。演算能力も持っていて、カード内で署名処理を実行し、その結果を取得できます。

Q3: カード内で署名できると何が嬉しいの?

役所に提出する書類(のデジタルデータ)に署名してオンラインで提出できます(e-Tax とか)。またオンラインでの個人認証にも使えます。

Q4: そんなのカードとか使わなくてもできるでしょ?

できるけどキーペアを各個人がしっかり管理して漏洩しないようにする必要があります。そんなの無理だと思いません?

Q5: カードだと漏洩しないの?

カードには公開鍵と秘密鍵の両方が記録されているけど秘密鍵を取り出すインタフェースは存在しないっぽいです。無理矢理取り出そうとすると IC チップが壊れるようになっているらしいです。だから滅多なことでは漏洩しない、ということらしいです。

Q6: え、取り出せないならどう使うの?

秘密鍵を使った署名処理はカード内で完結するため秘密鍵自体を取り出す必要はないんです。つまり「署名できるのはその人のカードだけ」ということになるわけです。

マイナンバーカードの基礎知識

スマートカードの一種である

マイナンバーカードは、カテゴリーとしてはスマートカードの一種になるようです。スマートカードというのは IC(集積回路)を組み込んだカードのことで、CPU, RAM, ROM が集積されています。要はちっこいコンピュータです。スマートカード自体はだいぶ昔から広く使われていて、たとえばクレジットカードやキャッシュカード、公共交通系カード、SIM カード、運転免許証なんかもスマートカードに含まれるらしいです。

複数のアプリをインストールできる

マイナンバーカードは複数のカードアプリをインストールできるみたいです。つまりマイナンバーカードというのは、ある一つの目的のためだけのカードではなく、様々な目的で使うことができるようになっているということです。スマホがアプリをインストールすることで色んなことができるのと同じように、マイナンバーカードもアプリをインストールすることで色んなことができるようになっているというわけです(ただしスマホと違ってユーザが勝手にアプリをインストールすることはできません)。

デフォルトでは以下の4つのカードアプリがインストールされているようです。

  • 住基ネット AP
  • 券面事項確認 AP
  • 公的個人認証 AP
  • 券面事項入力補助 AP

それぞれどんなアプリなのかは私もしっかりとは把握できていませんが、名前からなんとなく推察できるのではないかと。この記事の後半では券面事項入力補助 AP公的個人認証 APの使い方について説明していきます。

ファイルシステム

マイナンバーカードがコンピュータであることは分かりました。ということは、もしかしたらファイルシステムを持っているのでは?と考える人もいると思うのですが、実際あります。ファイルシステムは Windows や UNIX のそれと同様にディレクトリ構造(のような構造)になっています1。ルートディレクトリ直下に各カードアプリ用のディレクトリが切られていて、その中にそのアプリ専用のファイルが配置されています。

ディレクトリ構造.png

Windows や UNIX のファイルシステムと根本的に違うのは、必ずしも読んだり書いたりするためのものではないということです。後述しますが、ファイルに対して「読み取る」だけでなく「照合する」や「署名する」といったコマンドを実行することができます。というよりこのカードの OS はユーザに対して 「まず操作対象のファイルを選択し、そのファイルに対して何らかの操作をする」 というインタフェースしか提供していないようです。マイナンバーカードを扱う上ではこの仕組みを理解することが重要になります。

また多くのファイルは暗証番号による認証をしたあとでないとアクセスできないという点も押さえておく必要があります。ただし公的個人認証 AP の認証用公開鍵証明書ファイルは例外的に認証なしで読み取ることができます。

通信インタフェース

マイナンバーカードは接触型と非接触型の二つのインタフェースを持っています。どちらを使ってもできることは基本同じはずです。スマホと通信するときはほとんどの場合、非接触型インタフェースを使うことになります。非接触型インタフェースは ISO/IEC 14443 の Type B 規格に準拠しています。なんだそりゃと思うかも知れませんが、要は NFC-B と呼ばれている規格です。

通信プロトコル

私は NFC のことなんてろくに知りませんので物理層やデータリンク層の具体的なプロトコルは知りません。ですが NFC での通信は Android が標準でサポートしていますから Android に用意されている NFC インタフェースを使えば良いことになります。具体的には NfcAdapter クラスを使うことになります。

ただ、NfcAdapter クラスが提供している機能は「カードにバイト配列を送信し、それに対するカードからのレスポンスをバイト配列で受け取る」という、とてもシンプルでプリミティブなものです。どんなバイト配列を送るべきなのか、レスポンスのバイト配列をどう解釈するべきなのか、それが分からなければ何もできません。

この「どんなバイト配列を送るべきか」「レスポンスのバイト配列をどう解釈するべきか」を規定しているのが ISO/IEC 7816-4 です。この規格では「どんなバイト配列を送るべきか」を Command APDU として定義し、「レスポンスのバイト配列をどう解釈するべきか」を Response APDU として定義しています。Android でマイナンバーカードと交信するためにはこのプロトコルを実装する必要があります。

マイナンバーカード固有の仕様

以上で Android でマイナンバーカードと交信するためのプロトコルは一応分かりました。
ではこれで万全かというとそうではありません。というのも上述したプロトコル(Command APDU/Response APDU)は「ファイルの選択方法」や「ファイルの読み取り方」などは規定しているのですが、「どのディレクトリにどんなファイルが配置されていてそのファイルにはどんな機能があるのか」は規定されていません。なぜなら上述のプロトコルはスマートカード全般で使われることを想定して策定されたものですから、マイナンバーカードだけが持つ機能まではカバーしていないんです。当たり前と言えば当たり前ですが。

ではその、マイナンバーカードの「どのディレクトリにどんなファイルが配置されていてそのファイルにはどんな機能があるのか」という情報をどうやって手に入れるかなのですが、これがどうも公式な情報がネット上では見当たらなかったりします。たぶん一般公開はしていないのだと思います。

それではこれで手詰まりなのかというとそうでもありません。ググってみるとどういうわけか、マイナンバーカードとの交信の方法を説明してくれている記事がいくつかヒットします。私は以下の記事を参考にさせていただきました。

またこれらの記事で説明されている通信の実装として OSS のライブラリもいくつか公開されています。私は以下のものを参考にして、Command APDU/Response APDU の実装とマイナンバーカードとの通信機能の一部を Kotlin に移植し、また一部機能を拡張しました。

具体的にできること

マイナンバーカードでできることは色々ありますし、今後も機能が拡張されていく(カードアプリが追加されていく)と思います。今回私はとりあえず以下のことをやってみることにしました。

  • マイナンバーの取得
  • 個人情報(基本4情報)の取得
  • 認証用公開鍵証明書の取得
  • 認証用秘密鍵での署名

それぞれの具体的な通信手続きを説明することもできますが、長くなってしまいますし上で挙げた参考記事の内容とも被りまくってしまいますので、ここでは一例としてマイナンバーの取得を行うときの手続きをシーケンス図にするだけに止めておこうと思います。Android アプリからマイナンバーカードに送っているデータが Command APDU, マイナンバーカードから Android アプリに返却されているデータが Response APDU です。

マイナンバーカード取得.png

Android での実装

ここまではマイナンバーカード側の仕様を見てきました。ではそのマイナンバーカードに Android からアクセスするにはどうすれば良いのかという話を、サンプルアプリのコードを見ながら説明していこうと思います。
(説明し易くするため一部のコードはサンプルアプリのコードから少し改変しています)

NFC での通信方法

まずはマイナンバーカードと NFC で通信できなければいけません。これには前述したとおり NfcAdapter クラスを使います。具体的には以下のようなコードになります。

class CommunicateDialogFragment : DialogFragment(), NfcAdapter.ReaderCallback {
    private val handler = Handler(Looper.getMainLooper())
    private lateinit var nfcAdapter: NfcAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        nfcAdapter = NfcAdapter.getDefaultAdapter(context)
    }

    override fun onResume() {
        super.onResume()
        nfcAdapter.enableReaderMode(
            activity,
            this,
            NfcAdapter.FLAG_READER_NFC_B or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK,
            null
        )
    }

    override fun onPause() {
        nfcAdapter.disableReaderMode(activity)
        super.onPause()
    }

onResume() 内で NfcAdapter を Read mode にしています。すると端末の NFC ハードウェアがカードリーダーモードになり、マイナンバーカードにタッチすると NfcAdapter.ReaderCallback#onTagDiscovered() コールバックが呼ばれるようになります。ですのでカードとの具体的な通信処理はそのコールバックメソッドの中に書けば良いことになります。

    override fun onTagDiscovered(tag: Tag?) {
        if (tag == null) return
        val isoDep = IsoDep.get(tag)
        isoDep.connect()
        val commandAPDU = byteArrayOf(0x00, 0xA4.toByte(), 0x02, 0x0C, 0x02, 0x00, 0x11)
        val responseAPDU = isoDep.transceive(commandAPDU)
        isoDep.close()
        handler.post { dismiss() }
    }
}

ただしこのコールバックはメインスレッドとは異なるスレッドで呼ばれるので気を付ける必要があります。また通信の途中で例外が発生することも結構多い(タッチ位置がずれて通信できなくなるとかあるある)ので、ちゃんと実装したい場合はしっかり例外処理を書く必要があります。

APDU の実装

上述したように Command APDU/Response APDU の実装は参考にしたライブラリのコードをほぼそのまま移植しましたのでオリジナル性はほとんどないのですが、一応ざっくりと説明しておこうと思います。

Command APDU の実装

Command APDU は以下の構造のバイト列になります。

header body
CLA
(1 byte)
INS
(1 byte)
P1
(1 byte)
P2
(1 byte)
Lc
(0~3 bytes)
Data field
(0~? bytes)
Le
(0~3 bytes)
  • CLA: 命令のクラス(カテゴリのようなもの?)
  • INS: 命令
  • P1, P2: パラメータ
  • Lc: Data field の長さ
  • Data field: 命令に関連するデータ
  • Le: 期待するレスポンスの長さ

body 部は可変長ですが、この持ち方のパターンによって Case 1~4 の形式が定義されています。

  • Case 1: Lc, Data field, Le すべて 0 バイト
  • Case 2: Lc, Data field が 0 バイト, Le は 1~3 バイト
  • Case 3: Lc は 1~3 バイト, Data field が 1 バイト以上, Le は 0 バイト
  • Case 4: Lc は 1~3 バイト, Data field が 1 バイト以上, Le は 1~3 バイト

そこで APDU クラスでは companion object にインスタンス生成メソッドを4つ用意して、各 Case の Command APDU (のバイト配列)を生成できるようにしています。なお参考にしたライブラリでは Lc と Le のサイズは最大 1 バイト(指定できる値の最大値が 256)というミニマムな仕様(Case 2S, Case 3S, Case 4S)のみの実装になっていたのですが、移植するにあたって Lc, Le ともに指定できる値の最大値が 65536 になる拡張仕様(Case 2E, Case 3E, Case 4E)に対応させました。

なお、CLA と INS の組み合わせがカードに対する命令コードに当たるわけですが、今回の用途で使う命令コードは以下の4つになります。

命令 CLA INS 内容
SELECT FILE 0x00 0xA4 ファイルを選択する
VERIFY 0x00 0x20 選択したPIN(暗証番号)ファイルと照合する
READ BINARY 0x00 0xB0 選択したファイルを読み取る
COMPUTE DIGITAL SIGNATURE 0x80 0x20 選択した秘密鍵ファイルで署名する

Response APDU のパース

Response APDU は以下の構造のバイト列になります。

body trailer
Data field
(0~? bytes)
SW1
(1 byte)
SW2
(1 byte)
  • Data field: レスポンス本文(サイズは可変。ない場合もある)
  • SW1, SW2: Status(2バイト固定)

送信した Command APDU によって返却されるであろう SW1, SW2 は変わりますが、基本的に命令の処理に成功した場合は SW1=0x90, SW2=0x00 が返却されるようです。

各種コマンドの実装

次に Command APDU の中身である SELECT FILE, READ BINARY, VERIFY, COMPUTE DIGITAL SIGNATURE コマンドの実装ですが、こちらも参考にしたライブラリの一部の機能を移植したうえで Android から扱いやすいようアレンジしたものにしています。機能を簡単にまとめると以下のようになります。

  • 基本機能(Reader クラス)
    • SELECT FILE, READ BINARY, VERIFY, COMPUTE DIGITAL SIGNATURE コマンドの送信と結果の受信
  • 券面事項入力補助 AP との通信(TextAP クラス)
    • PIN(暗証番号)解除の残りカウントの取得
    • PIN(暗証番号)の解除
    • マイナンバーの取得
    • 基本4情報(氏名, 住所, 生年月日, 性別)の取得
  • 公的個人認証 AP との通信(JpkiAP クラス)
    • PIN(暗証番号)解除の残りカウントの取得
    • PIN(暗証番号)の解除
    • 認証用公開鍵証明書の取得
    • 認証用秘密鍵での署名

PIN 解除の残りカウントというのは「あと何回間違えるとロックがかかるか」のカウントとなります。個人情報を取得したりするには VERIFY コマンドで PIN(暗証番号)を照合する必要があるのですが、間違った PIN をカードに渡してしまうとこのカウントが 1 減ります。そして 0 になるとロックがかかり、カードの機能が使えなくなってしまいます(こうなると役所に行ってロックを解除してもらわないといけなくなってしまいます)。正しい PIN を渡すとこのカウントは初期状態に戻ります。

これら機能の実装にあたってポイントとなった点を三つほど挙げておきます。

《ポイント1》ASN.1 のパース

マイナンバーカードで扱われるファイルの多くは ASN.1 の DER 形式になっています。というか今回マイナンバーカードから読み取るデータはすべてそうです。ご存じない方のために超簡単に説明すると、ASN.1 の DER 形式というのは JSON や XML のバイナリ版と考えると分かりやすいと思います。JSON や XML は構造化データをテキストで表現するための形式ですが ASN.1 の DER 形式は構造化データをバイナリで表現するための形式です。結構いろんなところで使われていて、たとえば電子証明書や秘密鍵の PEM 形式ってよく見ると思うのですが、あれは ASN.1 の DER 形式を Base64 でエンコードしたものです。

話を戻して、マイナンバーカードから読み取ったデータは ASN.1 の DER 形式ですので、それらをうまくパースしてやる必要があります。そのためにこちらのライブラリを使わせていただきました。

app/build.gradle に以下の依存関係を追加することでプロジェクトに導入しています。

app/build.gradle
dependencies {
    implementation 'com.hierynomus:asn-one:0.6.0'
}

ただ、そのままだと今回の用途には少し使いづらかったので、このライブラリをラップする形で ByteArray 型に拡張関数として ASN.1 のフレームを返す反復子を実装しました。バイト配列からフレーム単位で逐次データを取得できるようにしています。

class ASN1Frame(
    val tag: Int,
    val length: Int,
    val frameSize: Int,
    val value: ByteArray? = null
)

fun ByteArray.asn1FrameIterator(): Iterator<ASN1Frame> {
    return object: Iterator<ASN1Frame> {
        private val decoder = DERDecoder()
        private val byteArrayInputStream = ByteArrayInputStream(this@asn1FrameIterator)
        private val asn1InputStream = ASN1InputStream(decoder, byteArrayInputStream)

        override fun hasNext(): Boolean = byteArrayInputStream.available() > 0

        override fun next(): ASN1Frame {
            if (!hasNext()) throw NoSuchElementException()
            val tag = decoder.readTag(asn1InputStream)
            val length = decoder.readLength(asn1InputStream)
            val position = this@asn1FrameIterator.size - byteArrayInputStream.available()
            val frameSize = length + position
            val value: ByteArray? = try {
                decoder.readValue(length, asn1InputStream)
            } catch (e: Exception) {
                null
            }
            return ASN1Frame(tag.tag, length, frameSize, value)
        }
    }
}

これによって、たとえば基本4情報(氏名, 住所, 生年月日, 性別)を取得する際は、以下のようにカードから受け取ったデータを反復子を使ってパースできます。

TextAP.kt
    wrappedFrame.value.asn1FrameIterator().forEach { frame ->
        val value = frame.value ?: return@forEach
        val valueString = String(value)
        when(frame.tag) {
            33 -> { // Header
            }
            34 -> { // Name
                name = valueString
            }
            35 -> { // Address
                address = valueString
            }
            36 -> { // Birth
                birth = valueString
            }
            37 -> { // Sex
                sex = when(valueString) {
                    "1" -> "男性"
                    "2" -> "女性"
                    "9" -> "適用不能"
                    else -> "不明"
                }
            }
        }
    }

《ポイント2》通信タイムアウトの調整

Android の NFC インタフェースは、デフォルトでは通信のタイムアウトが短めになっています。Command APDU を送ってから Response APDU が返却されるまでに長い時間がかかるコマンドの場合、Response APDU を受け取る前にタイムアウトして例外が発生してしまいます。私が試した限りでは SELECT FILE, READ BINARY, VERIFY コマンドではデフォルトのタイムアウト時間でも特に問題は起こらなかったのですが COMPUTE DIGITAL SIGNATURE コマンドでは毎回タイムアウトしてしまいました。この問題を回避するために COMPUTE DIGITAL SIGNATURE の Command APDU を送る際はタイムアウト時間を長めに変更するようにしています。

Reader.kt
    fun signature(data: ByteArray): ByteArray {
        val apdu = APDU.newAPDUCase4(0x80.toByte(), 0x2A, 0x00, 0x80.toByte(), data, 0)
        val previousTimeout = isoDep.timeout
        isoDep.timeout = 5000
        val (sw1, sw2, res) = trans(apdu)
        isoDep.timeout = previousTimeout
        if (sw1 == 0x90.toByte() && sw2 == 0x00.toByte()) {
            return res
        }
        return byteArrayOf()
    }

《ポイント3》証明書データのデコード

マイナンバーカードの公的個人認証 AP からは公開鍵証明書のデータを読み取ることができますが、読み取ったデータは X.509 の DER 形式になっています。この形式は Java 標準ライブラリにある CertificateFactory クラスを使うことで簡単にデコードすることができます。

JpkiAP.kt
    private fun readCertificate(efid: ByteArray): X509Certificate {
        reader.selectEF(efid)

        // 読み込むべきサイズを取得する
        val tempData = reader.readBinary(7)
        val sizeToRead = tempData.asn1FrameIterator().next().frameSize

        // 全体を読み込む
        val realData = reader.readBinary(sizeToRead)

        // 証明書のデコード
        val certificateFactory = CertificateFactory.getInstance("X.509")
        return certificateFactory.generateCertificate(ByteArrayInputStream(realData)) as X509Certificate
    }

個人情報(マイナンバーと基本4情報)の取得

ここまででお膳立ては整いましたので、あとは具体的にやりたいことをコーディングします。
まずはマイナンバーと基本4情報(氏名, 住所, 生年月日, 性別)を取得してみようと思います。

TextApViewModel.kt
    private fun procedure(reader: Reader): Result {
        // PIN取得
        val pin = pin.value ?: ""
        if (pin.length != 4) {
            return Result(ResultStatus.ERROR_INSUFFICIENT_PIN)
        }

        // AP選択
        val textAP = reader.selectTextAp()

        // PINの残りカウント取得
        val count = textAP.lookupPin()
        if (count == 0) {
            return Result(ResultStatus.ERROR_TRY_COUNT_IS_NOT_LEFT)
        }

        // PIN解除
        if (!textAP.verifyPin(pin)) {
            return Result(ResultStatus.ERROR_INCORRECT_PIN, count - 1)
        }

        // マイナンバー取得
        val myNumber = textAP.readMyNumber()

        // その他の情報を取得
        val attributes = textAP.readAttributes()

        return Result(ResultStatus.SUCCESS, count, myNumber, attributes)
    }

マイナンバーカードとのやり取りの手順はコメントしてある通りで、まずは利用するカードアプリ(ここでは券面事項入力補助 AP)を選択し、PIN の残りカウント数を確認し、PIN の照合をして、取得したい情報を読み取ります。

認証用公開鍵証明書の取得

次に公的個人認証 AP から認証用公開鍵証明書を取得してみます。

SetAuthCertificateViewModel.kt
    private fun procedure(reader: Reader): Result {
        // AP選択
        val jpkiAP = reader.selectJpkiAp()

        // 認証用証明書取得
        val cert = jpkiAP.readAuthCertificate()
        Log.d(LOG_TAG, "cert: $cert")

        return Result(ResultStatus.SUCCESS, cert)
    }

すごく簡単ですね。まずカードアプリ(ここでは公的個人認証 AP)を選択し、証明書データを取得するだけです。認証用証明書取得は PIN による認証なしに読み取ることができるのでこれだけで OK です。

認証用秘密鍵での署名

今度は公的個人認証 AP の認証用秘密鍵を使用した署名処理をやってみようと思います。

AuthViewModel.kt
    private fun computeSignature(reader: Reader, data: ByteArray): Pair<ByteArray?, Result?> {
        // PIN取得
        val pin = pin.value ?: ""
        if (pin.length != 4) {
            return Pair(null, Result(ResultStatus.ERROR_INSUFFICIENT_PIN))
        }

        // AP選択
        val jpkiAP = reader.selectJpkiAp()

        // PINの残りカウント取得
        val count = jpkiAP.lookupAuthPin()
        if (count == 0) {
            return Pair(null, Result(ResultStatus.ERROR_TRY_COUNT_IS_NOT_LEFT))
        }

        // PIN解除
        if (!jpkiAP.verifyAuthPin(pin)) {
            return Pair(null, Result(ResultStatus.ERROR_INCORRECT_PIN, count - 1))
        }

        // 署名対象のデータを SHA-1 でハッシュ化
        val digest = MessageDigest.getInstance("SHA-1").digest(data)
        Log.d(LOG_TAG, "digest: ${digest.toHexString()}")

        // ハッシュ値を DigestInfo の形式に変換する
        val header = "3021300906052B0E03021A05000414"
        val digestInfo = header.hexToByteArray() + digest

        // カードの秘密鍵で署名する
        val signature = jpkiAP.authSignature(digestInfo)
        Log.d(LOG_TAG, "signature: ${signature.toHexString()}")
        return Pair(signature, null)
    }

PIN の照合のところまでは個人情報取得のときと同じなので説明は省略しますが、その後の処理は少し説明が必要ですね。
①まず署名対象のデータを SHA-1 でハッシュ化し、②そのハッシュ値を DigestInfo という形式に変換して、③それをカードに渡すことで署名データを取得しています。

①を理解するにはそもそも電子署名はどう作られるのかを知る必要があります。電子署名は大きく分けて以下の二つの工程によって作成されます。

  1. 署名対象のデータをハッシュ化する。
  2. そのハッシュ値を秘密鍵で暗号化する。

このうちマイナンバーカードがやってくれるのは後者だけです。だから前者のハッシュの計算は Android 側で行うというわけです。ちなみにここではハッシュ関数として SHA-1 を使っていますが他のハッシュ関数(SHA-256 とか)を使っても大丈夫だと思います(でも未検証です)。

次に②ですが、マイナンバーカードにハッシュ値を渡す際にはハッシュ値をそのまま渡すのは NG で、必ず DigestInfo の DER 形式に変換してから渡す必要があります。また DER か!と思うかも知れませんが、その通りです。マイナンバーカードとデータのやり取りをする際は、基本的に ASN.1 の DER 形式でなければいけないということです。では DigestInfo とは何なのかというと、これは RFC2315 (PKCS #7) で定義されているデータ構造で、ASN.1 で以下のように表現されます。

DigestInfo ::= SEQUENCE {
    SEQUENCE {
        algorithm   OBJECT IDENTIFIER,
        parameters  ANY DEFINED BY algorithm OPTIONAL
    },
    digest OCTET STRING
}

ざっくり言うとハッシュ関数(ハッシュアルゴリズム)とハッシュ値を含んだ構造体です。で、この構造体を DER 形式に符号化しないといけないわけですが、これを細かく説明していくと睡眠不足になってしまいそうなので省略します。結論だけ言うとハッシュ関数に SHA-1 を使っている場合は以下のように符号化すれば OK です。

30 21 30 09 06 05 2B 0E 03 02 1A 05 00 04 14 [ハッシュ値のバイト配列]

これで DigestInfo 形式のハッシュデータができましたので、それをマイナンバーカードに送って暗号化してもらえば良いというわけです。

認証システムの構築

せっかく署名できるようになったので、それを使った認証システムを作ってみようと思います。と言ってもガチなやつじゃなくてデモレベルなものです。

想定するシステム

構築する認証システムは「ユーザ登録」と「ユーザ認証」の二つのユースケースのみサポートすることにします。
「ユーザ登録」のシーケンスは下図のようになります。

シーケンス図(ユーザ登録).png

そして「ユーザ認証」のシーケンスは下図のようになります。

シーケンス図(認証).png

ただし今回作るものはデモ用と割り切って、シーケンス図にある認証サービス(認証サーバとリポジトリ)は Android アプリ内にモックとして実装することにします(このためだけにわざわざサーバ構築とか面倒じゃないですか…)。

実装上のポイント

認証の仕組みをざっくり説明すると以下のようになります。

  1. 認証サーバ側でランダムなバイト配列(これを nonce を言います)を生成してクライアントに送りつける。
  2. クライアントは受け取った nonce を秘密鍵で署名して認証サーバに送り返す。
  3. 認証サーバは受け取った署名を nonce とそのユーザの公開鍵で検証する。

署名の検証には Java 標準ライブラリにある Signature クラスを使うことができます。

CertificationService.kt
    fun verifySignature(signature: ByteArray): Boolean {
        val nonce = nonce ?: return false
        val certificate = AuthCertificateRepository.fetch(context) ?: return false
        return Signature.getInstance("SHA1withRSA").apply {
            initVerify(certificate.publicKey)
            update(nonce)
        }.verify(signature)
    }

むすび

マイナンバーカード、思いのほか面白いですね。正直侮っていました。というより興味がなかったというかなんというか…。だってこのカードがどういうものなのか、政府の説明だけじゃよく分からなかったんです。こんな普通に Android からアクセスできることを以前から知っていたらもっと興味を持てていたと思いますし、このカードの利便性をもっと理解できていたと思います。現状だと「なんだかよくわからないけどマイナポイントをもらえるカード」になってしまっているのがすごく勿体ないと思います。この記事がマイナンバーカードへの理解を深める一助になってくれたら幸いです。

あと一点だけ、この記事を読んで実際にマイナンバーカードを使った認証システムを構築してみようと思った方のために注意点を書いておこうと思います。マイナンバーカードを使った認証システムを構築することはたぶん可能だと思いますが、そのシステムを公に使うのは避けた方が無難だと思います。実際にカードを作った人はご存知かと思いますが、カードを作るときは 4 種類の暗証番号の登録を求められます。そしてそのうちの 3 つ(利用者証明用電子証明書用, 住民基本台帳用, 券面事項入力補助用)は「同じで良いですよ」と案内されます(私はそう案内されました)。仮にこれら 3 種類の暗証番号を同じにしている人が大多数であるなら、ユーザ認証のために入力される暗証番号を使って各種個人情報を読み取れてしまうことになります。つまり「ユーザ認証のための暗証番号を入力してください」と暗証番号を求めることは、実は結構リスキーな行為ということになり得ると思います(というかそもそも暗証番号を求めること自体法律的に大丈夫なのだろうか)。ですので現状では、認証システムを作ったとしても、個人レベルで使う程度にとどめておくのが無難だと思います。

  1. 実際は Windows や UNIX のディレクトリ構造とはかなり違う概念ですので正確なことを知りたい方は調べてみることをおすすめします。

83
48
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
83
48

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?