概要
担当するプロダクトにおいて、いくつかの用途でJSON Web Signature(JWS)を利用してきました。
その経験を通して得られた知見から、JWSの署名生成/検証時に利用する鍵の管理を中心とした利用方針と実装例を紹介します。
JWSとは
JWSの仕様
JSON Web Token 周りは仕様がたくさんあることで有名です。
JWSについてはこの辺りを見る必要があるかと思います。
- RFC7515 JSON Web Signature (JWS) 日本語訳 : 署名まわり
- RFC7517 JSON Web Key (JWK) 日本語訳 : 鍵まわり
-
RFC7519 JSON Web Token (JWT) 日本語訳 : 標準的な
Payload
JWSの使い所
ざっくりと
- 構造化されたデータがやり取りできる
- 署名付きで改ざんされにくいし、検知できる
- デコードしたら中身見れちゃう
- 有効期限とかもつけられる
という特徴を生かして、
- OAuth のリソースアクセスに用いるアクセストークン
- microservices 内のサービス間のやりとりで用いるトークン
- セッションと紐づくCSRF対策トークン
- アカウント登録など、複数の処理を必要とする機能で一連の流れを保証するためのトークン
- データストアと組み合わせてワンタイムトークン
などに利用 できます。
JWS利用方針
ここからは、JWSを使っていくにあたり設計/実装方針として キメ たことを紹介していきます。
基本的な考えとして、下記の点を意識しています。
- 用途毎に鍵を分けよう
- それぞれの鍵あたりの影響範囲を限定する
- そのぶん、管理対象となる鍵の数は増えることは認識してる
- 複数のサービス/システムが
秘密鍵
を持たない- 2つのサービスで双方向のやりとりがある場合は、2組の鍵ペアを用意するぐらいのお気持ち
- そんなに頻繁にはないだろうけども、定期的、もしくは万が一の際の鍵交換も意識する
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
仕様に従う。
処理について
利用する言語やライブラリが異なったりする場合もあるので、基本的な流れを確認できるようにしておく。
鍵の管理
- 1つの鍵に対して、
kid
,alg
,鍵の値
のセットで管理する - 1つの用途に対し、複数の鍵を管理できるようにすることで、有効期限の長いJWSにおける鍵交換なども容易になる
- 鍵の種類によって値の保存方法を変える
-
hsXXX
では、バイナリ形式の鍵をBase64エンコードして保存 -
rsXXX
,esXXX
では PEM 形式
-
発行
-
Payload
生成 : 用途によって変わる -
Header
生成 : 発行時点で利用する鍵に対応するkid
,alg
を含む -
Signature
生成 :kid
に紐づく鍵を用いて生成
検証
- JWTのフォーマットかどうかを検証(2の
Header
取得で代替しても良い) -
Header
検証 : 複数鍵の対応も考慮して 有効な鍵のリストを用意し、kid
に紐づくものがあるかどうかを検証 -
kid
に紐づくalg
の値と、Header
に含まれるalg
が一致しているかを検証 -
Signature
:kid
に紐づく鍵とアルゴリズムを用いて署名検証
kid
に対して alg
と鍵が一意に紐づいている前提であれば3をスキップしても問題ないでしょう。
alg
を書き換えられても署名検証でこけるだけになります。
鍵のやりとり
-
hsXXX
: 鍵のやりとりはしない。 -
rsXXX
,esXXX
:kid
,alg
,PEM形式の公開鍵
を渡す
署名生成側が1箇所、検証側が複数であるときは、生成側が RFC7517 Appendix A. Example JSON Web Key Sets のような形で公開して検証側はリアルタイムもしくは定期的に参照することで(半)自動化することなども考えられる。
鍵交換
- 生成側が鍵ペアを生成、公開鍵を検証側に連絡
- 検証側が公開鍵を設定、検証可能にする
- 生成側が鍵を切り替える
- トークンの有効期限が決まっているものであれば、検証側は以前の公開鍵で発行されたトークンが無効になるタイミングで古い鍵を設定から落とす
1の際に秘密鍵で生成したJWSを一緒に送ることで検証側の動作確認が容易になるかもしれない。
こんなライブラリがあると良いみたいな実装
JWS 関連のライブラリで、一般的なのが
- 単一の鍵を用いて署名生成
- 単一の鍵を用いて署名検証
- 鍵のインポート/エクスポート
などの関数を実装するものです。
筆者は現時点で触れる機会が多いプログラミング言語が Elixir
であり、JWS関連のライブラリとしてはJOSE
(JOSE.JWK
, JOSE.JWS
) の実装内容が幅広く、よく使っています。
上記方針を実装に落とすためには、
- 単一の鍵を用いて署名生成
- 複数の鍵(リスト)を用いて署名検証
- 鍵リストのインポート/エクスポート
といったあたりの関数があると捗ります。
実際のプロダクトで複数鍵対応として手を動かした中で得られた知見から、JOSE
をちょいとだけラップしたライブラリを作りました。
鍵の管理、署名生成/検証のちょっとしたサンプルを載せておきます。
※これらの鍵はライブラリにサンプルで載せたものなので公開しても問題ないです
# 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"
}
]
# 1つ目の鍵で署名生成
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でライブラリを作成した
以上です。猫は良いものです。