はじめに
弥生モバイルチームのtijinsです。
弥生ではマイナンバーカードを使用して確定申告書類に電子署名を行う弥生 電子署名アプリを公開しています。
開発時にNFCやマイナンバーカードに関する情報が少なく困った為、調査結果を共有したいと思います。
- はじめに
- e-Taxと電子署名
- マイナンバーカード
- マイナンバーカードによる電子署名
- 電子署名
- 終わりに
- 求人
- 参考にしたWEBサイト
e-Taxと電子署名
e-Taxとは、確定申告などの各種手続をインターネットを通じて行うことができる国税庁のサービスです。 e-Taxでは、申告書類(xmlファイル)に電子署名する事で真正性の検証を行っています。
マイナンバーカード
マイナンバーカードはNFC-TypeB(ISO14443B)の仕様に準拠した非接触ICカードです。
非接触ICカードは、非接触で読み書きできるストレージとしてSuicaやWAONなどの電子マネーとして使われていますが、マイナンバーカードはデータの保存だけではなく、署名の計算もできるようになっています。 また、ファイルシステムがあり、以下のように複数のアプリケーションが搭載されています。
アプリ | 用途 |
---|---|
券面事項確認AP | 券面の情報が画像として保管されている |
券面事項入力補助AP | 券面記載事項がテキストとして保管されている |
公的個人認証AP | ファイルを入力して署名値を生成する |
その他・・・ | 自治体や企業が独自のアプリを追加して使用する |
マイナンバーカードによる電子署名
e-Taxでは、マイナンバーカードに搭載された公的個人認証APを使用して申告用のxmlファイルに署名を付与します。
e-Taxではパスワード認証方式も利用可能ですが、マイナンバーカードを使用すると2要素認証(カード+パスワード)となり、より安全になります。
資料の入手
マイナンバーカードを利用するアプリの情報は、あまり公開されていません。
以下のサイトが公式ですが、インターネット上に公開されている情報の方が充実しています。
インターネット上の情報のみでも目的のアプリは作れるのですが、エラーコードなど不明な部分も多く、商用アプリとしては不安があった為、地方公共団体情報システム機構に問い合わせてみました。 ダメ元だったのですが、とても簡単に通信仕様書を入手できました。
入手した通信仕様書はNDA対象なので、ここではインターネット上に公開されている情報のみを元に説明していきます。
電子署名の仕様
マイナンバーカードに搭載されている公的個人認証APを使用すると署名を生成可能です。
公開されている仕様書によるとRSAwithSha256、またはRSAwithSha1に対応しています。
弥生の電子署名アプリでは信頼性の高いSHA256の方を使用しています。
マイナンバーカードで電子署名を行う手順
- 申告文書xmlの正規化
- 正規化した申告文書xmlのハッシュ化
- ハッシュ値にOIDを追加する
- OID付きのハッシュ値をマイナンバーカードに送信
- 署名値を取得
申告書類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利用者ソフトを使用する
- AndroidのNFC-APIを直接使用する
実装が簡単なのはJPKI利用者ソフトを使用する方法ですが、別アプリのインストールが必要になる為、ユーザービリティは良くありません。
ユーザビリティが良くないからなのか、公式のe-Taxでもワクチン接種証明アプリでも使われていません。
ここでは、NFC-APIを使う方法で説明していきます。
AndroidのNFC API
AndroidにNFC関連の機能が追加されたのはAndroid 2.2.3(GingerBread)と古く、殆ど改良されていない為、あまり使いやすいものではありません。
マイナンバーカードを利用するアプリの基本的な実装
- ForegroundDispathでNFC-TypeBのカードを検知するように指定
- onNewIntent()内でIntentから
Tag
を取得し、Tag
からIsoDep
オブジェクトを生成する(NfcBではないので注意) IsoDep.connect()
を実行するIsoDep.transceive()
を使用してマイナンバーカードに指定の順序でコマンドを送信する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利用者ソフトを使用すると簡単に取り出せます。
マイナンバーカードから証明書を取り出す手順
- 自分の証明書を確認する
- 署名用電子証明書を選択
- マイナンバーカードをAndroid端末にかざして、証明書を読み出す
- ファイル出力
これで秘密鍵を含まない証明書がファイルに保存されます。
メール等で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に興味のあるモバイルエンジニアを募集しています。