🚡
JOSEのRFCを読む(1) - JWTとJWS

myuon

myuon

2023年7月10日
RFC

JOSE関連のRFCを読んだので内容についてまとめます

# はじめに

JWTはさまざまな場面で見かけるようになり、現に弊社でも全面的に採用されていますが、実のところJOSEのRFCにちゃんと目を通したことがありませんでした。 良い機会なので、JOSE関連のRFCを読んで内容についてまとめてみます。

# JOSEについて

JOSEはいくつかのRFC群からなる規格のことを指します。 この記事では、主に以下のようなものを対象とします。

JWT以外はJOSE WGから提出されており、JWTはoauth WGから提出されています。

# RFC7519 JSON Web Token (JWT)

JWTはJSONをJWS and/or JWEでエンコードしたものを指します。 エンコードされているJSONのことはJWT Claims Setと呼ばれます。

多くの人が扱うJWTはJWEなしの単なるJWSで署名されたJSONであると思われますが、規格としてはJWSとJWEは両立します。 さらにJWTをさらに別のJWEやJWSを使ってNested JWTを作ることも可能であるとRFCでは書かれています。 私はみたことがないので、以下では基本的にJWSでエンコードすることを前提に話を進めていきます。

# JWT Claims

JWSは構造としてJOSE Header, JWS Payload, JWS Signatureの3つからなります。ここではJWS PayloadにJWT Claims Setが入ることになります。

JWT Claims Setに登場するClaimには、Registered Claim Names, Public Claim Names, Private Claim Namesの3種類があります。 JWTの送信者と受信者の間で事前に合意しておいたものがPrivate Claim Namesと呼ばれます。 また、IANAに登録されているClaim NamesがPublic Claim Namesと呼ばれます。 Public Claim Namesについては IANAのJWTのページ を見ると良いでしょう。

規格として名前が指定されているものはRegistered Claim Namesと呼ばれ、iss, sub, aud, exp, nbf, iat, jtiの7つがあります。 RFC上でもexpを超えたものを受け入れてはならないことや、audが入っていれば検証しなければならないなど、Private Claim Namesなどの拡張性を持たせつつ使い方についてはある程度厳格に指定されています。

iss (Issuer)

JWTの発行者を表す文字列またはURIを入れる。

sub (Subject)

JWTの主体を表す文字列またはURIを入れる。 あまり使うことがないので一体どんな値を入れるのだろうと思っていますが、特にRFCにも例などがないため何を入れることが想定されているのかはよくわかりません。

aud (Audience)

JWTの受信者を表す文字列またはURIを入れる。 JWTを処理するシステムはこの値が正しいことを確認する必要があります。

複数ある場合は配列にします。

exp (Expiration Time)

JWTの有効期限を表す数値を入れる。 UNIX時間を表す数値を入れる必要があります。

expを超えたJWTを受け入れてはならないとありますが、実装者は(Clock skewのために)数分に満たない若干の余裕を持たせても良いとあります。

規格にはNumericDateとあり(TerminologyにこれはUNIX時間と同じと書いてある)、そういう言い方もあるんだなと思いました。

nbf (Not Before)

expの逆で、nbfより前の時間にはJWTは有効ではないことを表します。

iat (Issued At)

JWTが発行された時間を表します。

jti (JWT ID)

JWTの一意なIDを表します。 ここでは一意性について、うっかり同じ値を振ってしまう確率が無視できる程度のものでなければならないとあります。 UUIDの衝突程度は許容していそうな仕様となっていました。

# JWTの作り方

この辺りは普通の流れなので特に解説することもありません。

次の手順に沿って作ります。

  • JWT Claims Setを作る。(whitespaceは認められているので特に正規化などは不要) これをUTF-8でエンコードしたものをメッセージと呼ぶことにする。
  • JOSE Headerを作る。(JWSやJWEの規格に従ったヘッダーを作る必要がある。)
  • JWSを使う場合: メッセージをJWSのPayloadとし、JWSを使って署名する。ここの工程はJWSの規格の工程に従うこと
  • JWEを使う場合: メッセージをJWEのPayloadとし、JWEを使って暗号化する。ここの工程はJWEの規格の工程に従うこと
  • Nested JWTを使う場合はJOSE Headerのステップから繰り返す。

# JWTの検証の仕方

次の手順に沿って検証します。

  • JWTが最低1つのピリオド(.)を含むことを確認する。
  • 最初のピリオドより前の部分をエンコードされたJOSE Headerとする。JOSE HeaderをBase64urlを使ってデコードし、それが正しいJSONおよびJOSE Headerの形式であることを確認する。
  • JWEの規格に説明されている方法でJWTがJWSなのかJWEなのかを判定する。それぞれの規格に従ってメッセージを取り出す。
  • JOSE HeaderをみてNested JWTかどうかを判断し、そうであれば最初のステップから繰り返す。そうでない場合は、メッセージをBase64urlを使ってデコードする。

# 実装者への要求

JWTには実装者が満たさなければならない仕様について明記されています。

署名とMAC(メッセージ認証符号)については、JWAで定められているアルゴリズムのうちHMAC SHA-256(HS256)とnone(暗号化なし)の2つのみを必須としています。 またRS256(RSASSA-PKCS1-v1_5 using SHA-256)とES256(ECDSA using P-256 and SHA-256)の2つを実装することを推奨しています。

# RFC7515 JSON Web Signature (JWS)

JWSはJSONに署名をつけるための規格です。多くのJWTはJWSを使って作られることが多いですが、上記でも説明したようにJWEを使って暗号化することもできるため、JWTは即JWSというわけではありません。

JWSの構造としては、次の3つのデータからなります。

  • JOSE Header
  • JWS Payload
  • JWS Signature

また、JWSのSerializationとしては、次の2つの方法があります。

  • Compact Serialization
  • JSON Serialization

JWTではCompact Serializationを使うように定められているのでJSON Serializationを見かけることはあまりありませんが、一応軽く触れておこうと思います。

# Serialization

JWS Compact Serializationは次のような構造です。

BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload) || '.' ||
BASE64URL(JWS Signature)

また、JWS JSON Serializationは次のような構造です。 ここでJOSE HeaderはJWS Protected HeaderとJWS Unprotected Headerの2つに分かれてserializeされています。

{
  "protected": BASE64URL(JWS Protected Header),
  "header": JWS Unprotected Header,
  "payload": BASE64URL(JWS Payload),
  "signature": BASE64URL(JWS Signature)
}

# JOSE Header

JOSE Headerのキーも、JWT Claimsなどと同じくRegisteredなもの、Publicなもの、Privateなものの3つに分かれています。 ここではRegisteredなものについて見ていきます。

Registeredなヘッダーはalg, jku, jwk, kid, x5u, x5c, x5t, x5t#S256, typ, cty, critの10つです。 個別の詳細な話は一旦ここではしません。JWAあたりを読むときに戻って来れたらいいなと思います。

# 署名の仕方

署名は大まかに次のステップで行われます。

  1. JWS Payloadを作成し、Base64urlでエンコードする。
  2. JOSE Headerを含むJSONオブジェクトを作成する
  3. 署名を作成する。入力としては ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload)) を用いる。また、JOSE Headerとしてはalgヘッダーが含まれていなければならない。
  4. 署名した結果をBase64urlでエンコードしたものが、JWS Compact Serializationに従うJWS Signatureとなる。

JWS JSON Serializationの場合はさらに別のステップが必要になるようです。ここでは一旦Compact Serializationに従うものとして話を進めます。

# 検証の仕方

検証は大まかに次のステップで行われます。

  1. JWSをパースして、JOSE HeaderとJWS PayloadとJWS Signatureを取り出す。
  2. JWS Protected HeaderをBase64urlでデコードし、JSONとしてパースする。Compact Serializationの場合はこれがJOSE Headerそのものとなる。
  3. JWS PayloadをBase64urlでデコードし、JSONとしてパースする。
  4. JWS SignatureをBase64urlでデコードする。
  5. JOSE Headerに含まれるalgヘッダーの値を使って、署名を検証する。署名の検証については、上記の署名の入力の形式を参照する。

署名の方もそうですが、見ているとCompact SerializationとJSON Serializationでステップは結構違いがあります。主にProtected Headerを特別扱いしているあたりに差があるようです。

# セキュリティ考慮事項

RFCにはセキュリティ的な考慮事項についても説明があります。せっかくなのでいくつか見てみます。

  • 鍵は最低でも128bitのエントロピーを持つものを使用するべきであり、さらに不十分な疑似乱数はセキュリティ的に危険であるという記載があります。
  • デジタル署名のアルゴリズムの中については、署名自体の中にある情報を使って検証を行うものがあります。このようなケースで、ヘッダーのalgに記載のものと実際の署名に使われているアルゴリズムが同じかどうかを実装者は検証しなければなりません。
  • JWSの発行者は、サードパーティから任意のコンテンツをメッセージに入れられるようにすると選択平文攻撃が可能になるため、サードパーティがコントロールできないエントロピーを追加せずにこのようなことをするべきではありません。
  • タイミング攻撃にも気をつける必要がああります
  • Unicode文字列の比較についても、注意が必要なようです。筆者は知りませんでしたが、 UNICODE SECURITY CONSIDERATIONS などの文献もあるように、Unicode文字列の扱いは注意をしなければセキュリティ的な問題になりうるようです。

# 終わりに

ここまででJWTとJWSのRFCに目を通してきました。 結構端折ったところがありますが、実際のRFCには例などもいろいろ載っていますし、細かい説明もあるので興味がある方は読んでみると良いのではないかと思います。 (正直、あまりRFCを読んだから特別な知識が得られたという感じではありませんが、曖昧であったところが明らかになったのでよかったと思います)

次回はJWAとJWKについても見てみたいと思います。