[CWT入門その3] クレームもコンパクトに表現された CBOR Web Token (RFC8392)

どーも、ritouです。

## アドカレ

これは[認証認可技術 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/identity) 19日めの記事が空いてたので代わりにします。

## 概要

この投稿はCBOR Web Token(CWT)入門3部作の第3弾です。

これまでCBOR, COSEについて紹介しました。

* [CWT入門その1] CBORによるオブジェクトのバイナリ表現 : https://qiita.com/ritou/items/f3eafedab038d17e3066
* [CWT入門その2] CBOR Object Signing and Encryption (COSE) : https://qiita.com/ritou/items/bd2d429dae63bf0ad325

今回紹介する CBOR Web Token(CWT) では、 CBOR によりエンコードされ、 COSE により暗号化/署名付きでやりとりされる標準的なクレームの表現が定義されています。

[CBOR Web Token (CWT)](https://tools.ietf.org/html/rfc8392)

RFC化のタイミングとしては CBOR が2013年、 COSE が2017年、CWT は2018年です。
RFC や IETF Draft の仕様を見たことがある人は、目次やスクロールバーの長さを見るだけで「こいつ...短いな？」と気づくでしょう。

一言でいうと、「[RFC7519 JSON Web Token(JWT)](https://tools.ietf.org/html/rfc7519) で定義されているクレームを CBOR オブジェクトに突っ込むために `Claim Key` という整数をふったよ」ぐらいです。

それでは見ていきましょう。

## Claims

有効な CWT に含まれる一連のクレームは、コンテキストに依存し、この仕様の対象外です。
特別な要件がない限り、実装によって理解されないクレームは無視されなければならない(MUST)

これはお決まりな文言という感じでしょう。

## Registered Claims

ここで登録されているクレームのいずれも、使用や実装が必須にはなっていない。
便利で相互運用可能なクレームの集合としての出発点であり、利用するアプリケーションがどのようなクレームを利用すべきか、必須か任意かを定義すべき。

ということで、柔軟に使っていけという感じです。
登録済みのクレームについて、JWTとほぼ同じで `JWT ID` の部分が `CWT ID` になっています。

| Name(CWT)    | Name(JWT) |
|--------------|------------|
| `iss`        | `iss`      |
| `sub`        | `sub`      |
| `aud`        | `aud`      |
| `exp`        | `exp`      |
| `nbf`        | `nbf`      |
| `iat`        | `iat`      |
| `cti`（diff!） | `jti`      |

それぞれのクレームについての説明は省略されていますが、振り返っておきます。

* iss (Issuer) Claim : CWT の発行者. フォーマットは文字列
* sub (Subject) Claim : CWT の主体、対象者. フォーマットは文字列
* aud (Audience) Claim : CWT の受信者の識別子. フォーマットは文字列もしくは文字列を要素にもつ配列
* exp (Expiration Time) Claim : CWT の有効期限. フォーマットは整数もしくは浮動小数点数
* nbf (Not Before) Claim : この日時まで CWT を受け入れてはいけないという値. フォーマットは整数もしくは浮動小数点数
* iat (Issued At) Claim : CWT の発行日時. フォーマットは整数もしくは浮動小数点数
* cti (CWT ID) Claim  : CWT を識別する値で、値のフォーマットはバイナリ列

自分が JWT を多用してきた経験からすると、単一もしくは複数パーティー間のやりとりではこれらのクレームのどれかを選択 + ちょっと独自のを追加ぐらいで済むパターンが多い気がします。

また、 JWT との違いとして、それぞれに 1 ~ 7 の整数が割り当てられています。

| Name  | Key | Value Type                         |
|-------|-----|------------------------------------|
| `iss` | 1   | text string                        |
| `sub` | 2   | text string                        |
| `aud` | 3   | text string (or text string array) |
| `exp` | 4   | integer or floating-point number   |
| `nbf` | 5   | integer or floating-point number   |
| `iat` | 6   | integer or floating-point number   |
| `cti` | 7   | byte string                        |

クレームとして CBOR Object にする際に、Name の文字列ではなく Key の整数を map の key に用いることでコンパクトな表現が実現可能となります。

OAuthの仕様でJWTのclaimが追加されたりしたこともあるので、今後追加されることもありえます。
独自でキーを考えて実装する際にはその辺りも想定しておきたいところではありますが、後から出てきた仕様で重複したらちょっと切ないですね。

サンプルとして記載されている CWT Claim Set を見てみましょう。

```
# hexed
a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b71

# hexed を cbor.me で確認
A7                                      # map(7)
   01                                   # unsigned(1)
   75                                   # text(21)
      636F61703A2F2F61732E6578616D706C652E636F6D # "coap://as.example.com"
   02                                   # unsigned(2)
   65                                   # text(5)
      6572696B77                        # "erikw"
   03                                   # unsigned(3)
   78 18                                # text(24)
      636F61703A2F2F6C696768742E6578616D706C652E636F6D # "coap://light.example.com"
   04                                   # unsigned(4)
   1A 5612AEB0                          # unsigned(1444064944)
   05                                   # unsigned(5)
   1A 5610D9F0                          # unsigned(1443944944)
   06                                   # unsigned(6)
   1A 5610D9F0                          # unsigned(1443944944)
   07                                   # unsigned(7)
   42                                   # bytes(2)
      0B71                              # "\vq"

# デコード結果
{
  / iss / 1: "coap://as.example.com",
  / sub / 2: "erikw",
  / aud / 3: "coap://light.example.com",
  / exp / 4: 1444064944,
  / nbf / 5: 1443944944,
  / iat / 6: 1443944944,
  / cti / 7: h'0b71'
}
```

iss の key には 1 が振られているため、`01(hexed)` となっていますが、仮に `"iss"` という文字列だった場合、`63697373(hexed)` となるので、整数を振ることでコンパクトに表現できてると言えるでしょう。

## CBOR Tags and Claim Values

CBOR では、日時をタグ付きのデータとして表現できます。

```
# Standard date/time string

## hexed
c074323031332d30332d32315432303a30343a30305a

## with cbor.me
C0                                      # tag(0)
   74                                   # text(20)
      323031332D30332D32315432303A30343A30305A # "2013-03-21T20:04:00Z"

## tagged data
0("2013-03-21T20:04:00Z")

# Epoch-based date/time

## hexed
c11a514b67b0

## with cbor.me
C1             # tag(1)
   1A 514B67B0 # unsigned(1363896240)

## tagged data
1(1363896240)
```

しかし、この仕様で定義されている `exp`, `nbf`, `iat` の値ではそれらのタグ付き表現を採用していません。

> Tagging claim values would only take up extra space without adding information.

JWTでもUNIXタイムスタンプだし、日時が入っていることが自明なのでタグつけないでサイズ抑えようなという感じでしょう。

## CWT CBOR Tag

このデータは CWT であることをアプリケーションが判断できるようにいくつかの方法が定義されています。
COSEの解説にも出てきたと思いますが、バイナリデータを送信するときに `application/cwt` というcontent typeを用いたり、この CBOR データは CWT だよ！というのを CWT CBOR Tag をつけることで表現できます。

このタグを用いる場合は、必ず COSE CBOR Tag がついたオブジェクトに対してつけられます。

例えば、 `COSE_Mac0` オブジェクトには `COSE_Mac0` CBOR Tag がついていて、その前に CWT CBOR Tag がつきます。

```
/ CWT CBOR tag / 61( # 一番最初(外側)につくよ的なイメージ
  / COSE_Mac0 CBOR tag / 17(
    / COSE_Mac0 object /
  )
)
```

## Creating and Validating CWTs

最後に生成/検証方法です。COSEの復習という感じですね。

### Creating a CWT

まずは生成手順です。

1. CWT クレームセットを作成
2. CWT クレームセットを CBOR でエンコードしてバイナリ表現にする
3. COSE ヘッダを作成
4. COSE の種類(signed/MACed/encrypted) による処理を行い、オブジェクトを作成
5. ネストされた signed/MACed/encrypted の処理が必要な場合は、メッセージをタグ付きの `COSE_Sign/COSE_Sign1`, `COSE_Mac/COSE_Mac0`, `COSE_Encrypt/COSE_Encrypt0` としてステップ3に戻る
6. 必要ならば、それぞれの種類に対応した COSE CBOR Tag をつけ、必要ならば CWT CBOR Tag をつける

この仕様に関わるのは 2,6 の部分だけですね。

### Validating a CWT

1. CWT が有効な CBOR オブジェクトであることを検証
2. オブジェクトが CWT CBOR タグから始まるとき、それを削除して続くいずれかの  COSE CBOR タグを検証
3. オブジェクトが COSE CBOR タグを持っていたとき、それを削除して CWT の種類(`COSE_Sign`, `COSE_Sign1`, `COSE_Mac`, `COSE_Mac0`, `COSE_Encrypt`, `COSE_Encrypt0`)を特定する。COSE CBOR タグを持っていないときはアプリケーションコンテキストから特定する。
4. COSE Header のパラメータと値の構文と瀬マンティクスが理解されサポートされているもののみであることを確認する。理解できない場合は無視する。
5. CWT の種類に応じた処理
6. メッセージ自体が COSE CBOR タグで始まる場合は再度 Step 1 からやり直す
7. メッセージが CWT クレームセットに従った CBOR Map であることを検証

さらっと書いてますが、実装しようとすると 1 の漠然とした書き方が気になりますね。
デコードしてエラー出ないか確認するだけの `is_cbor(binary)` みたいな関数なイメージでしょうか。

この仕様に関わるのは最後の 7 のところです。

## まとめ

* JWT みたいなのの CBOR/COSE版を CWT と呼ぶ
* CWT のクレームで一般的に使えそうなやつを定義し、キーに整数をふった
* CBOR Map のあたりを含めた生成/検証方法を整理した

というあたりでした。これでCWT入門は完結です。

CWT完全に理解した！

ではまた。