マイナンバーカード検証#2 - 利用者証明用電子証明書

# はじめに
マイナンバーカードの検証の話です。
[#1](https://qiita.com/gebo/items/6a334b5453817a587683)からの続きです。

`利用者証明用電子証明書に関するFILE`を検証します
公開鍵の取得、秘密鍵で署名、署名の検証です。

# 目次
[１．認証用証明書の取得](#１認証用証明書の取得)
[２．認証用証明書から公開鍵を取り出す](#２認証用証明書から公開鍵を取り出す)
[３．署名](#３署名)
[４．検証](#４検証)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/252291/4fe77f3c-8d8d-0235-060c-73443b92fe2d.png)


# １．認証用証明書の取得
認証用証明書をSELECTして証明書を読み出します。この証明書はPINのロックがないので簡単です。
以下のおまじない(APDU)をSCardTransmitで送信していきます。

##（１）SELECT FILE 公的個人認証AP

- APDU = `0x00, 0xA4, 0x04, 0x0C, 0x0A, 0xD3, 0x92, 0xF0, 0x00, 0x26, 0x01, 0x00, 0x00, 0x00, 0x01`
- Response = `0x90, 0x00` ← 正常終了という意味

##（２）SELECT FILE 認証用証明書

- APDU = `0x00, 0xA4, 0x02, 0x0C, 0x02, 0x00, 0x0A`
- Response = `0x90, 0x00` 

##（３）READ BINARY 

認証用証明書へのSELECT FILEが成功したら証明書の読み出し(READ BINARY)となります。
証明書はDER形式のバイナリデータで私のカードの場合1,583byteありました。
READ BINARYのポイントは二つ

- 最初にDER証明書全体のサイズを知る必要がある。
- 1回のREAD BINARYは256バイトまでしかできない。

### DER証明書全体のサイズを求める

DER証明書はASN.1という形式になっており、これが[Tag]-[Length]-[Value]構造の繰り返しになっています。なので、まずは、LengthをGETしてDER証明書全体のサイズを求めます。
結論を言うと最初の4byteをREADすればおｋ。

- APDU = `0x00, 0xB0, 0x00, 0x00, 0x04`
- Response = `0x30, 0x82, 0x06, 0x2B, 0x90, 0x00` 

APDUの`0x04`が4byte READ する、ということです。
Responseの後方`0x90, 0x00`が成功した、という意味で、`0x30, 0x82, 0x06, 0x2B`がREADした4byteのBINARYです。

READしたBINARYをANS.1の形式でパースします。この部分は[Tag]-[Length]の部分。

`0x30, 0x82, 0x06, 0x2B`

- `0x30` = Tag っていう意味
- bitにすると0011-0000
	- b8-b7 = 00
		- クラス = 汎用 っていう意味
	- b6 = 1
		- 構造化フラグ = 構造型
	- b5-b1 = 0x10 
		- タグ番号 = SEQUENCE(ASN.1 オブジェクトの集合を表記するための型)

- `0x82` = Length
- bitにすると1000-0010
	- b8 = 1
		- Lengthが128オクテット(byte)以上ある
		- ここが0だと、この1byteの残り7bitでLentghとなる
	- b7-b = 0x02
		- 長さ部の長さ = 2byte
		- **ここが3以上になることは無い** という決めつけをする
- `0x06, 0x2B` = Length
	- Big Endian の short型の数値
	- ということで **1579 byte**

### 1回のREAD BINARYは256バイトまでしかできない

ブロックサイズが256byteでブロック単位でしか読めないというきわめてハード的なお約束があります。
なんで、何回かに分けてREADします。READするブロックNoを指定するため、APDUを少し変えて何発も送信する必要があります。

- Block0
	- APDU = `0x00, 0xB0, 0x00, 0x00, 0x00`
	- Response = `…256byte…, 0x90, 0x00` 
- Block1
	- APDU = `0x00, 0xB0, 0x01, 0x00, 0x00`
	- Response = `…256byte…, 0x90, 0x00` 
- Block2
	- APDU = `0x00, 0xB0, 0x02, 0x00, 0x00`
	- Response = `…256byte…, 0x90, 0x00` 

・・・

APDUの3byte目に0～のブロック番号を指定します。こんな感じで256byteずつREADしていきます。
こうしてDER形式の証明書をGETします。

ちゃんとGETできたかどうか、確認したいですね。
OpenSSLで証明書の中身を見ることができます。

```DOS:コマンドプロンプト

・・・READ_BINARYしたデータをAuth_Cert.derというファイルで保存しておく

c:\work>openssl x509 -in Auth_Cert.der -inform DER -out Auth_Cert.pem -outform PEM
c:\work>openssl x509 -text -fingerprint -noout -in Auth_Cert.pem

・・・証明書の中身が人間にもわかる形式でダラダラと表示されればOK・・・

```

#２．認証用証明書から公開鍵を取り出す
署名検証では公開鍵が必要になります。公開鍵は先ほどGETした証明書の中に入っています。
証明書の中から公開鍵だけを取り出さねばなりません。

C#では`System.Security.Cryptography.X509Certificates.X509Certificate2`クラスの`GetPublicKey`メソッドで簡単に公開鍵を取り出すことができます。
しかしトラップがあります。

`GetPublicKey`でGETした公開鍵はOpneSSLでは取り扱うことができません。
`GetPublicKey`メソッドではPKCS#1形式という形式で取り出すようで、OpenSSLで取り扱うにはPKCS#8形式でないとダメなようです。これは非常に不便です。

PKCS#1⇒PKCS#8変換の方法を調べてWebを彷徨いましたがよい方法が見つからず、あきらめて思考停止手段を用いることにしました。

**PKCS#1とPKCS#8の公開鍵のDiffをとって差分を固定で埋める。**

すなわちPKCS#1のデータに
`30820122300d06092a864886f70d01010105000382010f00`
という謎のヘッダを付加する。
これでうまくいく。なぜうまくいくかは知らない。

```CS

List<byte> pubkey_pkcs8 = new List<byte>();
var x509 = new System.Security.Cryptography.X509Certificates.X509Certificate2(certDER);
byte[] pubkey_pkcs1 = x509.GetPublicKey();

// ここで取れるpubkey_pkcs1はPKCS#1形式の公開鍵
// 先頭に
// 30820122300d06092a864886f70d01010105000382010f00
// を付加するとOpenSSLで取り扱い可能なPKCS#8になる

pubkey_pkcs8.AddRange(Common.HexStringToBytes("30820122300d06092a864886f70d01010105000382010f00").ToArray());
pubkey_pkcs8.AddRange(pubkey_pkcs1.ToArray());

byte[] publickeyDER = pubkey_pkcs8.ToArray();

```
[ソースはこちら](https://github.com/gebogebogebo/JPKIReader/blob/master/Source/JPKIReader/JPKIReader/JPKIReader.cs)

Webを彷徨っていてみつけた手掛かり⇒[RSA公開鍵のファイル形式とfingerprint](https://qiita.com/hotpepsi/items/128f3a660cee8b5467c6)

ちなみにOpenSSLで **DER形式** のPublicKeyを確認するコマンド

```DOS:PublicKeyをOpenSSLで見るコマンドDER形式の場合

・・・READ_BINARYしたデータをAuth_PublicKey.derというファイルで保存しておく

c:\work>openssl rsa -pubin -in Auth_PublicKey.der -text -noout -inform der

Public-Key: (2048 bit)
Modulus:
    00:8f:e0:2a:24:45:51:40:77:b0:01:fa:01:48:fb:
    ・・・
ってかんじで表示されればOK

```

ついでに **PEM形式** の場合

```DOS:PublicKeyをOpenSSLで見るコマンド【PEM形式の場合】
c:\work>openssl rsa -pubin -in Auth_PublicKey.pem -text -noout

Public-Key: (2048 bit)
Modulus:
    00:8f:e0:2a:24:45:51:40:77:b0:01:fa:01:48:fb:
    ・・・
ってかんじで表示されればOK

```
# ３．署名
『とある電子データについての電子署名を作成する』ことです。マイナンバーカードではそれができるのです。自分のハンコを押すみたいなものです。
電子署名に自分の個人情報が入ることはありませんので安心してください。

今さっき「マイナンバーカードでは電子署名を作成することができる」としましたが、厳密には間違いです。
マイナンバーカードでできるのは、「与えられたデータを秘密鍵で暗号化する」ことだけです。
認証用鍵にSELECT FILEしてからCOMPUTE DIGITAL SIGNATUREコマンドで認証用鍵で暗号化します。
ただし、認証用鍵にアクセスするにはPINロックの解除が必要です。先にPINロックを解除してから認証用鍵にSELECT FILEしましょう。

##（１）SELECT FILE 公的個人認証AP

- APDU = `0x00, 0xA4, 0x04, 0x0C, 0x0A, 0xD3, 0x92, 0xF0, 0x00, 0x26, 0x01, 0x00, 0x00, 0x00, 0x01`
- Response = `0x90, 0x00`

##（２）SELECT FILE 認証用PIN

- APDU = `0x00, 0xA4, 0x02, 0x0C, 0x02, 0x00, 0x18`
- Response = `0x90, 0x00`

## （３）VERIFY 認証用PIN

※PINが1234の場合

- APDU = `0x00, 0x20, 0x00, 0x80, (※1)0x04, (※2)0x31, 0x32, 0x33, 0x34`
- Response = `0x90, 0x00`

※1＝PINのバイト数、※2＝PINデータを指定します。
成功するとPINロックが解除されて認証用鍵にアクセスできるようになります。
PINが間違っている場合はResponse=`0x63, 0xC?`といった値が返ってきます。このとき`?`にロックまでのリトライ回数が返ってきます。

**認証用PINのリトライは3回です。3回間違うとロックされてしまいます。復旧は役所に行って申請書書かないといけいないので注意してください。**
ちなみに認証成功するとまたリトライが3回に戻ります。

リトライ回数だけ知りたいですね。その場合はPINデータをセットしないでVERIFYします。

- APDU = `0x00, 0x20, 0x00, 0x80`
- Response = `0x63, 0xC3`

この場合リトライ回数を減らさずにリトライ回数がGETできます。

## （４）SELECT FILE 認証用鍵

- APDU = `0x00, 0xA4, 0x02, 0x0C, 0x02, 0x00, 0x17`
- Response = `0x90, 0x00`

## （５）COMPUTE DIGITAL SIGNATURE

このコマンドでデータを暗号化します。
直前にSELECT FILEしている`認証用鍵`で暗号化します。

- APDU = `0x80, 0x2A, 0x00, 0x80, (※1), (※2)`
- Response = `(※3), 0x90, 0x00`

※1＝対象データのバイト数(1byte)、※2＝対象データを指定します。
※3＝暗号化されたデータ

## 電子署名について

COMPUTE DIGITAL SIGNATUREで暗号化しますが、いわゆる電子署名にするには`PKCS #1 (RFC 3447)`の仕様の通りにする必要があります。
これがまためんどくさいのですが、仕方ありません。以下の手順でRFCに準拠したDigestInfoを作成します。

### ①電子署名を作成するデータのSHA1を求める

```CS
byte[] digest = null;
using (var fs = new System.IO.FileStream(targetFile, System.IO.FileMode.Open, System.IO.FileAccess.Read)) {
    digest = System.Security.Cryptography.SHA1.Create().ComputeHash(fs);
}
```

### ②ASN.1 DigestInfoを作成

DigestInfoなるデータの構造

```
DigestInfo::= SEQUENCE {
  SEQUENCE {
    OBJECT IDENTIFIER / SHA1(1,3,14,3,2,26)
    NULL
  }
  OCTET STRING digest
}
```

この構造でバイナリデータを作成します。ASN.1のことを多少知っとく必要があります。
説明すると大変なので、以下の説明を参照ください。

- [電子署名のファイルフォーマット](https://qiita.com/kunichiko/items/2e0a2bd35c8e9492ceb5)
- [図説RSA署名の巻](http://blog.livedoor.jp/k_urushima/archives/979220.html)
- [GitHubのソース](https://github.com/gebogebogebo/JPKIReader/blob/master/Source/JPKIReader/JPKIReader/JPKIReader_private.cs)

### ③COMPUTE DIGITAL SIGNATUREする
かなり端折ってしまいましたが、こうして生成されたDigestInfoをCOMPUTE DIGITAL SIGNATUREコマンドに指定してGETしたデータが電子署名です。

# ４．検証

署名は検証(Verify)するためにあります。検証してみましょう。

「とっても大事な文書.pdf」というファイルの署名を[前述の手順](#３署名)で作成し、「とっても大事な文書の署名.sig」というファイルで保存します。
さらに、[前述の手順](#２．認証用証明書から公開鍵を取り出す)で取り出した公開鍵を「`Authentication_PublicKey.der`」というファイルで保存します。

- とっても大事な文書.pdf
- とっても大事な文書の署名.sig
- Authentication_PublicKey.der

これらの情報があれば、Verifyの準備は完了です。
Verifyにはマイナンバーカードは必要ありません。

### OpenSSLでVerifyする
コマンド2発です。
1発目でDER⇒PEM変換、2発目でVERIFYです。

```DOS:Verify
c:\work>openssl rsa -pubin -in Authentication_PublicKey.der -inform der -out Authentication_PublicKey.pem
writing RSA key

c:\work>openssl dgst -sha1 -verify Authentication_PublicKey.pem -signature とっても大事な文書.sig とっても大事な文書.pdf
Verified OK
```

### C#のプログラムででVerifyする

RSACryptoServiceProviderクラスのVerifyHashメソッドです。
手順をここで書くとまた長くなるので以下のソースを参照ください。
[GitHubのソース](https://github.com/gebogebogebo/JPKIReader/blob/master/Source/JPKIReader/JPKIReader/Verify.cs)
↑
`public static bool VerifySignature(byte[] publicKeyDER,byte[] signature,string targetFile)`
がエントリポイントです。

- とっても大事な文書.pdf　⇒　`targetFile`
- とっても大事な文書の署名.sig　⇒　`signature`
- Authentication_PublicKey.der　⇒　`publicKeyDER`

として見てください。

参考
[C#でRSA暗号を使って署名や暗号化する](https://qiita.com/rawr/items/9492e7fc93be7ca99ac1)

# おつかれさまでした

もうすこしつづく
⇒[#3](https://qiita.com/gebo/items/37fcb50565c5ebeacb7b)