PKI.jsについて調べてみた

# はじめに

この記事は、2018/11/27現在における[GitHub](https://github.com/PeculiarVentures/PKI.js)のREADME.mdの内容をベースとして、[**PKI.js**](https://pkijs.org/)についてふわっとした紹介を行うものです。

より詳細な情報に関しては、[GitHub](https://github.com/PeculiarVentures/PKI.js)をご覧ください。
API仕様書はないものと思われます。。


# PKI.jsって何？

さっくり説明すると、「ブラウザ(JavaScript)で証明書の作成や署名の検証等いろいろ出来たら夢が広がるよね」を実現するのに役立つ**オープンソース**のライブラリです。

もう少しきちんと説明すると、PKIアプリケーション（署名、暗号化、証明書要求、OCSPおよびTSP要求/応答）を開発するためのJavaScriptライブラリで、[**Web Cryptography(WebCrypto) API**](https://www.w3.org/TR/WebCryptoAPI/)を使用しています。

[WebCrypto API](https://www.w3.org/TR/WebCryptoAPI/)自体が特にプラグイン等を必要としないので、[PKI.js](https://pkijs.org/)も同じくプラグイン等なしで動作するのですが、[WebCrypto API](https://www.w3.org/TR/WebCryptoAPI/)をサポートしていないブラウザでは当然ながら動作しないので注意が必要です。

ブラウザの対応状況に関しましては、[下記](#ブラウザの対応状況は)をご覧ください。


# ライセンスは？

BSDライセンス(厳密にはBSD 3-Clause License)が適用されているようです。

# 特徴は？

完全なオブジェクト指向です。

HTML5データオブジェクト(ArrayBuffer, Uint8Array, Promises等)を用いて動作します。

以下を扱うためのヘルパーを持っています。

  - GeneralName
  - RelativeDistinguishedName
  - Time
  - AlgorithmIdentifier
  - UniversalString, UTF8String, BMPStringのような "international" を含む、すべてのタイプのASN.1文字列
  - X.509証明書のすべての拡張タイプ(BasicConstraints, CertificatePolicies, AuthorityKeyIdentifier等)
  - OCSP要求・応答のためのすべての「サポートタイプ(support type)」
  - タイムスタンププロトコル(TSP)要求と応答のためのすべての「サポートタイプ(support type)」

以下のようなWebCrypt APIのすべての署名アルゴリズムに対応しています。

  - RSASSA-PKCS1-v1_5
  - RSA-PSS
  - ECDSA

以下のようなすべての "Suite B"(とそれ以上の)暗号化アルゴリズムとスキーマに対応しています。

  - RSASSA-OAEP + AES-KW + AES-CBC/GCM
  - ECDH + KDF on SHA-1/256/384/512 + AES-KW + AES-CBC/GCM
  - 事前に定義された「キー暗号化キー」+ AES-KW + AES-CBC/GCM
  - PBKDF2 on HMAC on SHA-1/256/384/512を用いたCMSのパスワードベースの暗号化 + AES-KW + AES-CBC/GCM


# 何ができるの？

マイナーなものを含めるともっと色々あるようですが、PKIに関連するメジャーな仕様とできることは以下の通りです。

- [X.509証明書](https://tools.ietf.org/html/rfc5280)
  - 証明書内の値のパース・取得・設定
  - 証明書の作成(スクラッチで)
  - 内部証明書チェーンの検証エンジンあり
- [X.509証明書失効リスト(CRL)](https://tools.ietf.org/html/rfc5280)
  - CRL内の値のパース・取得・設定
  - CRLの作成(スクラッチで)
  - CRL署名の検証
  - 任意の証明書がCRL内にないか検索
- [PKCS#10 認証要求](https://tools.ietf.org/html/rfc2986)
  - 認証要求内の値のパース・取得・設定
  - 認証要求の作成(スクラッチで)
  - PKCS#10 署名の検証
- [OCSP要求](https://tools.ietf.org/html/rfc6960)
  - OCSP要求内の値のパース・取得・設定
  - OCSP要求の作成(スクラッチで)
- [OCSP応答](https://tools.ietf.org/html/rfc6960)
  - OCSP応答内の値のパース
  - OCSP応答内の値の取得・設定
  - OCSP応答の作成(スクラッチで)
  - OCSP応答の署名検証
- [タイムスタンプ要求](https://tools.ietf.org/html/rfc3161)
  - タイムスタンプ要求内の値のパース・取得・設定
  - タイムスタンプ要求の作成(スクラッチで)
  - タイムスタンプ要求の署名検証
- [タイムスタンプ応答](https://tools.ietf.org/html/rfc3161)
  - タイムスタンプ応答内の値のパース・取得・設定
  - タイムスタンプ応答の作成(スクラッチで)
  - タイムスタンプ応答の署名検証
- [CMS署名データ(Signed Data)](https://tools.ietf.org/html/rfc5652)
  - CMS署名データ内の値のパース・取得・設定
  - CMS署名データの作成(スクラッチで)
  - CMS署名データの署名検証
- [CMSエンベロープデータ(Enveloped Data)](https://tools.ietf.org/html/rfc5652)
  - CMSエンベロープデータ内の値のパース・取得・設定
  - Suite Bアルゴリズム等を完全にサポートした作成(暗号化)
  - Suite Bアルゴリズム等を完全にサポートした復号
- [CMS暗号化データ(Encrypted Data)](https://tools.ietf.org/html/rfc5652)
  - CMS暗号化データ内の値のパース・取得・設定
  - パスワードを用いたCMS暗号化データの作成(暗号化)
  - パスワードを用いた復号
- [PKCS#12 個人情報交換構文](https://tools.ietf.org/html/rfc7292)
  - PKCS#12形式ファイル内の値のパース
  - 任意の種類のパラメータを使用した、任意の種類の内部の値（SafeContext / SafeBags）の作成

# 実際に試すには？

[PKI.jsのサイト](https://pkijs.org/)の下の方に「EXAMPLES」としてサンプルページが並んでいますので、そこで色々と試すことができます。
時間がかかっているだけかもしれないので、反応がないからと言ってボタンを連打するのは推奨しません。

ローカル環境にHTMLやJavaScriptのファイルを配置して試したい場合は、[PKI.jsのサイト](https://pkijs.org/)からダウンロードしたアーカイブを展開し、展開したディレクトリで以下のように実行することでサンプル(examplesディレクトリ以下に各種ファイルあり)をビルドすることができます。

```
$ npm install
$ npm run build:examples
```

# ブラウザの対応状況は？

お使いのブラウザの[WebCrypto API](https://www.w3.org/TR/WebCryptoAPI/)への対応状況は[このページ](https://peculiarventures.github.io/pv-webcrypto-tests/)で確認することができます。実際に実行しているようで、少し時間がかかります。
(「Select crypto module」は「JavaScript」とした上で実行してください。)

ブラウザ毎の対応状況は[このページ](https://caniuse.com/#feat=cryptography)で確認できます。


# やってみた

[PKI.jsのサイト](https://pkijs.org/)からダウンロードできるアーカイブの中には、X.509証明書と秘密鍵を作成するサンプル(CertificateComplexExample)が用意されています。

折角なので、ここで作成される秘密鍵を用いて署名を行う関数を書いてみました。実行すると署名値がコンソールに出力されます(16進数表記で)。

関数はes6.jsに追記する前提となっています。既存のコードを継ぎ接ぎしながら書いたので、もっとコンパクトにできたかもしれませんがご了承ください。(動作確認はCentOS上のFirefox v60.2.2esrとWindows上のFirefox v63.0.3で行いました。)

```javascript:es6.js
// import文の変更
import { getCrypto, getAlgorithmParameters, getEngine, setEngine} from "../../src/common.js";  // 元は「import { getCrypto, getAlgorithmParameters, setEngine } from "../../src/common.js";」でした

// 作成した関数
function signHelloWorld()
{
  if (privateKeyBuffer.byteLength == 0) {
    alert("秘密鍵が作成されていません。"); // 秘密鍵がない場合のアラート
    return;
  }

  // 変数の宣言
  let sequence = Promise.resolve();
  let parameters;
  let privateKey;
  const engine = getEngine();
  const algo = {
    name: signAlg,
    hash: {name: hashAlg}
  };
  const data = 'Hello, World!';
  const dataArray = (new TextEncoder).encode(data);

  // 秘密鍵をインポートする
  sequence = sequence.then(() => {
    return engine.subtle.importKey('pkcs8', privateKeyBuffer, algo, true, ['sign']);
  });

  // 秘密鍵のオブジェクトを変数に代入する
  sequence = sequence.then(key =>
  {
    privateKey = key;
  });

  // 署名パラメータを取得する
  sequence = sequence.then(() => {
    return engine.subtle.getSignatureParameters(privateKey, hashAlg);
  });

  // 署名パラメータを変数に代入する
  sequence = sequence.then(result =>
  {
    parameters = result.parameters;
  });

  // 署名を実行する
  sequence = sequence.then(() => {
    return engine.subtle.signWithPrivateKey(dataArray, privateKey, parameters);
  });

  // 署名値をコンソールへ出力する
  sequence = sequence.then(buffer => {
    let bufferArray = new Uint8Array(buffer);
    let str = Array.from(bufferArray).map(v => v.toString(16).padStart(2, "0").toUpperCase()).join('');
    console.log("署名値(16進数表記) = " + str);
  });
}
```

# 所感

API仕様書が見当たらない件について。。まあ、なくても何とかなるとは思いますが。

ソースを追っていくとかなり頻繁に遭遇するので、[WebCrypto API](https://www.w3.org/TR/WebCryptoAPI/)についても多少は勉強しておくと良さそうな印象でした。

# 参考文献

https://github.com/PeculiarVentures/PKI.js
http://blog.livedoor.jp/k_urushima/archives/1758899.html
https://qiita.com/masakielastic/items/8eb4bf4efc2310ee7baf