JSON Web Signatureを簡単かつ安全に使うためのkid/typパラメータの使い方

f:id:ritou:20200325101819p:plain

こんにちはこんにちは、ritou です。

現状、様々な用途で利用されているJWTですが、今後はますます開発者にとって "簡単に" かつ "安全に" 利用できる状況が求められていくと考えられます。 今回はそのために重要になる、各種パラメータの扱いに注目します。

とりあえずライブラリ使えで終わりでは?

JWTを扱うためには

あたりの処理が必要です。

関連仕様がRFC化されてからある程度時間も経っており、各言語で仕様を忠実に実装されたものから自身が使う機能をピンポイントで抽出して実装したものまで様々なライブラリが存在します。 ここで、 仕様に忠実に、全ての暗号化処理をサポートするライブラリ を使うだけで、誰もが安心、安全に利用できるかと言うと、そうでもないことは想像できるでしょう。

JWTの各種仕様とは別で最近RFC化された "JSON Web Token Best Current Practices" では "暗号化処理の細けぇ話" 以外で 気をつけることとして

  • 署名検証処理をちゃんとやれ
  • 複数のJWTを使う場合は、用途を区別できるようにしろ
  • 用途の区別のためにいろんな値を使って検証しろ

と言う内容が書いてあります。

qiita.com

私もJWT(JWS)の鍵管理と署名生成、検証方法について実践していることを以前Qiitaに投稿したことがあります。

qiita.com

この投稿のように、実際のプロダクトでJWT、厳密にはJWSやJWEなどを使う際、

  • 署名生成時の鍵の管理
  • 用途の表現
  • 上記2つの適切な検証

と言うあたりの設計は自由度が結構あって、開発者は自身の設計を基にしてそれにフィットした誰かが作った優秀なJWTのライブラリを使ったり、間を埋める "薄いラッパーライブラリ" のような機能を作る必要があるのが現状だと思います。

本投稿では自身のプロダクトでJWT(JWS)を利用したい、強制的に利用することになった開発者が簡単、安全に実装するために使えるパラメータを紹介しつつ、ライブラリのどのような機能を使っていくべきかを整理します。

署名生成などに利用する鍵の識別子である "kid" パラメータ

JWSを扱いたい開発者にとって、要件としてはもちろん

  • JWSを生成したい : (例 : "JWSを用いたユーザー認証やAPI利用のために、JWSを生成して送る")
  • JWSを検証したい : (例 : "OIDCのID Tokenのように外部サービスからJWSを受け取る")

のいずれか、もしくは両方でしょう。

JWSにて署名生成のためには、鍵が必要です。

  • alg=HSXXX系で利用する共有鍵
  • alg=RSXXX/ESXXX/PSXXX系で利用する鍵ペア

使い分けに関しては個別のユースケースのための仕様で決められているものはそれに従えば良いでしょう。 自前のユースケースの場合は、 JWSの生成/検証それぞれを別のパーティーが行う場合は秘密鍵/公開鍵のペア、単一で生成して検証する場合は共有鍵を利用する ような整理をしています。

これらの鍵を識別するためのパラメータが "kid" であり、いわゆる JWT Header に含まれます。

The "kid" (key ID) Header Parameter is a hint indicating which key was used to secure the JWS. This parameter allows originators to explicitly signal a change of key to recipients.

まずは "kid" の利用の有無と処理の流れを見ていきます。

"kid" を利用しない場合

"kid" を利用しない理由としては

  • 鍵が一つしかない
  • 別の方法 で鍵を識別できる

といったあたりかと思いますが、この場合、識別子を持たない

  • "alg"
  • 鍵のデータ

によって鍵情報が表現されます。

JWSを生成する時には

  1. JWT Header の生成 : 鍵の "alg" の値を指定
  2. JWT Payload の生成 : 送りたいデータを指定
  3. JWT Signature の生成 : 鍵のデータを用いて署名生成

検証する時は

  1. JWT Header の検証 : "alg" が鍵に紐づく値と一致するかどうかを検証
  2. JWT Signature の検証 : 鍵のデータを用いて署名検証

という流れになります。 世の中に出回っているほとんどのライブラリで

  • Payload, Header の値と単一の鍵を用いてJWSを生成
  • 単一の鍵とJWSを指定して署名を検証

といった機能が提供されているため、それを利用するべきでしょう。 よく言われる "alg" の扱いに関する脆弱性を避けるために、1にて "alg" の値による署名検証ロジックを分岐させたりしてはいけません。 気になる方は実装を確認してみても良いでしょう。

"kid" を利用する場合

"kid" を利用して鍵を識別する場合、鍵の表現は

  • "kid"
  • "alg"
  • 鍵のデータ

のようになります。

単一の鍵によるJWSの生成については

  1. JWT Header の生成 : 鍵の "kid", "alg" の値を指定
  2. JWT Payload の生成 : 送りたいデータを指定
  3. JWT Signature の生成 : 署名生成

検証する時は

  1. JWT Header の検証 : "kid", "alg" が鍵と一致するかどうかを検証
  2. JWT Signature の検証 : 署名検証

となります。 こちらもライブラリの機能で提供されるものを利用すれば良いと思いますが、

  • 鍵の表現として "kid" を扱っているか
  • 検証時にどこまでチェックしているか

というあたりはライブラリによって差が出るところかもしれません。

複数の鍵を扱う場合

単一の鍵を利用する場合はそれほど大きな差が見られませんでしたが、ここでは

  • 署名生成には1つの鍵
  • 複数の鍵リストを用いて署名検証

という場合を考えます。

"kid" を利用せずに行うには、上に書いた

  1. JWT Header の検証 : "alg" が鍵と一致するかどうかを検証
  2. JWT Signature の検証 : 署名検証

という処理を 複数の鍵で成功するまで繰り返す、もしくは、

  1. JWT Header / Payload に含まれる値を用いて鍵を識別
  2. JWT Header の検証 : "alg" が鍵と一致するかどうかを検証
  3. JWT Signature の検証 : 署名検証

という処理が必要となりますが、鍵の識別の部分が独自実装となるので簡単、安全にとはならないかもしれません。

一方、"kid" を利用することで、

  1. JWT Header から "kid" を取得
  2. 鍵リストから "kid" が一致するものを探索
  3. JWT Header の検証 : "alg" が鍵と一致するかどうかを検証
  4. JWT Signature の検証 : 署名検証

という流れになります。 1,2 の処理について、ライブラリで

  • JWT Header を(map形式などにデコードして)取得する関数

があったりするので、そこから "kid" を取得して手元の鍵リストと突きあわせても良いですが、 最初から署名検証の処理に複数の鍵リストを指定して1,2が内部で行われるような関数 があればより簡単、安全に利用できるでしょう。

実際にこのようなやり方が必要となるユースケースを紹介します。 OpenID Connect の ID Token の検証の際、ここまで説明した "kid" を用いた署名検証が必要となります。 Google, Yahoo! JAPAN, Microsoft 最近は Apple といった Identity Provider は公開鍵のリストを "jwks_uri" というURIにて提示しています。

$ curl -i "https://auth.login.yahoo.co.jp/yconnect/v2/jwks"
HTTP/1.1 200 OK
Date: Sat, 28 Mar 2020 17:22:35 GMT
P3P: policyref="http://privacy.yahoo.co.jp/w3c/p3p_jp.xml", CP="CAO DSP COR CUR ADM DEV TAI PSA PSD IVAi IVDi CONi TELo OTPi OUR DELi SAMi OTRi UNRi PUBi IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE GOV"
Strict-Transport-Security: max-age=15552000; includeSubDomains
Expires: Sat, 28 Mar 2020 18:22:35 GMT
Cache-Control: public, max-age=3600
Vary: Accept-Encoding
Content-Length: 985
Content-Type: application/json
Age: 0
Connection: close
Server: ATS

{
  "keys": [
  {
    "kid": "0cc175b9c0f1b6a831c399e269772661",
    "kty": "RSA",
    "alg": "RS256",
    "use": "sig",
    "n": "0bXcnrheJ2snfq1wv6Qz8-TEPDGKHCM0SsrQjxEFpXSEycL2_A-oW1ZGUzCuhz4HH4wkvc4CDJl25johSIUTVyo4mrFrJ0ab0QAhrWE7gMyWFIfraj9cksPAGyVAiXLCN9Ly2xuoJxFjCAZXw1VO8i7RTYK8ZP6dhcosiyzdhYt7C_65B5ikmCS4AymXIa83QQanCtjoGiwy4Cf2pLnn9zXMZEnqQ-wwSoGn32YExmap7GAtjOwHNWU5zpW3dwNMq-zkcln3ICEBwxDpWJhEZHZPBpPWgN-dQZDR2FiGHJgUFE3EM-CIcwxekrRBP-R3xEUeMFf5z1HeQNK8sjZeRw",
    "e": "AQAB"
  },
  {
    "kid": "b0c88084cd7ced792748340968b7d689",
    "kty": "RSA",
    "alg": "RS256",
    "use": "sig",
    "n": "xf9qYN87qbnuzKZFLM756UZXhBZuaB7g8l-jBeQsf2Suf6QUC1A_v30Y4yC0Jht_D5M3RzGzRxvPfBRnKm3NxUDV5Ihmunt3-ZW6ia3bNdd7RRgCj3HdtQRiVroa9nDj_8abXZA1n2v2RpfiJKSoHR8fim2TmfM7EMqXaoe65l1P3drEUkRMAOCMnsCXxCEfpcw_z0tXVTuOI_w3aCI8D3mfPe2fTmCUOiYLV4jhnF5-pMZEBcF4_RsYTdKg_50F4hhgQ0qpkFJ2UI_UMV6tHKw0lSJefcwj5j_pfeW4kfutUjb0xPQ2VrJ5IPM-efF5wtlkIhhQE58U5XuhWnc6Iw",
    "e": "AQAB"
  }
  ]
}

これらを鍵のリストに変換し、署名検証機能に渡してあげられると独自実装をせずに済みます。 Rubyjson-jwtって言うライブラリでは同様の処理がサポートされています。

jwk_set = JSON::JWK::Set.new(
  JSON.parse(
    RestClient.get(idp_jwks_url)
  )
)

id_token = JSON::JWT.decode id_token_string, jwk_set

優秀ですね。

"kid" について、一旦まとめます。

  • "kid" は鍵の識別子
  • 単一の鍵でのJWS生成/検証は "kid" 使わなくてもそれほど違いはない
  • 複数の鍵を考慮する際には "kid" を利用することで簡単、安全を実現できそう

「とりあえず "kid" はなしで良いかな...」とは言わずに、最初から利用すべきだと思います。

次は、JWTの用途に注目します。

"typ" パラメータでJWTの用途を表現

ざっくりといってしまうと、JWS の Payload に含まれる値というものは、Base64 URL エンコードできれば何でも良かったりします。 とはいえ、異なるサービスなどでやりとりされるデータの場合、RFC 7519 で定義されている "発行者"、"受信者"、"有効期限"などの標準的なクレームを利用することで送信側、受信側双方で取り扱いがしやすくなるでしょう。

ふむふむ、これは便利だとあるサービス内でJWSを色々な場所で使っていくと、用途が異なるのにPayload に含まれるクレームの key が一緒のものが使われるようになる 場合も出てきます。 さらにJWSの署名生成/検証に使う鍵がサービス内で共通だったりすると、用途Aで処理した、もしくはすべきJWSを用途Bで処理することで意図しない挙動を発生させるなんていう脆弱性が生まれる可能性が出てきます。 それを防ぐためには、"用途" をどこかに含み、検証する必要があります。

このために利用できるのが、JWT Header の "typ" パラメータです。 (仕様では "cty" というパラメータもあり、コンテンツの種類を示すっぽいネーミングだしこっちじゃね?と思いましたが、JWT BCP では "typ" パラメータの利用について言及されていたため、"typ" の使い方について紹介します。)

JWSを扱うだいたいのアプリケーションは、各種検証をした後に中身のデータを処理しますが、"typ" パラメータを使うことで、用途の検証を署名検証などと同じタイミングで行う方が簡単かつ安全でしょう。 単一の鍵を用いたJWS生成時の流れは

  1. JWT Header の生成 : 鍵の "kid", "alg" と用途を示す "typ" の値を指定
  2. JWT Payload の生成 : 送りたいデータを指定
  3. JWT Signature の生成 : 署名生成

となり、検証する時は

  1. JWT Header の検証 : "kid", "alg" が鍵と一致するか、"typ" が意図したものであるかを検証
  2. JWT Signature の検証 : 署名検証

となります。

"kid" が鍵に紐づくものであるのに対して、"typ" は独立しているため、複数の鍵の場合は署名検証よりも先にするか後にするかを考える必要があります。

  1. JWT Header から "kid", "typ" を取得
  2. "typ" が意図した値と一致するかを検証
  3. 鍵リストから "kid" が一致するものを探索
  4. JWT Header の検証 : "alg" が鍵と一致するかどうかを検証
  5. JWT Signature の検証 : 署名検証

もしくは

  1. JWT Header から "kid", "typ" を取得
  2. 鍵リストから "kid" が一致するものを探索
  3. JWT Header の検証 : "alg" が鍵と一致するかどうかを検証
  4. JWT Signature の検証 : 署名検証
  5. "typ" が意図した値と一致するかを検証

となります。 署名検証の負荷まで考えると前者でやりたい気もしますが、ラッパーライブラリを作る場合などは後者の方がシンプルな場合もあるでしょう。

これをライブラリで実現したい場合、"kid" と同様に

  • JWT Header を(map形式などにデコードして)取得する関数

を利用して "typ" を取得してその値を検証する処理を行うか、最初から"JWS検証"と言った機能を用意して、そこに JWT Header の想定する key/value を指定して実際の値との比較が内部で行われる ようになっていれば、より簡単、安全に利用できるでしょう。

既に "typ" パラメータを利用する JWS を利用している RFC もあります。 サービス間でセキュリティイベント情報をやりとりするための仕様である RFC 8417 : Security Event Token (SET) では "typ" パラメータに "secevent+jwt" を利用すべきとあります。

Payloadのクレームにて用途を表現

"typ" の利用を意識する前は、「別にこんなの JWT Payload に適当に "usage" とか用意したら一緒じゃん」と思ったことが私にもありました。 Payloadに含む場合、

  1. JWT Header の検証 : "kid", "alg" が鍵と一致するかを検証
  2. JWT Payload の検証 : "usage" が意図したものであるかを検証
  3. JWT Signature の検証 : 署名検証

とするか、もしくは

  1. JWT Header の検証 : "kid", "alg" が鍵と一致するかを検証
  2. JWT Signature の検証 : 署名検証
  3. JWT Payload の検証 : "usage" が意図したものであるかを検証

のような処理が考えられますが、"kid" のところでも一旦 JWT Header をデコードしているので同じく JWT Header の中にある値を利用するほうが効率的な気がします。

"kid" にて用途を表現

ここまで書いておきながら、実際に開発したプロダクトでは、"typ" を使わずに鍵リストをを用途ごとに用意することで対応しました。 この場合は "kid" の値に "用途" に関する文字列を含んだりすると人間にも優しい感じになります。

"typ" パラメータを使うべきかどうかは鍵リストと用途の数の関係にもよるでしょう。

  • 鍵リスト : 用途 が 1:1 ならば "typ" は不要かもしれません
  • 鍵リスト : 用途 が 1:n ならば "typ" を使って用途を別で検証すべきでしょう

例えば

  • 自サービスのいくつかの機能でJWSを生成、検証する。JWSをハンドリングするための汎用的なモジュールがあって利用するときは "typ" の重複に気をつけながら使う
  • 様々な種類のトークン発行を目的としたサービスがあり、社内外の別のサービスが検証する

と言った使い方まで踏み込んでいく場合は、用途の表現に "typ" パラメータを使っていくのが良いかと思います。

まとめ

JWS をサービスで利用する際は、"kid", "typ" を使って鍵管理や用途の検証を行うことをお勧めします。 ここで書いたような細かい実装を意識しなくても良いようなライブラリがあれば「簡単、安全」が実現できると思います。 ちょうど良いライブラリがなかったらラッパーライブラリでも作れば良いと思います。

おまけ : Elixir で上記の処理を実現するためのラッパーライブラリ

Elixir, Erlang では JOSE という JWT のライブラリがあり、それを使って上記のような処理を実現するラッパーライブラリ KittenBlue を実装しています。

鍵リストを用いた署名検証

JWSの署名検証の関数では、引数に鍵のリストを受け付けます。

@spec verify(token :: String.t(), keys :: List.t(), required_header :: map)

{:ok, payload} = KittenBlue.JWS.verify(token, kb_jwk_list)

鍵のリストを生成する関数も用意しています。

@spec public_jwk_sets_to_list(public_json_web_key_sets :: map) :: List.t()

kb_jwk_list = KittenBlue.JWK.public_jwk_sets_to_list(public_jwk_sets)

GoogleのIDTokenの例を紹介しましたが、public_jwk_sets というのに jwks_uriJSONレスポンスを指定することで鍵のリストを生成できます。 って言うのを個人的によく使うので、Googleの場合は1つの関数呼び出しでHTTP GETで取得したものを鍵のリストに変えるものを用意しました。

iex(x)> kb_jwk_list = KittenBlue.JWK.Google.fetch!()
[
  %KittenBlue.JWK{
    alg: "RS256",
    key: %JOSE.JWK{
      fields: %{
        "alg" => "RS256",
        "kid" => "cb404383844b46312769bb929ecec57d0ad8e3bb",
        "use" => "sig"
      },
      keys: :undefined,
      kty: {:jose_jwk_kty_rsa, 
       {:RSAPublicKey,
        23559603576875225300516496747863315901159811550023890989775190602953070457251744574277164711804232777930691164503766335809595820519587969666799742618661883170645295829810454994446891146636842862247943583070585283519455642403178376555771276441887509803782686400621436423897652203980773024251492902834360213913266119209668280813169144553844586444664567379373320411287000801783329657597460647965207784138972860305800997638056311176902160490179340442844385059621186040784773075957366134393281123221196446107212037616578676463217957073092746307384684405844850216745668358506167614836872166650356637194235779935491814491191,
        65537}}
    },
    kid: "cb404383844b46312769bb929ecec57d0ad8e3bb"
  },
  %KittenBlue.JWK{
    alg: "RS256",
    key: %JOSE.JWK{
      fields: %{
        "alg" => "RS256",
        "kid" => "a541d6ef022d77a2318f7dd657f27793203bed4a",
        "use" => "sig"
      },
      keys: :undefined,
      kty: {:jose_jwk_kty_rsa,
       {:RSAPublicKey,
        18762754202955820134622590402370752480881774188179408616754635358623594945478938027256785796402587678256230913279330972176390848402816954890173143705716195619041968607912861390415901526508453885537509057572453582237323091784128038040790496847188179617000650969981899214218795261781625441530308527714819530349949418550772542624424910914978261040800935020496046984026021447949276764747792890846218651552541231126454438533384071335837361262551487823641818719455020363417580745785975945076349317744486057798572886663924803577041289507190965386424906046073948681270612506806128266494262830084437561463759567010172055162873,
        65537}}
    },
    kid: "a541d6ef022d77a2318f7dd657f27793203bed4a"
  }
]

JWT Header パラメータを指定した JWS の生成

KittenBlue ではJWSの生成時に鍵情報(KittenBlue.JWK) から alg, kid クレームの値を利用します。 "typ" パラメータのように、JWT Header に値を入れたいときのために、JWS生成時に引数に指定できるようにしています。

@spec KittenBlue.JWS.sign(payload :: map, key :: KittenBlue.JWK.t(), header :: map) ::
          {:ok, String.t()} | {:error, :invalid_key}

iex(x)> rs256_jwk_1 = [
...(x)>                  kid: "rs256_first",
...(x)>                  alg: "RS256",
...(x)>                  key:
...(x)>                    ~S"""
...(x)>                    -----BEGIN RSA PRIVATE KEY-----
...(x)>                    MIIEowIBAAKCAQEAvTpKoAgqi3TtyT20ncxKkcNOOJEmOgy96Spry+AC0F+2UDFG
...(x)>                    JJ7shvhhEwxZy5+24H+Td5DGV1DKN0Gn2wb8dfWMH1x0HzsDEtJldFTf5GCK96QC
...(x)>                    U79XtwedX7p8Yvt5cDGnVCVlODhM9S7/5Ztvnm3PsE/8ZFnsLUI4zdx4qg5295x0
...(x)>                    oYU1zmBDAOl3y9i9vGdhmtqZ1uwVXJXTziWooV9z7Qyi3Y4+6QOgj/6p6GSFDZv9
...(x)>                    CYHMYZPWk6+dFmnSrOaHfA5C5W++vdlAinhn8zWxO3ROdaKklmV9doF45cq843SK
...(x)>                    +E+N/aYYEmTkpCrOApyI76nNrFzdrsRb+2KVUwIDAQABAoIBAB2opUmv/fsduKdy
...(x)>                    JH0XKBjwo7H6DiPLG3kQTRUHZ2mBlvG6x2O2BRyikZSKuwhPYDqPxG1ZI71LzGYc
...(x)>                    xFJwJeHXOr8vnoPGnBS3JW+2XeFNwHpQGo1F0Fm/t8rpT9Wz1LThE3j844CMUoOb
...(x)>                    ekBivHv4ejUIVGbmMT5mwsCBbeg5VwFWN1Q74KHJgpTW/uY9ItbZp1chXpzJffxz
...(x)>                    QuU6eefkHbaHDuFYlJ9OSs6raZihyZSso/Td8M2g5O12ZbtK7Qc5AYoURfedVbRp
...(x)>                    K4f+LUyHH8jmtXqU1xN/4yCOUlsiS8eQ74zwPEcTXG1aRwa/QIGSJ4bvvkbka3F7
...(x)>                    smgpqwECgYEA7DjXusxmai1Eu3RGTfKWfLA/Br3j9FMGruxqa9R8xn6PQWDLuczl
...(x)>                    4ttIN9ST/lWR/XTMtdEFv0zEtze3ytVvKgbaRtqwUZCChe8wluMQRbN+/yBIW46X
...(x)>                    n6pdSzIfwS8Q3YgdOVZd+N1zgE7u3bUseS0uNIAHHwFNSEJA4bxhgDcCgYEAzRIt
...(x)>                    YdihERIZ01qN77MTxbuyXm6wuLOLaXrnomFmtjbM3iVBkrmLGhANTOUhfgPI54ka
...(x)>                    bXaklSqyv0zukgMn6MthXg+tSydi683jrgLg0wdhDje4Wb+1Pu6mViTYEzEHDvYj
...(x)>                    s8duj8J/3SEASRnnBdwku3yc+EW1zkxvcWCY7cUCgYEArmyqnvwfA3e5sNECuLvP
...(x)>                    8vIRF+FPWTGVVcSsMEMOf2MkVJos1F0/wms4wEDvpnV4/zYnknltTPxapQ83X0aK
...(x)>                    dvXoZzlDyHZ0aoFb146CjXUk6S3lP/Xib7tUeBni6LrgMTQ4oAXuDb03dB7UslD9
...(x)>                    Ldz2qT2ABJzpe9mwHv8C37ECgYBfvNC7EWuAkLbF2UzSTwQ4F/yZ4YtXb1ryj5J8
...(x)>                    WISfJM5YF4SZf03ViRDsiTwtnI66qWNRH0aO7TQt4zitqhODtw9p3l/E6kpgU+qr
...(x)>                    XmSfoJ5LCPBj1gBDtR6qsOC/dPAaqAba84xGSUNwdOuxNQqJzdDIRtDxh3ntKfoN
...(x)>                    ME+1EQKBgFQQA3KJiwu0Vy0xmvCa9L5x+Ye8XZc8rH8k/aUZqaw02kzGj+tJha5u
...(x)>                    y5S2rrlPsZse3QHXRO2bklSM4w8TX3OfZ+/UwnikTZXCVI0LzZfKvbQeJHe9xfcm
...(x)>                    HHZOuo4XBKSFKckk5uKh6uOIVkdu47wDuJ6AQLjdNY73+82T/ZBl
...(x)>                    -----END RSA PRIVATE KEY-----
...(x)>                    """
...(x)>                    |> JOSE.JWK.from_pem()
...(x)>                ] |> KittenBlue.JWK.new()
%KittenBlue.JWK{
  alg: "RS256",
  key: %JOSE.JWK{
    fields: %{},
    keys: :undefined,
    kty: {:jose_jwk_kty_rsa,
     {:RSAPrivateKey, :"two-prime",
      23887784250727630316275809631128181418019737776255801605864618160204240776719803666317908074747681159398695862838220403653302425517683356761243070504760826171598857148793348816474961674958362252696860381597038089154426567710056288030876197461232648892712282755310983552228486351250470069084197476500525614553744825922598437607204933566247107733336312172470812156739795950920867166810567432860798141104064634233139621588930688857402065189366212046691810735625674464012122874442883055968679298939504932914622476516593055298437166198578018245264529279821684455040347159389391921905462963843464444466480442655050794440019,
      65537,
      3744073116307951517597465806047708615376027990870799610837257697813722954338248977835689026714805085209017866290403893774916802949748133735927625924667030935725826031591395380362708185073657583650489797210198441365858518142693412738653894751389013372994500335116871737316187982361793010812714596008566139931533083564795373872347186983542263400276129961867924891969921891909900625784243989908469276253045564221677183175334438753932794161715983643564958167118861179237681564968441043741184083339090247362671044715659085409294554087654935608902188295421556518561902693797902685451096957145838130713682410664715247790849,
      165880758906150399291135048661183463406407433113424473810503004412871873812435170415220873632378991804875248755073671792436986556673474817159545219329130498920059671266176701275614478922948106193874806041323257371549793099599031745489910172499363570300750341362384960831710405729252739377305236834547220447287,
      144005756956070553706478293636419162463322836033965404401959318188373280534931486877883443698725445543912975466071472612424820554774082629812964172987664963563642572010748344211563528175244760773768284033471756603976820992024010350960102526613349938770806237474714486734914816635746598724046297357356845559237,
      122485034178958910577179414297450145126613493190485330983076146139550112417891614915747875502663902397447595064704291093269613400835295990844632989819114135583556275263024290524887252453412648655476900284598243293468385609286301543063425745267394020080167089113150327670905442025878489463151426230944490774449,
      67229200906784482982184260373527636216607801566980568428251938588758546946713517135598544996051142589095646693622271023234963603672243650788981061640456493663017961353752175709858518211846570649163288192747636679360892550345096978774971342970567080071281682740686397548582537036899811321252430137871584703761,
      59030731919528272000968194889255430549869261117598269061736354937748840662335109895164381473069940718361084490370316136012764124770337022214514681586404858422354002075066391172100445960650914065118822097887628485137753546773317882566753980768787835222561197839761197921305443848496610762768920267943487311973,
      :asn1_NOVALUE}}
  },
  kid: "rs256_first"
}

iex(x)> payload = %{"foo" => "var"}
%{"foo" => "var"}

iex(x)> {:ok, jws} = KittenBlue.JWS.sign(payload, rs256_jwk_1, %{"typ" => "secevent+jwt"})
{:ok,
 "eyJhbGciOiJSUzI1NiIsImtpZCI6InJzMjU2X2ZpcnN0IiwidHlwIjoic2VjZXZlbnQrand0In0.eyJmb28iOiJ2YXIifQ.dWayLDJ60IGsWXYmSBbChul1aq80AiDUVbfWaC_UVObPpd2K2PtycFOMeJ5WEPNOnihCu0KgGlb2bL5Kj7bzhg1oklC-r3uNJ6C8OtkFwrzOLQ1iWGcDjTku4WHLK6Z09UHI3elwU7gFnNAW_S0HB2KMbGj-H8bu1CeJv2h71hH4KD_lky1Bp1l19VPnT1OOe7TIOClZv7kS9OfjzLDBDWcHnqQx_XkEbmf1_yh46407bFBFo6hPOJY42bgkxrl8CRRO2Nk-ihT_SrjNWRGGiTzxD0Cj9U2CrtOXxcSHFfos68XBdTq9TVT9nznhj1T4ZrWihqohMv2e5llsk7CDXQ"}

 # decoded JWT header
 {
  "alg": "RS256",
  "kid": "rs256_first",
  "typ": "secevent+jwt"
 }

JWT Header パラメータを指定した JWS の検証

上記のように指定できるようにした typ クレームの値を署名検証時に検証できます。

@spec verify(token :: String.t(), keys :: List.t(), required_header :: map) ::
          {:error, :invalid_jwt_format}
          | {:error, :invalid_jwt_kid}
          | {:error, :invalid_jwt_signature}
          | {:error, :invalid_jwt_header}
          | {:ok, payload :: map}

iex(x)> KittenBlue.JWS.verify(jws, [rs256_jwk_1], %{"typ" => "secevent+jwt"})    
{:ok, %{"foo" => "var"}}

ではまた。 ご安全に!