Beginner
Security
Elixir
JWT
Jws

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

概要

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

JWSとは

JWSの仕様

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

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 形式

発行

  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形式の公開鍵 を渡す

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

鍵交換

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

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

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

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

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

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

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

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

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

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

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

kitten_blue | Hex

s_kitten_blue.jpg
キトンブルーの意味が知りたかったら検索!

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

# 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でライブラリを作成した

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