Nodejsでマイナンバーカードを使ったログインと認証をやってみる(PKCS11js,X509,OpenSC,crypto)

# はじめに
<!-- 発端や概要を記載 -->
電子証明書には署名用と利用者証明書用の2種類があり、pkcs11jsを使えばマイナンバーカードから利用者証明書用電子証明書を取得することができます。

利用者証明書は「ログインした者が、利用者本人であること」を証明し、インターネットサイトなどにログインする際のセキュリティをより強固なものにできます。

今回はコマンドライン上のみの実行ですが、マイナンバーカードを使った認証サーバーを開発することをイメージしながらコーディングしています。

# 目次
<!-- タイトルとアンカー名を編集 -->
1. [PKCS11jsの読み込み](#pkcs11js)
1. [認証のチャレンジデータを生成する](#Chapter1)
1. [カード内の秘密鍵でチャレンジデータに署名](#Chapter2)
1. [カードから利用者証明書を取得](#Chapter3)
1. [利用者証明書を検証する](#Chapter4)
1. [利用者証明書の公開鍵で署名を検証する](#Chapter5)
1. [利用者証明書の属性を印字する](#Chapter6)
1. [参考文献](#reference)

<!-- 各チャプター -->

# 実行環境
MacOS 12.3.1
<a id="#pkcs11js"></a>
# 使用したライブラリ
|library |    version  |  
|---:|-------------|
| PKCS11js |2.3|            
| SoftHSMv2 |10.12.3|            
| nodejs |v18.0.0|  

pkcs11jsとcryptoは以下のコマンドでインストールできます。
```
npm install pkcs11js crypto --save
```
SoftHSMv2のインストールは[SoftHSMv2](https://github.com/opendnssec/SoftHSMv2/blob/develop/OSX-NOTES.md)を参照してください。

<a id="#cardreader"></a>
# 使用したカードリーダー
[ZoWEETEK ICカードリーダー](https://www.amazon.co.jp/gp/product/B09J4MKS55/ref=ppx_yo_dt_b_asin_title_o07_s00?ie=UTF8&psc=1)

<!-- 各チャプター -->
<a id="#pkcs11js"></a>
# 1. pkcs11jsの読み込み
```my_number.js
pkcs11js = require("pkcs11js");

var pkcs11 = new pkcs11js.PKCS11();

//以下のURLを参考にしながら/usr/local/pkcs11にある共有オブジェクト(opensc-pkcs11.so)のloadを行なった
//https://github.com/OpenSC/OpenSC/wiki/macOS-Quick-Start
//https://pointsandlines.jp/env-tool/mac/usr-pass
pkcs11.load("/usr/local/lib/pkcs11/opensc-pkcs11.so");

pkcs11.C_Initialize();

try {

  // Getting list of slots
  var slots = pkcs11.C_GetSlotList(true);
  var slot = slots[0];


  var session = pkcs11.C_OpenSession(slot, pkcs11js.CKF_RW_SESSION | pkcs11js.CKF_SERIAL_SESSION);

  pkcs11.C_Login(session, 1, "my_number_password");

  //この中にマイナンバー認証するためのコードを記載していく

  pkcs11.C_Logout(session);
  pkcs11.C_CloseSession(session);
 } 
  catch(e){
   console.error(e);
  }
  finally {
   pkcs11.C_Finalize();
  }
```
my_number_passwordはマイナンバーカード発行時に作成した4桁のパスワードを記載する必要があります。

<a id="#Chapter1"></a>
# 2. 認証用のチャレンジデータを生成する(base64エンコード)
署名用にBase64で乱数を生成します。
```my_number.js
 // 認証のチャレンジデータを作成する
  const crypto = require('crypto');
  const N = 16;
  const challenge = crypto.randomBytes(N).toString('base64').substring(0, N);
```

<a id="#Chapter2"></a>
# 3. カード内の秘密鍵ハンドルでチャレンジデータに署名
先ほど作成したチャレンジデータに、マイナンバーカードの中にある秘密鍵ハンドルで署名していきます。
```my_number.js
// 秘密鍵ハンドルを取得する
  pkcs11.C_FindObjectsInit(session, [{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_PRIVATE_KEY }]);
  
  var hObject = pkcs11.C_FindObjects(session);
  while (hObject) {
      var attrs1 = pkcs11.C_GetAttributeValue(session, hObject, [
          { type: pkcs11js.CKA_CLASS },
          { type: pkcs11js.CKA_TOKEN },
          { type: pkcs11js.CKA_LABEL }
      ]);
      // Output info for objects from token only
      if (attrs1[1].value[0]){
          console.log(`Object #${hObject}: ${attrs1[2].value.toString()}`);
      }
      privateKey = hObject;
      hObject = pkcs11.C_FindObjects(session);
  
  }
  
  pkcs11.C_FindObjectsFinal(session);


  // 秘密鍵を使って署名する
  pkcs11.C_SignInit(session, { mechanism: pkcs11js.CKM_SHA256_RSA_PKCS }, privateKey);

  pkcs11.C_SignUpdate(session, Buffer.from(challenge));

  var signature = pkcs11.C_SignFinal(session, Buffer.alloc(256));
```

<a id="#Chapter3"></a>
# 4. カードから利用者証明書を取得
利用者証明書用電子証明書をマイナンバーカードから取り出します。
```my_number.js
// 利用者証明書を取得する
  pkcs11.C_FindObjectsInit(session, [{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_CERTIFICATE }, { type: pkcs11js.CKA_LABEL, value: "User Authentication Certificate" }]);
  
  var hObject = pkcs11.C_FindObjects(session);
  while (hObject) {
      var attrs2 = pkcs11.C_GetAttributeValue(session, hObject, [
          { type: pkcs11js.CKA_CLASS },
          { type: pkcs11js.CKA_TOKEN },
          { type: pkcs11js.CKA_LABEL },
          { type: pkcs11js.CKA_VALUE }
      ]);
      // Output info for objects from token only
      if (attrs2[1].value[0]){
          console.log(`Object #${hObject}: ${attrs2[2].value.toString()}`);
      }
      hObject = pkcs11.C_FindObjects(session);
  }
  pkcs11.C_FindObjectsFinal(session);


  const { X509Certificate } = require('crypto');

  const x509 = new X509Certificate(attrs2[3].value);
```


<a id="#Chapter4"></a>
# 5. 利用者証明書を検証する
```JPKI.js
pkcs11js = require("pkcs11js");

var pkcs11 = new pkcs11js.PKCS11();

pkcs11.load("/usr/local/lib/pkcs11/opensc-pkcs11.so");

pkcs11.C_Initialize();

try {

  // Getting list of slots
  var slots = pkcs11.C_GetSlotList(true);
  var slot = slots[0];

  var session = pkcs11.C_OpenSession(slot, pkcs11js.CKF_RW_SESSION | pkcs11js.CKF_SERIAL_SESSION);

  pkcs11.C_Login(session, 1, "my_number_password");


  // JPKIの証明書を取り出す
  pkcs11.C_FindObjectsInit(session, [{ type: pkcs11js.CKA_CLASS, value: pkcs11js.CKO_CERTIFICATE }, { type: pkcs11js.CKA_LABEL, value: "User Authentication Certificate CA" }]);
  
  var hObject = pkcs11.C_FindObjects(session);

  while (hObject) {
      var attrs = pkcs11.C_GetAttributeValue(session, hObject, [
          { type: pkcs11js.CKA_CLASS },
          { type: pkcs11js.CKA_TOKEN },
          { type: pkcs11js.CKA_LABEL },
          { type: pkcs11js.CKA_VALUE }
      ]);
      // Output info for objects from token only
      if (attrs[1].value[0]){
          console.log(`Object #${hObject}: ${attrs[2].value.toString()}`);
      }
      hObject = pkcs11.C_FindObjects(session);
  }
  pkcs11.C_FindObjectsFinal(session);

  const { X509Certificate } = require('crypto');

  const x509 = new X509Certificate(attrs[3].value);

  //JSONではなく、PEM形式の証明書が取得できる
  //https://www.geeksforgeeks.org/node-js-x509-tojson-method/
  JPKI = x509.toJSON();

  var fs = require("fs");
  
  fs.writeFile('JPKI.txt', JPKI, (err, data) => {
    if(err) console.log(err);
    else console.log('write end');
  });

  pkcs11.C_Logout(session);
  pkcs11.C_CloseSession(session);
}
catch(e){
  console.error(e);
}
finally {
  pkcs11.C_Finalize();
}
```



```my_number.js
 // JPKIの証明書の属性を印字する

  const fs = require('fs');
  let JPKI = fs.readFileSync("JPKI.txt");

  const x509JPKI = new X509Certificate(JPKI);

  console.log("[subject]\n"+x509.subject);
  console.log("[validFrom]\n"+x509.validFrom);
  console.log("[validTo]\n"+x509.validTo);
  console.log("[issuer]\n"+x509.issuer);
```
このように出力され、利用者証明書はJPKIが発行しているということが分かります。
```
subject:
C=JP
O=JPKI
OU=JPKI for user authentication
OU=Japan Agency for Local Authority Information Systems
validFrom:Sep 14 23:41:59 2019 GMT
validTo:Sep 14 14:59:59 2029 GMT
issuer:
C=JP
O=JPKI
OU=JPKI for user authentication
OU=Japan Agency for Local Authority Information Systems
```

```my_number.js
  //利用者用証明書がJPKIによって発行されたものであるかの検証を行う
  JPKI_publicKey = x509JPKI.publicKey;

  //発行元が異なっている場合、エラーメッセージを出力する
  if(!x509.verify(JPKI_publicKey)){
    console.log("verify failed");
  }
```
エラーメッセージが出力されなければ利用者用証明書の検証は成功です。




<a id="#Chapter5"></a>
# 6. 利用者証明書の公開鍵で署名を検証する
利用者証明書の公開鍵で署名ができているか検証します。
```my_number.js
// 署名したものをx509の公開鍵で検証する
  publicKey = x509.publicKey;

  const {createVerify} = require('crypto');

  const verify = createVerify('RSA-SHA256');
  verify.update(challenge);
  verify.end();
  console.log("検証結果:"+verify.verify(publicKey, signature));
```
x509の公開鍵を使って検証するとtrueが表示されます。
```
検証結果:true
```
verify.update()の引数を変えるともちろんfalseになります。


<a id="#Chapter6"></a>
# 7. 利用者証明書の属性を印字する
```my_number.js
  // 利用者証明書の属性を印字する
  console.log("[subject]\n"+x509.subject);
  console.log("[validFrom]\n"+x509.validFrom);
  console.log("[validTo]\n"+x509.validTo);
  console.log("[issuer]\n"+x509.issuer);
```
マイナンバーカードの利用者証明書を取得できていることが確認できるはずです。




<a id="#reference"></a>
# 参考文献
- [利用者クライアントソフトAPI仕様書](https://www.j-lis.go.jp/data/open/cnt/3/854/1/02PKCS11.pdf)
- [マイナンバーカードと電子署名の本](https://www.cuspy.org/diary/2021-09-04-myna-book/myna-book.pdf)
- [PKCS11js](https://github.com/PeculiarVentures/pkcs11js)
- [Node.js v18.4.0 documentation](https://nodejs.org/api/crypto.html)
- [総務省公的個人認証サービスによる電子証明書](https://www.soumu.go.jp/kojinbango_card/kojinninshou-01.html)
- [JavaScriptでお手軽にランダム文字列の生成](https://qiita.com/fukasawah/items/db7f0405564bdc37820e)
- [macOS Quick start](https://github.com/OpenSC/OpenSC/wiki/macOS-Quick-Start)
- [Node.js x509.toJSON() Method](https://www.geeksforgeeks.org/node-js-x509-tojson-method)
- [Finderから/usr/local ディレクトリを参照する](https://pointsandlines.jp/env-tool/mac/usr-pass)