投稿日
JWE、JWSとは?JWEとJWSによる暗号化の実装方法を事例を用いて解説
もくじ
はじめに
Javascript Object Signing and Encryption (以降JOSE)は、JSONを利用したデータ転送用の規格群のひとつです。
当事者間で承認情報などを安全に転送する方法を提供することを目的としています。
本ドキュメントは、JOSEを利用した暗号化の事例紹介です。
同様の技術を必要とする開発者や、暗号化について学習している開発者の参考となることを目的としています。
背景
今回事例を紹介するに至ったAPIの開発では、暗号化にJOSEを使用することが決定していました。
また、APIの暗号化のフローも下記のように決定していました。
- クライアントからリクエストでIDを受け取り、そのIDに紐づくユーザ情報を取得する
- 取得したユーザ情報の一部を署名し、更に暗号化する
- 暗号化したデータをJOSEのひとつであるJWE形式でクライアントに返却する
JOSEを利用した暗号化は、Nimbus JOSE + JWT(以降Nimbus)というオープンソースで実現できました。
JOSEについて調査するにあたり、以下の理由から、Nimbusを採用することを決定しました。
- NimbusがJOSE用の代表的なJavaライブラリのひとつであること
- テストを含めた実装例が豊富であること
JWE, JWS, JWTの概要
今回開発したAPIでは、JOSEが提供するJWE, JWS, JWTという3つの表示形式を使って暗号化を実現しました。
以下では、それぞれの仕様について詳細に説明します。
JSON Web Encryption (JWE)
JWE(RFC7516)は、JSONベースのデータ構造を使用してコンテンツを暗号化する表示形式です。
暗号化に必要な5つの要素をそれぞれBASE64URLでエンコードし、 .
で繋げたものがJWEです。 RFC 7516では、以下のように定義されています。
BASE64URL(UTF8(JWE Protected Header)) || '.' ||
BASE64URL(JWE Encrypted Key) || '.' ||
BASE64URL(JWE Initialization Vector) || '.' ||
BASE64URL(JWE Ciphertext) || '.' ||
BASE64URL(JWE Authentication Tag)
||
は、両側の値の連結を意味します。
5つの要素にはそれぞれ下記が設定されます。
- Protected Header
暗号化に使用するアルゴリズム - Encrypted Key
暗号化に使用した共通鍵
コンテンツ暗号化キー(CEK)と言い、暗号化した状態で設定される - Initialization Vector
平文の暗号化に使われる初期化ベクトル - Ciphertext
暗号化したデータ本体 - Authentication Tag
暗号文と追加認証データの整合性を保証する認証タグ
JSON Web Signature (JWS)
JWS(RFC7515)は、元のデータがJSON形式であることはJWEと同様ですが、コンテンツを暗号化ではなく署名したものです。
JWSは以下の3つの要素で構成されています。
BASE64URL(UTF8(JWS Protected Header)) || '.' ||
BASE64URL(JWS Payload) || '.' ||
BASE64URL(JWS Signature)
- Protected Header
署名に使用するアルゴリズム - Payload
コンテンツ - Signature
署名
署名の対象はHeader
とPayload
。ふたつを.
で繋げたHeader.Payload
に対して署名を行う
JSON Web Token (JWT)
JWT(RFC7519)は、JSONオブジェクト内のkey-valueのペアで表されるデータの集合のことです。JWEの Ciphertext
もしくはJWSの Payload
にその値を設定します。key-valueのペアで表されるデータのことをクレームと言います。
クレームはRFC 7519であらかじめ定義されたものがいくつか存在しますが、定義されたもの以外のクレームを自作して設定することも可能です。
暗号化仕様
暗号化に必要なもの
- ID検索で取得したユーザ情報(平文)
- サーバ側で発行した署名用の秘密鍵A
- クライアント側で発行した暗号用の公開鍵B
本APIでは鍵ペアをサーバ側とクライアント側でそれぞれ用意します。サーバ側で発行する鍵ペアをA、クライアント側で発行する鍵ペアをBとします。
署名にはサーバ側で発行した鍵ペアAの秘密鍵を利用します。
平文の暗号化に使った共通鍵の暗号化に、クライアント側で発行した鍵ペアBの公開鍵を利用します。
クライアント側で発行した公開鍵Bは、事前にファイル連携されたものをJavaに取り込んで使用しました。
仕様
今回の暗号化要件では、署名付きJWT(JWS)を更にクレームの値としてJWEに組み込んでいます。
JWE Ciphertext
の暗号化には共通鍵(コンテンツ暗号化キー)を使用します。 コンテンツ暗号化キーはRSA暗号化した後、 JWE Encrypted Key
に格納します。
以降に詳細な暗号化要件を説明します。
- JWTには以下のクレームを設定する(JWT)
クレーム名 | 説明 |
---|---|
iss |
JWTの発行者の情報を設定する |
sub |
JWTの本文を設定する |
customClaimKey |
カスタムクレーム |
- JWTを秘密鍵Aで署名する(JWS)
- JWSを含む平文を256bit鍵でAES暗号化する(JWE Ciphertext)
- 平文データもkey-valueのJSON形式で設定し、作成したJWSを
authValue
キーの値として設定する
キー | 説明 |
---|---|
userId |
リクエストで受け取るID |
userName |
IDに紐づくユーザ情報 |
authValue |
秘密鍵Aを使って作成したJWSの署名データ |
- AES暗号化に使用した共通鍵を、公開鍵Bを使ってRSA暗号化する(JWE Encrypted Key)
- JWE Headerにはアルゴリズム以外に以下のデータを設定する
キー | 説明 |
---|---|
customParamKey |
カスタムパラメータ |
JWTのクレームについて、 iss
と sub
はRFC 7519で定義されているクレームです。 customClaimKey
は本API独自のものとなりますが、実際にプロジェクトで使用したクレーム名は秘匿情報のため、本記事用に customClaimKey
という名前で例示しています。
JWE Headerの <span">customParamKey
についても同様、本記事用に作成している名前となります。
署名、暗号化に使用するアルゴリズム
- JWTの署名:
RS256
- JWE Ciphertextの暗号化:
A256GCM
- コンテンツ暗号化キーの暗号化:
RSA-OAEP-256
Nimbus JOSE + JWTのサイトはExampleが豊富で、要件に沿った実装を比較的容易に検索できました。
実装について、参考にしたNimbusのExampleと共に説明します。
実装
JWSの生成
JWSに設定したい値は、以下のJSON形式になります。
{"iss":"issuer", "sub":"subject", "customClaimKey":"customClaimValue"}
public class CreateJws {
public String createJws(PrivateKey privateKey, String issureId, String subjectName, String customClaimValue) {
// クレームの設定
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
builder.issuer(issureId).subject(subjectName).claim("customClaimKey", customClaimValue);
JWTClaimsSet claimsSet = builder.build();
// 署名の作成
JWSSigner signer = new RSASSASigner(privateKey);
// 署名
SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build(), claimsSet);
signedJWT.sign(signer);
// シリアライズ
return signedJWT.serialize();
}
}
参考Example
JWEの生成
Protected Headerの設定
JWEのヘッダーに暗号化で使用する2種類のアルゴリズムを設定します。
コンテンツキーを暗号化するアルゴリズムを alg
に
JWSを含むユーザー情報(平文)を暗号化するアルゴリズムを enc
に、それぞれ設定します。
alg
と enc
はヘッダーの必須項目です。
public class EncryptData {
public String createJwe(String id, String name, String customParamValue, String customClaimValue){
/* 中略 */
JWEAlgorithm alg = JWEAlgorithm.RSA-OAEP-256;
EncryptionMethod enc = EncryptionMethod.A256GCM;
Map<String, Object> customParams = new HashMap<>();
customParams.put("customParamKey", customParamValue);
JWEHeader.Builder builder = new JWEHeader.Builder(alg, enc);
builder.customParams(customParams);
JWEHeader header = builder.build();
/* 中略 */
}
}
Ciphertextの設定
ここでは、暗号化される前の平文が設定された jweObject
を作成します。
暗号化する平文は以下のJSONデータです。
{"userId":"id", "userName":"name", "authValue":"(JWS)"}
authValue
には先ほど生成した signedJWT
を設定します。
public class EncryptData {
public String createJwe(String id, String name, String customParamValue, String customClaimValue){
/* 中略 */
String jwsString = createJws(privateKey, issureId, subjectName, customClaimValue);
JSONObject json = new JSONObject();
json.put("userId", id);
json.put("userName", name);
json.put("authValue", jwsString);
Payload payload = new Payload(json.toString());
JWEObject jweObject = new JWEObject(header, payload);
/* 中略 */
}
}
JWE Encrypted Keyの設定、暗号化
平文の暗号化に利用するAES共通鍵を生成し、 jweObject
内の平文を暗号化します。
AES共通鍵は公開鍵Bで暗号化し、 Encrypted Key
に設定します。
public class EncryptData {
public String createJwe(String id, String name, String customParamValue, String customClaimValue){
/* 中略 */
// AES暗号鍵の作成
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(enc.cekBitLength());
SecretKey cek = keyGen.generateKey();
// 暗号化
jweObject.encrypt(new RSAEncrypter(publicKey, cek));
// シリアライズ
return jweObject.serialize();
/* 中略 */
}
}
参考Example
JSON Web Encryption (JWE) with a preset Content Encryption Key (CEK)
全文
public class EncryptData {
/**
* JWEを作成する。
*/
public String createJwe(String id, String name, String customParamValue, String customClaimValue){
//Protected Headerの設定
JWEAlgorithm alg = JWEAlgorithm.RSA-OAEP-256;
EncryptionMethod enc = EncryptionMethod.A256GCM;
Map<String, Object> customParams = new HashMap<>();
customParams.put("customParamKey", customParamValue);
JWEHeader.Builder builder = new JWEHeader.Builder(alg, enc);
builder.customParams(customParams);
JWEHeader header = builder.build();
// Ciphertextの設定
String jweString = createJws(privateKey, issureId, subjectName, customClaimValue);
JSONObject json = new JSONObject();
json.put("userId", id);
json.put("userName", name);
json.put("authValue", jweString);
Payload payload = new Payload(json.toString());
JWEObject jweObject = new JWEObject(header, payload);
// AES暗号鍵の作成
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(enc.cekBitLength());
SecretKey cek = keyGen.generateKey();
// 暗号化
jweObject.encrypt(new RSAEncrypter(publicKey, cek));
// シリアライズ
return jweObject.serialize();
}
/**
* JWSを作成する。
*/
public String createJws(PrivateKey privateKey, String issureId, String subjectName, String customClaimValue) {
// クレームの設定
JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder();
builder.issuer(issureId).subject(subjectName).claim("customClaimKey", customClaimValue);
JWTClaimsSet claimsSet = builder.build();
// 署名の作成
JWSSigner signer = new RSASSASigner(privateKey);
// 署名
SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build(), claimsSet);
// シリアライズ
return signedJWT.serialize();
}
}
まとめ
機密情報を扱うほとんどのサービスにおいて、暗号化は必須の技術です。
ITの分野に限らず暗号化という用語は一般的な知識として広く知られていますが、 実際のサービスにおける暗号化の複雑な仕様を理解し、実装することは言葉の意味を知ることに反して難解です。
今回は、暗号化の手法のひとつであるJOSEと、Numbusを使ったJOSEでの暗号化について紹介しました。
Nimbusを利用することで、JOSEを使った暗号化の仕様を簡単に吸収でき、開発の負担が減少しました。
また、それぞれの実装例に復号と署名検証の実装も展開されているため、検証で品質を担保することも可能でした。
JOSEのJWE, JWS, JWT形式での暗号化を検討されている方に、本記事が参考になれば幸いです。
本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。