JSON Web Signature導入における鍵周りの基本的な考え方

# 概要

担当するプロダクトにおいて、いくつかの用途でJSON Web Signature(JWS)を利用してきました。
その経験を通して得られた知見から、JWSの署名生成/検証時に利用する鍵の管理を中心とした利用方針と実装例を紹介します。

# JWSとは

## JWSの仕様

JSON Web Token 周りは仕様がたくさんあることで有名です。
JWSについてはこの辺りを見る必要があるかと思います。

* [RFC7515 JSON Web Signature (JWS)](https://tools.ietf.org/html/rfc7515) [日本語訳](https://openid-foundation-japan.github.io/draft-ietf-jose-json-web-signature-14.ja.html) : 署名まわり
* [RFC7517 JSON Web Key (JWK)](https://tools.ietf.org/html/rfc7517) [日本語訳](https://openid-foundation-japan.github.io/rfc7517.ja.html) : 鍵まわり
* [RFC7519 JSON Web Token (JWT)](https://tools.ietf.org/html/rfc7519) [日本語訳](https://openid-foundation-japan.github.io/draft-ietf-oauth-json-web-token-11.ja.html) : 標準的な `Payload`

## JWSの使い所

ざっくりと

* 構造化されたデータがやり取りできる
* 署名付きで改ざんされにくいし、検知できる
* デコードしたら中身見れちゃう
* 有効期限とかもつけられる

という特徴を生かして、

* OAuth のリソースアクセスに用いるアクセストークン
* microservices 内のサービス間のやりとりで用いるトークン
* セッションと紐づくCSRF対策トークン
* アカウント登録など、複数の処理を必要とする機能で一連の流れを保証するためのトークン
* データストアと組み合わせてワンタイムトークン

などに利用 **できます**。

# JWS利用方針

ここからは、JWSを使っていくにあたり設計/実装方針として **キメ** たことを紹介していきます。
基本的な考えとして、下記の点を意識しています。

* 用途毎に鍵を分けよう
  * それぞれの鍵あたりの影響範囲を限定する
  * そのぶん、管理対象となる鍵の数は増えることは認識してる
* 複数のサービス/システムが `秘密鍵` を持たない
  * ２つのサービスで双方向のやりとりがある場合は、２組の鍵ペアを用意するぐらいのお気持ち
* そんなに頻繁にはないだろうけども、定期的、もしくは万が一の際の鍵交換も意識する

## JWSの構成要素について

JWSの仕様はいくらでも難しいことができるのですが、ここではよく使われている `JWS Compact Serialization` の利用を前提としています。

### Header

* 鍵の識別子として、必ず `kid` を含む
  * 命名ルール : `(用途)_201804_001` のような感じ
  * 用途について : 用途により鍵を分けるという基本的な考えがあるので、`kid` に用途も含める
* `alg` : 仕様上必須なので含む
  * `hsXXX` : 例えば `API Gateway` が発行/検証を両方やるケースで利用する
  * `rsXXX`, `exXXX` : 発行/検証が別のところで行われるケースで利用する

### Payload

RFC7519の `JWT Claims` で定義されている `iss`, `sub`, `aud`, `iat`, `exp` のような値が使えそうなものは使う。`iss`, `aud` あたりの検証で脆弱性が生まれることを耳にするが、ここでは細かく決めない。

それ以外のパラメータ定義や、そもそもJSONにすらなってない形式を使いたければ自由。

### Signature

仕様に従う。

## 処理について

利用する言語やライブラリが異なったりする場合もあるので、基本的な流れを確認できるようにしておく。

### 鍵の管理

* １つの鍵に対して、`kid`, `alg`, `鍵の値` のセットで管理する
* １つの用途に対し、複数の鍵を管理できるようにすることで、有効期限の長いJWSにおける鍵交換なども容易になる
* 鍵の種類によって値の保存方法を変える
  * `hsXXX` では、バイナリ形式の鍵をBase64エンコードして保存
  * `rsXXX`, `esXXX` では PEM　形式

### 発行

1. `Payload` 生成 : 用途によって変わる
2. `Header` 生成 : 発行時点で利用する鍵に対応する `kid` , `alg` を含む
3. `Signature` 生成 : `kid` に紐づく鍵を用いて生成

### 検証

1. JWTのフォーマットかどうかを検証(2の `Header` 取得で代替しても良い)
2. `Header` 検証 : **複数鍵の対応も考慮して** 有効な鍵のリストを用意し、`kid` に紐づくものがあるかどうかを検証
3. `kid` に紐づく `alg` の値と、 `Header` に含まれる `alg` が一致しているかを検証
4. `Signature` : `kid` に紐づく鍵とアルゴリズムを用いて署名検証

`kid` に対して `alg` と鍵が一意に紐づいている前提であれば3をスキップしても問題ないでしょう。
`alg` を書き換えられても署名検証でこけるだけになります。

### 鍵のやりとり

* `hsXXX` : 鍵のやりとりはしない。
* `rsXXX`, `esXXX` : `kid`, `alg`, `PEM形式の公開鍵`　を渡す

署名生成側が１箇所、検証側が複数であるときは、生成側が [RFC7517 Appendix A.  Example JSON Web Key Sets](https://tools.ietf.org/html/rfc7517#appendix-A) のような形で公開して検証側はリアルタイムもしくは定期的に参照することで(半)自動化することなども考えられる。

### 鍵交換

1. **生成側**が鍵ペアを生成、公開鍵を**検証側**に連絡
2. **検証側**が公開鍵を設定、検証可能にする
3. **生成側**が鍵を切り替える
4. トークンの有効期限が決まっているものであれば、**検証側**は以前の公開鍵で発行されたトークンが無効になるタイミングで古い鍵を設定から落とす

1の際に秘密鍵で生成したJWSを一緒に送ることで検証側の動作確認が容易になるかもしれない。

# こんなライブラリがあると良いみたいな実装

JWS 関連のライブラリで、一般的なのが

* 単一の鍵を用いて署名生成
* 単一の鍵を用いて署名検証
* 鍵のインポート/エクスポート

などの関数を実装するものです。

筆者は現時点で触れる機会が多いプログラミング言語が `Elixir` であり、JWS関連のライブラリとしては`JOSE`(`JOSE.JWK`, `JOSE.JWS`) の実装内容が幅広く、よく使っています。

上記方針を実装に落とすためには、

* 単一の鍵を用いて署名生成
* 複数の鍵(リスト)を用いて署名検証
* 鍵リストのインポート/エクスポート

といったあたりの関数があると捗ります。

実際のプロダクトで複数鍵対応として手を動かした中で得られた知見から、`JOSE` をちょいとだけラップしたライブラリを作りました。

[kitten_blue | Hex](https://hex.pm/packages/kitten_blue)

![s_kitten_blue.jpg](https://qiita-image-store.s3.amazonaws.com/0/10192/70d7fd7f-5ce6-b6b2-fab1-f9b79ef44f55.jpeg)
[キトンブルーの意味が知りたかったら検索！](https://www.google.co.jp/search?q=%E3%82%AD%E3%83%88%E3%83%B3%E3%83%96%E3%83%AB%E3%83%BC)

鍵の管理、署名生成/検証のちょっとしたサンプルを載せておきます。
※これらの鍵はライブラリにサンプルで載せたものなので公開しても問題ないです

```Elixir
# config とかにはこんな感じで書いておく
iex(1)> config = [
...(1)>   ["samplehs256_201804_001", "HS256",
...(1)>    "qTVOR3jMtu0iKDw1y0wMJsjEsXhO8RirCw9OF84NZPk"],
...(1)>   ["samplers256_201804_001", "RS256",
...(1)>    "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA44LvBo0ossKN6TqEY64MVW427j978f8TQwWjod1qcNb6yTLO\nMBm6z20Yu0hIYb58Dz/a00pImvWxS8ghnGcTiawM3t8AfSFcESsLe0j4h3bDD8+c\nEk+vvPYUTCobiUrC5unyoQg+izpDwkFZPaYvxhF0lSEUEg1glnIrdZcM74RG+wx/\nzs4Ti3xQ8abrCqMoKnPkLChszQQ1z5K1TVz4C9M0CA7BU3d1tLSLueb3OuQ/sn6i\nkuCXL7Fk20gCXTLNxpjnC95rfy5kt1hoMpG6j5Nhel5zG+LSz8u4BZhrbkCHo2Le\nH13srBHqcjBgB2hJfTjmapDV6OG6litjhS5dCwIDAQABAoIBADbfusUqwNk04cOS\nMKJWNXVZivg16bA5pKyyrH/7BpRG1eA3V9H0MsGa/68URRkGN6f5786siQyPG/86\nOxkdJ3I6RDRxNw33QNyHNFK9C+yocW3b1jn4bFu4QrcIJPIdTRsi7Dl44pT0Lamp\n11tuPPGZ/jOF8fBUt6oxyVeoYh9WpMjViP4q9Brwx8kPzj2LcB3sf3h6cA2A+M0x\nljx2VomAJwoSa9xPT2/RAM7SoBy2oo0QGfKuZKrG3w9G8gOhNOWYCHK9BuU8Ja8U\nwJIE9C0DS1y4NY3xl5oPKv9BLqhiiKTstFseTCCdZolRPzC+6e/tgEEVOiYkFite\n0l9KqXkCgYEA+W5miW00Cx++ydRFw2PRyekl6znpDA7V8aZolTbXdU3eDqYkpAJN\nwDiv5QFU27vfTqaa9yTDK3Mnt/YYq2bwA9MmhMms+isl/XQ7hCQJea8mIrCJVjN3\nC/3kmdZ92wMkEefBugCDHCWhiL3R141vLpUYeb+M25xpx4ELcPK7UY0CgYEA6YDB\ndwaZ0WQIgCMaZHnIpPVKMUpRxGj5/7dvOVXzDl1jxIv7irRwDKl4rk2gMsu5f9Rg\nlZHmsOuXwHTzdfNXl2RtP8xt0y0lp0t+t2jtHWzyPEcklD++dhd7Vj7XvlbZAXmo\n/cXVoMhcmsw6gaRjE5Kj9dEp/AaQycbmv+1m5vcCgYEArrtODTt418oXRx/y9xt4\nHS+8pnnc7dt+uFfQr6bJbJ1tz2lIBbSvbtbHMW+rWHrVxi0kJbmVF10GF/LH+VqV\nDLjgJOl6yY1sgY7pGnp8QMgXuPleXAqVfMsRV0tQwxoCLKDjrz8omZErBbWjyJKF\nrmY3zSIItNdvqpiVwTvpSQkCgYB23baestuGvledc0EyONStNLkKEn1BcPci9+xE\n0b4jZ+Mr4N6yI1yO9Y6bnKSadx79Nc3dFiLLmYLs1BxDxRan2NXMjnKx99+dJE9j\naqSaQCWoDcdPOIvqbdW5d9A38toRaC9g0F8JtDWAD8sQx/AvvIx+zHWE+IqoTPqW\nZXHmzQKBgF3qT/iOWDLIAyft4MVujvzZ0DcN9/AMxnvT9nnCsDwLJ8VQTCX62r1K\nEnUhgCYNM/zjYOZhxxYDI1gCKPAp56Ii6cuBQ8ROw0ctqAVD+ggcSiCaQe6JCsMZ\nvQP/ZDQrDplGFvtw5xpVm3IOgcOXjizGXjEZ0nFgzkJgYYLTaVaD\n-----END RSA PRIVATE KEY-----\n\n"]
...(1)> ]

# 設定から鍵リストを作成
iex(2)> kb_jwk_list = KittenBlue.JWK.compact_to_list(config)          
[
  %KittenBlue.JWK{
    alg: "HS256",
    key: %JOSE.JWK{
      fields: %{},
      keys: :undefined,
      kty: {:jose_jwk_kty_oct,
       <<169, 53, 78, 71, 120, 204, 182, 237, 34, 40, 60, 53, 203, 76, 12, 38,
         200, 196, 177, 120, 78, 241, 24, 171, 11, 15, 78, 23, 206, 13, 100,
         249>>}
    },
    kid: "samplehs256_201804_001"
  },
  %KittenBlue.JWK{
    alg: "RS256",
    key: %JOSE.JWK{
      fields: %{},
      keys: :undefined,
      kty: {:jose_jwk_kty_rsa,
       {:RSAPrivateKey, :"two-prime",
       （略）
        :asn1_NOVALUE}}
    },
    kid: "samplers256_201804_001"
  }
]

# １つ目の鍵で署名生成
iex(3)> [jwk_hs, jwk_rs] = kb_jwk_list
iex(4)> {:ok, jws} = KittenBlue.JWS.sign(%{"a" => "b"}, jwk_hs)
{:ok,
 "eyJhbGciOiJIUzI1NiIsImtpZCI6InNhbXBsZWhzMjU2XzIwMTgwNF8wMDEiLCJ0eXAiOiJKV1QifQ.eyJhIjoiYiJ9.TZZYlj0yL2jH1No6LdjgoB7IFsTQc-UITwuru-yaHx4"}

# 鍵のリストを用いて署名検証
iex(5)> {:ok, payload} = KittenBlue.JWS.verify(jws, kb_jwk_list)
{:ok, %{"a" => "b"}}
```

こんな感じで鍵の管理と署名検証を何も考えずに実装できるぐらいになっていれば、JWSを安心して導入できると思います。

# まとめ

* 自らのプロダクトでJWSの利用方針を一般化して紹介した
* 利用方針を実装するためにElixirでライブラリを作成した

以上です。猫は良いものです。