Web API リクエストで良く使われる JWS と JWT を理解してみる

投稿日:2018年07月16日

JWS(JSON Web Signature) と JWT(JSON Web Token) を、良く聞くもののちゃんと知らなかったので調べてみました。
JWT は「ジョット」と読みます。

JWS(Json Web Singature) は、RFC 7515 として公開されている規格です。
正確に言い表そうとすると難しいですが、「署名されたJSONオブジェクト」を指している事が通常だと思います。
(RFC的に細かく言うと署名される対象は JSON でなくても良いですが、一般的な文脈では JSON 以外を署名している事は私の観測範囲では見た事がないです)

APIの世界で、APIサーバーがクライアントから受けた APIリクエストが正当なユーザー(許可を与えられたユーザー)からのリクエストかどうかを確認するために用いられています。

JWS による署名の基本的な仕組みは一般的な署名と同じで、ざっくりと言うと「署名アルゴリズム」と「データペイロード」を BASE64 URL にエンコードした後、HASH 値を取り、秘密鍵と混ぜ合わせて HMACを取得してそれを署名とするものです。(RSA公開鍵暗号を使った署名もあります)

ハッシュ値を取って署名対象のデータをコンパクトにまとめてから「秘密鍵」を使って暗号化する事で、後の署名検証負荷を考慮するという一般的な署名の考え方の部分は同じです。
加えてURLの一部として使っても問題なく取り扱えるように BASE64URL (Base64 Encodingからさらに URLで使用できない文字を別の文字に変更したもの)で Encoding をするようにしています。

JWSは「JOSE Header」「JWS Payload」「JWS Signature」の3つから構成されています。

JWSでは、「JOSE Header」の部分で署名の対象のデータ(JWS Payload)が何か、署名の方式は何を使うかを定義しています。この部分の記述方式は JSON形式で表されています。
上の図では、署名の対象になる「JWS Payload」の部分は JWT(JSON 形式)になっていますが、Base64URL 形式であれば、必ずしも JSON である必用は無いとされています。

The JWS Payload used in this example is the octets of the UTF-8 representation of the JSON object below.
(Note that the payload can be any base64url-encoded octet sequence and need not be a base64url-encoded JSON object.)

とは言え、Header がJSON であれば Payload の部分も JSON で行うのが自然な考えで、そのための規格が JWT(Json Web Token)です。JWTはRFC7519で定義されています。

上記の例では、「User ID」と「Expiration Date」がデータペイロードに含まれた絵を描いてます。
JWTでは、Claim (Keyと値=パラメーターの事)として、基本的な Key の値が予約されています。(独自の値を追加する事もできますが、衝突しないように注意しないといけません)

例えば、以下の例の “iss”は、”Issuer Claim(発行者)”、”exp”は、”Expiration Time Claim(有効期限)” として予約されている値です。

{“iss”:”joe”,
“exp”:1300819380,
“http://example.com/is_root”:true}

他にも exp が切れた JWT を処理してはいけない。等の決まりが定義されています。

JWT と JWS という言葉の定義

世の中的には、署名の付いた JSON オブジェクトの事を JWT と呼んでいるのが一般的だと思います。
ですが、その気持ちで JWT の RFC7519 を読んでいくと「?」となります。

JWT の RFC には以下のようにあります。

3.  JSON Web Token (JWT) Overview
    JWTs represent a set of claims as a JSON object that is encoded in a JWS and/or JWE structure.

ここに書いてある通り「JWTは、JWS または JWE(もしくは両方)の構造の中にエンコードして埋め込まれる、JSON 形式の Claim のセットです」とあります。(Claim とは JWS / JWT の中で使われる事前定義された変数を指します。)
つまり JWT は単体として使われるものでは無く、JWS や JWE の中の部分の定義になります。

JWT と言うと”署名の付いた JSON オブジェクト“と言う文脈で使われている事が多いと思いますが、実際には署名は JWT にははいっておらず、JWS の部分に入っていてるので、署名の入った全体を表すには JWS と言うのが正しいのでは無いかと思っています。この話は JWSの構造の解説の後で再度考えて見ます。

JWS の構造

以下は、ほぼ RFC7515 内に書かれている事になりますが、JWSの構造を少し詳しく解説していきます。
JWSは「JOSEヘッダー」「JWSペイロード」「JWS署名」の3つの部分で構成されています。

  • JOSE ヘッダー 

JOSE(JSON Object Signing and Encryption)ヘッダーです。
前述の絵で「署名アルゴリズム」と書いてある所です。以下のように、署名の対象になるデータの形式  ( typ : JWT )と、署名アルゴリズム (alg : HS256) が記載されています。

{“typ”:”JWT”,
“alg”:”HS256″}

この typ や alg 等の Claim (パラメーター)は、適用されているデジタル署名やMACを表すClaimとしてJWSの RFC で予約されているものです。
ここでは、Payload の種類が JWTである事、署名アルゴリズムは HS256(HMAC SHA-256)である事をJSON形式のデータで示しています。
実際には上記のJSON型式のままデータを取り扱うのでは無く、以下のように上記を BASE64URL でエンコードした値にします。

eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9

  • JWS ペイロード

JWSペイロードとは、署名対象となるデータ部分です。実際には JOSEヘッダーも改竄を防ぐため署名の対象に含まれますが、署名したいご本尊はこの部分になります。
ここは JSON である必用はないのですが、一般的に JSON が使われていて、JSON の場合の定義を決めているのが JWT です。

JWT では、以下のようにJSON型式で記述します。

{“iss”:”joe”,
“exp”:1300819380,
“http://example.com/is_root”:true}

iss (発行者) や exp(有効期限) は、JWSでは無く、JWT で定義されている予約された Claim (パラメーター)です。
JWSではデジタル署名等に関わる Claim が定義されていましたが、JWT では実際の Web上でのやり取りで使用しそうな Claim を定義しています。
このJSONデータもこのまま取り扱うのでは無く、BASE64URLでエンコードした値に変換します。

eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ

  • JWS署名

「JWS署名」は「JOSE ヘッダー」と、「JWSペイロード」を「・」でつないだ値を入力にして、 HMAC SHA-256 で署名を作り、BASE64URL に変換したものです。
少し数式ぽく表現すると

ASCII(BASE64URL(UTF8(JWS Protected ヘッダー)) || ‘.’ || BASE64URL(JWS ペイロード))

を作成した後、このHMAC SHA-256のハッシュ値を作り、そえれを BASE64URLに変換したものが「JWS署名」です。

この例では以下のようになります。

dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
  • 上記の3つを足し合わす (JWS Compact Serialization)

上記の3つを「・」でつなげる手法を JWS Compact Serialization と良い、つなげたものがJWSになります。以下がJWSです。
(RFCにはこれ以外の”シリアライゼーション”手法 (3つのデータを結合させる方法) が定義されていますが、JWS Compact Serialization が一般的だと思います)

eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.
dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

JWSのヘッダーとペイロードと署名の3つを足したものの呼び方

はじめの方でも書きましたが、JWS の Header + Payload + Signature を足したものを何と呼ぶべきか、RFCを読んでいると少し悩んでしまいます。
単純にJWSと呼ぶのが、恐らく正しいと思いますが、一方で慣習的に JWT と呼んでいるケースも見られます(というよりそっちの方が多い気がします)

JWT の RFC7519 にも JWSの3つのパーツをつなげる解説があり、以下のような記述があり”(ピリオドで部品をつなぐ事で)この完全なJWT(を生み出す)”という呼び方をしています。

Concatenating these encoded parts in this order with period (‘.’)
characters between the parts yields this complete JWT

JWSのRFC7515では同じものを以下のように”(ピリオドで部品をつなぐ事で)この完全なJWS表現(を生み出す)”としています。

Concatenating these values in the order Header.Payload.Signature with
period (‘.’) characters between the parts yields this complete JWS
representation
using the JWS Compact Serialization

これを読む限り RFC 自体が特に呼び方を気にしてないように見えます。

どちらかに呼び方を統一した方が混乱しないので、この記事では、3つの値を足し合わせた全体の物体を JWS と呼ぶ事にします。
また、文脈的にデータのペイロードに含まれる Claim がキモになっている場合は、JWTと呼ぶ事にします。

こちらのサイトは、JWT と言うタイトルのサイトになっています。
ですが、このサイトで取り扱うのは JWT だけでは無く、JWS 構造そのものです。

JWSのユースケース

JWSを APIの認証のToken に使うケースを下に書いてみました。

この例では、ユーザーは Token 発行サーバーに userid / password を渡して、Token の発行を依頼しています。
Token 発行サーバーに何を渡すように設計するかは、人それぞれだと思うのですが、ここでは userid と password で認証をする事で、そのユーザー本人である事を確認させています。

発行される Token には、ユーザーIDと、Token の有効期限を入れて見ました。
ユーザーID は、後でAPIサーバー側でアクセス権の範囲を見るために使い、有効期限はいつまでも Token が使い回せないようにするため入れました。

JWS の Token を手にしたユーザーは、APIサーバーにアクセスをします。

図の下の APIサーバーでは、Token の検証を行います。Tokenは、HTTPのヘッダーに埋め込むのが一般的だと思います(URLの引数にしても良いですが)。

API サーバーが Token の検証を行い、Token の改竄がなければ、少なくても userid / password を使って正規のユーザーが発行を受けた Token である事が保証できます。
もし、誰かが 正規のToken を盗んだ場合は、有効期限内で使えてしまいますが、盗まれたユーザーのTokenを許可しないようにするか、逆に有効期限が切れる事で、Token を盗んだユーザーは使えなくなる事を対処法としてします。

この例では、Token の中にはユーザー名を含んでいるので、そのユーザーが触れる範囲しかアクセスさせないようにしたり、Read までしか許可しないなど、APIサーバー側で「認可」を設定する事ができます。

 

署名の検証方法

署名の検証方法も一般的な署名と同じですが、この手の事はたまにしか考えないので、頭の体操に以下に図示してみました。
(Base64URLでエンコーディングする部分は省略しています)

 

RSA公開鍵による署名

RSA公開鍵方式による署名は、Webサイトの証明書の改竄確認用に Web ブラウザで使われている考え方なので、一般的に広く知られている方法だと思います。
Token発行側と API サーバーの間で公開鍵を共有できさえば良いので、公開鍵を広く公開する必用はありませんが、公開鍵を広く公開すれば Token を発行した人と、Token を検証する人が関係の無い人でも、公開鍵を使って Token の正当性を確認する事ができます。(実際そういう使い方がされているケースがあるかは、よくわからないです・・)

HMACによる署名

こちらは両方が共通の秘密鍵を持つ方法です。
共通の秘密鍵が必用なので、Token を発行すると側とTokenを検証する側で共通の鍵を持っている必用があるので、システム間でなんらかの連携が取れている事が前提です。
一般的には APIサーバーが受け取る Token は、同じシステム内にある Token 発行サーバーが発行するので、共通秘密鍵の交換に複雑な交換方法を考える必用は無いと思います。

 

 

Tags: , , ,