HDE Advent Calendar 2015

HDE Advent Calendar Day 2: GoでOAuth2/OpenIDとJOSE (JWA/JWT/JWK/JWS/JWE)

(English version available here)

HDE Advent Calendar二日目です。子育ての話の続きでも書こうかと思ったのですがうちの子供がどれだけ可愛いかを話してもう会社と関係ないところに行ってしまいそうでしたので止めました。

blog.hde.co.jp

では何を書こうか… 今年はあちこちのカンファレンス顔を出したりYAPC::Asia Tokyo 2015の主催をしたりもしたのですが、それももうあちこちに書いてしまっていました

というわけで裏方の技術なので地味ですが、ここしばらくやっていた自分の本来の商売である技術ネタを紹介します。

 

OAuth2/OpenIDとJOSE (JWA/JWT/JWK/JWS/JWE)

f:id:lestrrat:20151202095417j:plain

Dilbert Comic Strip on 2004-01-11 | Dilbert by Scott Adams

 

ここしばらくGoでOpenID関連の実装をシコシコと書いていました。OpenIDの仕様を読んでいくとClaims回りはかなり柔軟な実装になっていることがわかりました。OpenID Certification用のサイトのほうで試してみていると、なるほど… データの取り回しをかなり細かく設定できるようです。

このデータのやりとりは基本JWTという形式でなされています。JWTはただのJSON形式のフォーマットで、いくつかの規定されたキーとデータがある他、それ以外のカスタムデータも挿入できるものです。このデータ形式自体は大変シンプルなものです。それに署名をつけたり、署名を検証したり、必要であれば暗号化したり… と色々ある事がわかりました。

…と、これ以上説明する前にまず関連技術のおさらい書き出しておきます。このあたりの仕様と名前が色々ある上にこれらの技術の関係性について語ってくれるものがなかなか見つからなかったのでこのプロジェクトに着手した当初の僕は大変混乱しました。きっとこれを読んでいる方達も同様だと思うのでまずざっとおさらいです

JOSE回りのおさらい

JOSE (IETFのWG)はJavascript Object Signing and Encryptionの略です。ちらっとこの名前をドキュメントで見るとまるでこれもまた何かの仕様のような印象を受けますが、この後の仕様群をまとめてくくるための総称、と考えると存在を無視できるので技術者として幸せになれます。

JWA (RFC 7518)はJSON Web Algorithmsの略です。この後に続く仕様群の中で使う署名や暗号化アルゴリズム等の名称および表記方法が定義されています。要はJWS/JWE等で使う「語彙」を集めたものです。例えば署名する際のアルゴリズムはHMA+SHAを使うHS256やRSASSA-PKCS+SHAを使うRS256などがこの中で定義されています。

JWT(RFC 7519)はJSON Web Tokenの略です。読み方は"jot"らしいです(この読み方は担当者達が金曜の夜23時頃にビールを飲みながら決めたのではないかと密かに思っています)。細かい仕様は色々ありますが、要はJSON形式で個人情報を含むデータを安全にやりとりするための基本データ構造体です。言い方はムズカシイですがいくつかの規定キーが存在するマップです。JWTの仕様上決まっているキーは少量なので問題ないのですが、OAuth2やOpenIDで付与することが可能な任意のキーも含めると色々と小回りが効く必要がでてきます。

JWK(RFC 7517)はJSON Web Keyの略です。RSA鍵やDSS鍵などをJSON形式で公開するための仕様です。それぞれの鍵が内部でどういうデータを持っているのか知らないと生のデータを見ても意味がわからないと思いますが、単純にそれぞれの鍵の内部データをダンプしているだけです。

JWS(RFC 7515)はJSON Web Signatureの略です。JWTに限らず任意のペイロードの内容を保証するための署名をつけ、その結果をJSON形式でエンコードする際の仕様を定義します。一般的に用いられている表記法はbase64エンコードされた値が三つピリオド(".")で結合されているものですが、実はこれはCompact Formatと呼ばれている簡易版の表記法です。Full JSON Formatを使うと、一つのメッセージに複数のJWS署名を付与してそれぞれ違う鍵を持っている受取人のみが検証できるようにすることができます。

JWE(RFC 7516)はJSON Web Encryptionの略です。JWTに限らず任意のペイロードを暗号化し、復号化に必要なヒントを付与した結果をJSON形式でエンコードする際の仕様を定義します。一般的に用いられている表記法はbase64エンコードされた値がいつつピリオド(".")で結合されているものですが、実はこれはCompact Formatと呼ばれている簡易版の表記法です。Full JSON Formatを使うと、一つのメッセージに別々の鍵で暗号化された複数のデータを付与して、それぞれの鍵を持っている受取人のみが復号化することができるようにすることができます。

Go言語での実装

今回このプロジェクトはGoで開発しました。当然できればすでに存在するライブラリを使い回したいので前述のClaims回りの事をGo言語から行うためのライブラリを探したところ、いくつかの候補がでてきましたが、どれも帯に短したすきに長しでした。

例えば github.com/golang/oauth2/jws ではペイロードであるClaimSetは規定のフィールド以外にもPrivateClaimsという形で任意の情報を入れることができるものの、逆にHeader構造体に任意の情報を入れることができません。さらに細かいところではJWSにはコンパクトとフルJSONという二つの表記法があるのですが、このライブラリは後者をサポートしていません。

これ以外のライブラリも、JWS/JWEは完璧にサポートしていてもJWTペイロードの内容を指定する際の自由度が低かったり、ClaimSetを自由にできない等の問題がでてきました。

これらの問題はOpenIDのクライアント側を実装するだけであればほぼ問題ないのですが、今回我々が目指していたのは柔軟性が必要なサーバー側の実装でした。結果、全ての要求を満たすために微妙に違う複数のライブラリを組み合わせて使わなければいけなかったりと少し困ってしまいました。結局のところ細かい部分も全てフレキシブルに実装するためにはJWSだけやJWTだけ実装するのではなく、JWT/JWS回りの全ての実装が自由度の高い形で実装されている必要があったのです。

残念ながら前述5つの仕様群は基本的には全てが渾然一体になった形で使われる事がほとんどです。OAuth2/OpenIDクライアント側から利用するだけ、というように適用範囲が狭められている場合にはそれぞれの一部を実装するだけで事足りてしまう事がおおいため、実に多くのライブラリは上記の本当に一部しか実装していません。

また、JWE/JWSの実装を選定する際にひとつ重要な指標があります。ライブラリを選定する際には必ず署名検証関数・復号化関数が署名・暗号アルゴリズムを明示的に要求する物を選びましょう。明示的なアルゴリズムの指定なしで表記方法から機械的に推測して検証・復号を行おうとするライブラリには潜在的なセキュリティ上の問題が存在します

auth0.com

ともあれ、すでにあるもので色々とまとめてみたところざっくり動く物は作れました。しかしやはりこの部分ではある実装を使い、こちらの部分ではまた違う実装を使う… というのはコードベースの保守性にも影響が出てきますし、違和感がぬぐいきれませんでした。さらに仕様を読んでも実装を読んでも自分が細部でのプロトコルの理解ができていないと感じた事もありました。

ならば完全に理解しつつ細かいところも納得出来る形で実現しよう!ということで自前の実装をすることにしました。それがgo-jwxです。

go-jwx

そんなこんなでしばらく悩んだ後、前述のRFCとにらめっこしつつgo-jwxの開発を行いました。使い方はGithubのREADMEを見ていただくとわかりやすいかと思います。

github.com

godoc.org

既存ライブラリ(特にgo-jose)へのPRも長い間考慮したのですが、最終的にはAPIを変えるような変更が必要そうであった事(そしてそのような変更の導入はなかなか難しい…)や時間の関係、そして最終的には自分がプロトコルの全てを理解する必要があるという判断の元開発を始めました。

go-jwxは現時点でいくつかのJWS/JWEアルゴリズムが未実装なものの、JOSE回りの全てのRFCを大枠では実装しており基本APIも安定しています。公開はしていませんが、とあるOpenID実装に組み込んだ上でOpenID FoundationのCertificationのテストも一応通っております。

個人的にひとつやってよかったと思っているのはそれぞれの実装を名称ごとにパッケージ化できた事です。JOSE回りの実装を見ていて常に理解がなかなか進まなかった理由の一つが、実は五つものRFCが存在するのに一つの名前空間にごちゃっと詰め込まれている事が多かったからです(しかも詰め込まれているものがその五つのRFCの全てというわけではなく、必要な部分が少しずつしか入ってなかったりした)。今回意図的に全ての実装を整理したため、コードを書いている時にもどのパーツがどのRFCに収まるのかわかりやすいかと思います。

まとめ

ということでいわゆる車輪の再発明になってしまいましたが、かなり良い形にまとまったのではないかと思います。是非利用を検討していただけるとありがたいです。PRもお待ちしております

開発した本人は最悪他のライブラリが改良されてまた移行することになったとしても確実に仕様を理解したので今後デバッグ等が生じた時に使える知識が増えたので万々歳です!