はじめに

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 の「表示—継承」に準拠しています。