JOSE関連のRFCを読んだので内容についてまとめます
JWTはさまざまな場面で見かけるようになり、現に弊社でも全面的に採用されていますが、実のところJOSEのRFCにちゃんと目を通したことがありませんでした。 良い機会なので、JOSE関連のRFCを読んで内容についてまとめてみます。
JOSEはいくつかのRFC群からなる規格のことを指します。 この記事では、主に以下のようなものを対象とします。
JWT以外はJOSE WGから提出されており、JWTはoauth WGから提出されています。
JWTはJSONをJWS and/or JWEでエンコードしたものを指します。 エンコードされているJSONのことはJWT Claims Setと呼ばれます。
多くの人が扱うJWTはJWEなしの単なるJWSで署名されたJSONであると思われますが、規格としてはJWSとJWEは両立します。 さらにJWTをさらに別のJWEやJWSを使ってNested JWTを作ることも可能であるとRFCでは書かれています。 私はみたことがないので、以下では基本的にJWSでエンコードすることを前提に話を進めていきます。
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などの拡張性を持たせつつ使い方についてはある程度厳格に指定されています。
JWTの発行者を表す文字列またはURIを入れる。
JWTの主体を表す文字列またはURIを入れる。 あまり使うことがないので一体どんな値を入れるのだろうと思っていますが、特にRFCにも例などがないため何を入れることが想定されているのかはよくわかりません。
JWTの受信者を表す文字列またはURIを入れる。 JWTを処理するシステムはこの値が正しいことを確認する必要があります。
複数ある場合は配列にします。
JWTの有効期限を表す数値を入れる。 UNIX時間を表す数値を入れる必要があります。
expを超えたJWTを受け入れてはならないとありますが、実装者は(Clock skewのために)数分に満たない若干の余裕を持たせても良いとあります。
規格にはNumericDateとあり(TerminologyにこれはUNIX時間と同じと書いてある)、そういう言い方もあるんだなと思いました。
expの逆で、nbfより前の時間にはJWTは有効ではないことを表します。
JWTが発行された時間を表します。
JWTの一意なIDを表します。 ここでは一意性について、うっかり同じ値を振ってしまう確率が無視できる程度のものでなければならないとあります。 UUIDの衝突程度は許容していそうな仕様となっていました。
この辺りは普通の流れなので特に解説することもありません。
次の手順に沿って作ります。
次の手順に沿って検証します。
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つを実装することを推奨しています。
JWSはJSONに署名をつけるための規格です。多くのJWTはJWSを使って作られることが多いですが、上記でも説明したようにJWEを使って暗号化することもできるため、JWTは即JWSというわけではありません。
JWSの構造としては、次の3つのデータからなります。
また、JWSのSerializationとしては、次の2つの方法があります。
JWTではCompact Serializationを使うように定められているのでJSON 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のキーも、JWT Claimsなどと同じくRegisteredなもの、Publicなもの、Privateなものの3つに分かれています。 ここではRegisteredなものについて見ていきます。
Registeredなヘッダーはalg, jku, jwk, kid, x5u, x5c, x5t, x5t#S256, typ, cty, critの10つです。 個別の詳細な話は一旦ここではしません。JWAあたりを読むときに戻って来れたらいいなと思います。
署名は大まかに次のステップで行われます。
ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload))
を用いる。また、JOSE Headerとしてはalgヘッダーが含まれていなければならない。JWS JSON Serializationの場合はさらに別のステップが必要になるようです。ここでは一旦Compact Serializationに従うものとして話を進めます。
検証は大まかに次のステップで行われます。
署名の方もそうですが、見ているとCompact SerializationとJSON Serializationでステップは結構違いがあります。主にProtected Headerを特別扱いしているあたりに差があるようです。
RFCにはセキュリティ的な考慮事項についても説明があります。せっかくなのでいくつか見てみます。
ここまででJWTとJWSのRFCに目を通してきました。 結構端折ったところがありますが、実際のRFCには例などもいろいろ載っていますし、細かい説明もあるので興味がある方は読んでみると良いのではないかと思います。 (正直、あまりRFCを読んだから特別な知識が得られたという感じではありませんが、曖昧であったところが明らかになったのでよかったと思います)
次回はJWAとJWKについても見てみたいと思います。