Web Cryptography (WebCrypto) APIを使うと、JavaScriptで暗号化や復号、署名やその検証等の処理を実行することができるようになります。特に、CryptoJSでも扱われていないRSA等も扱えるのが便利かもしれません。
Chrome 37以降、Firefox 34以降で利用できるようですので、まず試してみました。なお、Safari 8でもwebkitプレフィクス付きで使えるようですが(window.crypto.webkitSubtle
)、仕様が異なるのかこちらでは動作確認が取れなかったため、今回はChromeとFirefoxのみに限定することにします。
今回は、GoogleのOpenID Connect認証で得られるJWTを例にして試してみます。
JSON Web Token (JWT)
例えば、GoogleアカウントでOpenID Connectを使った認証を自分のWebサイトに実装すると、ログイン成功時にリダイレクトURIに遷移する際、JWTを格納したパラメータid_token
がURLクエリパラメータの一つとして付与されます。
具体的には、次のような内容になります:
[ヘッダ].[クレーム].[シグネチャ]
[ヘッダ]
は、署名に使われた鍵の識別子をJSONで記述したものをBase64 URLエンコードしたものとなります。Base64 URLデコードして得られるJSON文字列の内容は、例えば次のようになります:
{"alg":"RS256","kid":"7158d85c857f368bf3a448b22720a3a55b073447"}
[クレーム]
は、トークンで検証するIDプロバイダ名やユーザ名等の情報をJSONで記述したものをBase64 URLエンコードしたものになります。
ここで、[ヘッダ]
のkid
で示されるIDプロバイダ(ここではGoogle)の秘密鍵を用いて、ペイロード[ヘッダ].[クレーム]
に署名して得られるシグネチャをBase64 URLエンコードしたものが、上記の[シグネチャ]
となります。
Google OpenID Connectの場合、署名を検証するために必要となる公開鍵(JWK; JSON Web Key)がWebサイト上で公開されています。一定時間ごとに更新されるため、署名の検証時にはその都度JWKの内容をWebサイトから取得したほうがよいでしょう。
(参考): http://christina04.hatenablog.com/entry/2015/01/27/131259
まず、鍵を取得
それでは、順を追ってJWTの検証を試してみます。
簡単のため、今回はJWTの内容を予め変数に入れておくことにします。
まず、GoogleのWebサイトよりJWKを取得し、JWTのヘッダで指定されているkid
の鍵を選択します。
var token = '[JWTの内容]';
function base64UrlDecode(t) {
return atob(t.replace(/\-/g, '+').replace(/_/g, '/'));
}
function getJSON(e) { return e.json(); }
function getJWK(e) {
var key;
var k = JSON.parse(base64UrlDecode(token.split('.')[0]));
for(var i of e.keys) {
if(i.kid == k.kid) {
key = i;
// Base64のpaddingが残っていると、Chromeではcrypto.subtle.importKeyでエラー
key.n = key.n.replace(/=+$/, '');
}
}
if(key)
getCryptoKey(key);
}
fetch('https://www.googleapis.com/oauth2/v2/certs').then(getJSON).then(getJWK);
次に、得られたJWKをインポートして、WebCryptoで利用するCryptoKey
オブジェクトを取得します。
function getCryptoKey(key) {
crypto.subtle.importKey('jwk', key, {
name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' }
}, true, ['verify']).then(verifyJWT);
}
上記の例では、関数verifyJWT()
の引数として、CryptoKey
オブジェクトとして格納されたGoogleのRSA公開鍵が渡されることになります。
crypto.subtle.importKey()
の引数ですが、第1引数はインポート元の鍵のデータフォーマットを指定します。JWKであれば、'jwk'
を指定し、第2引数でJWKのJSONオブジェクトをそのまま渡せばよいことになります。
第3引数で、暗号化や署名の方式を指定しますが、JWTのヘッダでRS256
が指定されているため、WebCryptoの場合は、署名の方式として'RSASSA-PKCS1-v1_5'
(長いですが)、ハッシュの方式として'SHA-256'
を指定します。
第4引数は、鍵をエクスポートできるようにしてよいかどうかを指定します。
第5引数では、鍵の用途を一つないし複数、配列で指定します。暗号化なら'encrypt'
、復号なら'decrypt'
、署名なら'sign'
、署名の検証なら'verify'
、などが指定可能です。
得られた鍵で署名を検証
これまでの手順で得られた公開鍵を用いて、JWTの内容が真正なものかどうかを検証します。
function toBufferSource(t) {
var r = new Uint8Array(t.length);
for(var i = 0 ; i < s.length ; i++)
r[i] = s.charCodeAt(i);
return r;
}
function verifyJWT(cryptoKey) {
var t = token.split('.');
var signature = toBufferSource(base64UrlDecode(t[2]));
var payload = toBufferSource(t[0] + '.' + t[1]);
crypto.subtle.verify({
name: 'RSASSA-PKCS1-v1_5', hash: { name: 'SHA-256' }
}, cryptoKey, signature, payload).then(function(result) {
console.log(result ? '検証成功' : '検証失敗');
});
}
crypto.subtle.verify()
の引数ですが、第1引数はcrypto.subtle.importKey()
の第3引数と同じです。第2引数には検証に用いる公開鍵、すなわちcrypto.subtle.importKey()
で取得したCryptoKey
オブジェクトを渡します。
さらに、第3引数には署名をBase64 URLデコードしてUint8Array
に格納したもの、第4引数には検証したいJWTデータ([ヘッダ].[クレーム]
)をUint8Array
に格納したものを渡すと、Promiseで検証結果をbooleanで取得できます。