Android端末でマイナンバーカードを使用して電子署名を生成する

はじめに

弥生モバイルチームのtijinsです。

弥生ではマイナンバーカードを使用して確定申告書類に電子署名を行う弥生 電子署名アプリを公開しています。

開発時にNFCやマイナンバーカードに関する情報が少なく困った為、調査結果を共有したいと思います。

e-Taxと電子署名

e-Taxとは、確定申告などの各種手続をインターネットを通じて行うことができる国税庁のサービスです。 e-Taxでは、申告書類(xmlファイル)に電子署名する事で真正性の検証を行っています。

マイナンバーカード

マイナンバーカードはNFC-TypeB(ISO14443B)の仕様に準拠した非接触ICカードです。

非接触ICカードは、非接触で読み書きできるストレージとしてSuicaやWAONなどの電子マネーとして使われていますが、マイナンバーカードはデータの保存だけではなく、署名の計算もできるようになっています。 また、ファイルシステムがあり、以下のように複数のアプリケーションが搭載されています。

アプリ 用途
券面事項確認AP 券面の情報が画像として保管されている
券面事項入力補助AP 券面記載事項がテキストとして保管されている
公的個人認証AP ファイルを入力して署名値を生成する
その他・・・ 自治体や企業が独自のアプリを追加して使用する

マイナンバーカードによる電子署名

e-Taxでは、マイナンバーカードに搭載された公的個人認証APを使用して申告用のxmlファイルに署名を付与します。

e-Taxではパスワード認証方式も利用可能ですが、マイナンバーカードを使用すると2要素認証(カード+パスワード)となり、より安全になります。

資料の入手

マイナンバーカードを利用するアプリの情報は、あまり公開されていません。

以下のサイトが公式ですが、インターネット上に公開されている情報の方が充実しています。

地方公共団体情報システム機構

e-Taxの公式サイト

インターネット上の情報のみでも目的のアプリは作れるのですが、エラーコードなど不明な部分も多く、商用アプリとしては不安があった為、地方公共団体情報システム機構に問い合わせてみました。 ダメ元だったのですが、とても簡単に通信仕様書を入手できました。

入手した通信仕様書はNDA対象なので、ここではインターネット上に公開されている情報のみを元に説明していきます。

電子署名の仕様

マイナンバーカードに搭載されている公的個人認証APを使用すると署名を生成可能です。

公開されている仕様書によるとRSAwithSha256、またはRSAwithSha1に対応しています。

弥生の電子署名アプリでは信頼性の高いSHA256の方を使用しています。

マイナンバーカードで電子署名を行う手順

  1. 申告文書xmlの正規化
  2. 正規化した申告文書xmlのハッシュ化
  3. ハッシュ値にOIDを追加する
  4. OID付きのハッシュ値をマイナンバーカードに送信
  5. 署名値を取得

申告書類xmlの正規化

xmlの正規化とは、属性の順序を揃えたり、スペースを取り除くなどして、xmlの書式を統一する操作です。 e-TaxではCanonical XML 1.0(2001年3月15日勧告)に従って正規化します。 Androidではorg.apache.xml.security.c14n.Canonicalizerを使用する事で可能です。

    private fun canonicalize() {
        org.apache.xml.security.Init.init()
        val factory = DocumentBuilderFactory.newInstance().apply {
            isNamespaceAware = true
        }
        val doc = factory.newDocumentBuilder().parse(ByteArrayInputStream(sourceXmlBin))
        val c14n = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS)
        val canonicalized = ByteArrayOutputStream(1024).use {
            c14n.canonicalizeSubtree(doc.getElementsByTagNameNS("http://www.w3.org/2000/09/xmldsig#", "SignedInfo").item(0), it)
            it.toByteArray()
        }
    }

申告書類xmlのハッシュ化

正規化したXMLをSHA256を使用してハッシュ化します。 SHA256を使用すると32byteのハッシュ値が出力されます。 Androidではjava.security.MessageDigestを使用する事で可能です。

    private fun calcSha256Hash() {
        val sha256 = MessageDigest.getInstance("Sha256")
        val hash = sha256.let { 
            it.update(canonicalizedXml)
            it.digest()
        }
    }

ハッシュ値にOIDを追加する

ハッシュ化方式を示すOIDを追加します。(以下はSHA256の例)

0x30, 0x31,
0x30, 0x0d,
0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05,
0x00,
0x04, 0x20,
[以降ハッシュ値32byte]

BouncyCastleの導入が必要ですが、org.bouncycastle.asn1.x509.DigestInfoを使用してもOIDを追加可能です

private fun encodeToDigestInfo(hash: ByteArray): ByteArray {
    val digestInfo = DigestInfo(
        AlgorithmIdentifier(
            NISTObjectIdentifiers.id_sha256,
            DERNull.INSTANCE
        ),
        hash
    )
    return digestInfo.encoded
}

参考

https://www.j-lis.go.jp/file/12_Android_siyou_intent.pdf

マイナンバーカードにハッシュ値を送信する

Androidアプリからマイナンバーカードにアクセスする方法

マイナンバーカードには、地方公共団体情報システム機構が提供する公式アプリか、AndroidのNFC-APIを直接使用してアクセスします。

実装が簡単なのはJPKI利用者ソフトを使用する方法ですが、別アプリのインストールが必要になる為、ユーザービリティは良くありません。

ユーザビリティが良くないからなのか、公式のe-Taxでもワクチン接種証明アプリでも使われていません。

ここでは、NFC-APIを使う方法で説明していきます。

AndroidのNFC API

AndroidにNFC関連の機能が追加されたのはAndroid 2.2.3(GingerBread)と古く、殆ど改良されていない為、あまり使いやすいものではありません。

マイナンバーカードを利用するアプリの基本的な実装

  1. ForegroundDispathでNFC-TypeBのカードを検知するように指定
  2. onNewIntent()内でIntentからTagを取得し、TagからIsoDepオブジェクトを生成する(NfcBではないので注意)
  3. IsoDep.connect()を実行する
  4. IsoDep.transceive()を使用してマイナンバーカードに指定の順序でコマンドを送信する
  5. IsoDep.close()を実行する

マイナンバーカードはNFC TypeB形式のカードですが、ISO7816-4コマンドで通信するためNfcBではなくIsoDepを使用します。

参考

高度な NFC の概要  |  Android デベロッパー  |  Android Developers

ForegroundDispath

Android端末は、ディスプレイがONの時に、およそ0.5秒〜1秒周期でカードをポーリングしています。

AndroidManifestにIntent-FilterとしてNFCの対応を記載している場合、カードが検知されるとアプリが起動されます。

弥生の電子署名アプリでは、AndroidManifestには記載せず、Foreground Dispatchという方法を採用しました。

Foreground Dispatchは、アプリの使用中に限定して、他のアプリに優先してカードにアクセスできます。

また、AndroidManifestには記載していないので、他の用途でマイナンバーカードを使用している時に、弥生の電子署名アプリが起動してしまう心配がありません。

        val nfcAdapter = NfcAdapter.getDefaultAdapter(this as Activity)
        val pendingIntent = PendingIntent.getActivity(this, 0, Intent(this, this::class.java), 0)
        val techListsArray = arrayOf(arrayOf(NfcB::class.java.name))
        nfcAdapter?.enableForegroundDispatch(this, pendingIntent, null, techListsArray)

Foreground dispatchは不要になったら解除します。

        nfcAdapter?.disableForegroundDispatch(this)

マイナンバーカードのAPDU

マイナンバーカードのAPDU(Application Protocol Data Unit)はISO7816-4に準拠したものになっています。

コマンド

CLA INS P1 P2 Lc DATA Le
コマンド種別 コマンドコード パラメータ1 パラメータ2 Data部の長さ データ レスポンスData長

DATA, Leは存在しない場合があります

フィールド サイズ 備考
CLA(命令クラス) 1 命令種別
INS(命令コード) 1 命令コード
P1 1 命令に関連するパラメーター
P2 1 命令に関連するパラメーター
Lc 1~3 Data部の長さ
DATA Lcで示される長さ データのペイロード
Le 1~3 期待するレスポンスのData部の長さ。可変・未定の場合は0

この形式のパケットを作ってIsoDep.transceive()でカードに送信すると、レスポンスとして結果が返ってきます。

参考

ISO 7816 part 4 smart card standard APDU commands ATR historical bytes

レスポンス

STATUS1 STATUS2
ステータス1 ステータス2

正常時の応答はISO7816-4で規定されていて0x90,0x00です。

公式の仕様書にはエラーコードと発生条件も記載されていますが、NDA対象なので割愛します。

マイナンバーカードで署名を生成する

ここからが本題です。 署名を生成する為には、次の順序でコマンドを送信していきます。

sequenceDiagram
 App ->> Card: SelectFile(公的個人認証サービスAP)
 Card -->> App: Success
 App ->> Card: SelectFile(署名用パスワード)
 Card -->> App: Success
 App ->> Card: Verify(署名用パスワード)
 Card -->> App: Success
 App ->> Card: SelectFile(署名用秘密鍵)
 Card -->> App: Success
 App ->> Card: Compute Digital Signature
 Card -->> App: Signature

SelectFile(公的個人認証サービスAP)

公的個人認証サービスAPを起動する

CLA INS P1 P2 Lc DATA
00 A4 04 0C 0A D392F000260100000001

コマンド詳細

フィールド 備考
CLA 00
INS 0xA4 SelectFileコマンド
P1 0x04 Data部にDF(dedicated file)の識別子が含まれている
P2 0x0C ile control information option
Lc 0x0A Data部の長さ
Data 公的個人認証サービスAPの識別子

SelectFile(署名用パスワード)

署名用パスワードをVerifyコマンドの対象として選択する

CLA INS P1 P2 Lc DATA
00 A4 02 0C 02 001B

コマンド詳細

フィールド 備考
CLA 00
INS 0xA4 SelectFileコマンド
P1 0x02 Data部にEF(Elementary file)の識別子が含まれている
P2 0x0C file control information option
Lc 0x02 Data部の長さ
Data 0x00,0x1B 署名用パスワードの識別子

Verify

マイナンバーカードの署名用暗証番号(英数字6文字〜12文字)を送信して認証する

CLA INS P1 P2 Lc DATA
00 20 00 80 * *****

コマンド詳細

フィールド 備考
CLA 00
INS 0x20 Verifyコマンド
P1 0x00 固定値
P2 0x80 Data部には選択されたEFに対応するパスワードが含まれている
Lc Data部の長さ
Data 署名用パスワード
Verifyコマンドのレスポンス値

Verifyコマンドは、レスポンスのSTATUS2で残り試行可能回数が分かるようになっています。

  • 成功時
STATUS1 STATUS2
90 00
  • 失敗時
STATUS1 STATUS2
63 C5〜C0

5回連続で間違えるとSTATUS2がC0となり、市区町村の窓口でリセットするまでマイナンバーカードが使用不可になります。

SelectFile(署名用秘密鍵)

署名用秘密鍵を選択する

CLA INS P1 P2 Lc DATA
00 A4 02 0C 02 001A

コマンド詳細

フィールド 備考
CLA 00
INS 0xA4 SelectFileコマンド
P1 0x02 Data部にEF(Elementary file)の識別子が含まれている
P2 0x0C file control information option
Lc 0x02 Data部の長さ
Data 0x00,0x1A署名用秘密鍵の識別子

Compute Digital Signature

ハッシュ値を入力して、署名を生成する

CLA INS P1 P2 Lc DATA Le
80 2A 00 80 33 DigestInfo 0

コマンド詳細

フィールド 備考
CLA 0x80
INS 0x2A Compute Digital Signatureコマンド
P1 0x00 固定値
P2 0x80 鍵値は選択中のEFとする
Lc 0x33 Data部の長さ
Data OID付きのハッシュ値
Le 0 レスポンス長はカード側で規定
Compute Digital Signatureのレスポンス
署名値 STATUS1 STATUS2
計算された署名値 90 00

これで、署名値を生成できました。

取得した署名値はOpenSSLなどを使って検証可能です。

電子署名

公開鍵基盤を使用する電子証明

公的個人認証サービス(PKI, Japanese Public Key Infrastructure)は、公開鍵基盤(Public Key Infrastructure)を使用する電子署名サービスです。

公開鍵基盤では、まず認証局(Certification Authority)があり、認証局により利用者が認証されます。(マイナンバーカードの申請と発行)

マイナンバーカードには、JPKIの認証局から発行された証明書(公開鍵)と、証明書とペアになる秘密鍵が内蔵されています。

利用者は、秘密鍵で文書を署名し、受信者は公開鍵で文書の真正性を検証します。

また、公開鍵自体の有効性は、CAに問い合わせる事で検証可能です。

sequenceDiagram
participant 利用者
participant 受信者
participant 認証局(CA)

認証局(CA) ->> 利用者: マイナンバーカード(秘密鍵、公開鍵)
利用者 -> 利用者: 秘密鍵で文書を署名
利用者 ->> 受信者: 文書、署名、公開鍵
受信者 ->> 認証局(CA): 利用者の証明書が失効されていない事を確認
認証局(CA) -->> 受信者: 有効
受信者 ->> 受信者: 公開鍵を使用して文書を検証

署名値の検証

証明書公開鍵の読み出し

JPKI利用者ソフトなどを利用して、証明書公開鍵を読み出しておきます。

なお、マイナンバーカードから秘密鍵を取り出す事は不可能です。

秘密鍵が漏洩しない物理カードや物理トークンは、パスワードに比べて強力な認証手段になります。 (分解すると自動的にメモリが消えるなど、ハードウェア的な対策も施されています。)

署名値の検証

マイナンバーカードから証明書を取り出す

マイナンバーカードから、署名用証明書を取り出します。

ISO7816-4のREAD BINARYコマンドでも取り出せますが、JPKI利用者ソフトを使用すると簡単に取り出せます。

JPKI利用者ソフト

マイナンバーカードから証明書を取り出す手順

  1. 自分の証明書を確認する
  2. 署名用電子証明書を選択
  3. マイナンバーカードをAndroid端末にかざして、証明書を読み出す
  4. ファイル出力

これで秘密鍵を含まない証明書がファイルに保存されます。

メール等でPCに転送して利用可能です。

証明書をPEM形式の公開鍵に変換する

マイナンバーカードから取り出した証明書はDER形式(バイナリ)なので、PEM形式(テキスト)に変換します。

> openssl x509 -pubkey -in CertUserSign2022XXXXXXXXXX.cer -inform der -noout -out pubkey.pem

検証する

正規化後のファイルを指定して検証します。(正規化していない場合は、元ファイルを指定します)

> openssl dgst -sha256 -verify pubkey.pem -signature signature_202208221512.bin 正規化後のファイル.xml
> Verified OK

終わりに

AndroidのNFC-APIのみで署名の生成ができました。

NFC-APIを直接使えば、UIやUXの自由度を高められますね。

求人

弥生では、Androidに興味のあるモバイルエンジニアを募集しています。

herp.careers

参考にしたWEBサイト