« 2015年11月のAWS初心者向けWebinarのご案内 | メイン | AWS Black Belt 年の瀬座談会のご案内と質問/トピック募集のお願い »

AWS Key Management ServiceとEncryptionContextを利用して暗号化データの完全性を保護する方法

AWS Key Management Service (KMS)において、先進的でセキュアなデータ利用を行うためにもっとも重要かつクリティカルなコンセプトの一つがEncryptionContextです。EncryptionContextを適切に利用することで、アプリケーションのセキュリティを顕著に向上させることができます。この記事では、EncryptionContextの重要性をお知らせし、暗号化データの完全性と信憑性を保護するためにどのように利用できるかの簡単な例をお見せします。
 
EncryptionContextは、それぞれの暗号化、復号化のリクエストと一緒にKMSに対して指定するkey-valueマップです。暗号化の際のマップと復号化の際のマップは必ず一致する必要があり、一致しない場合は復号化のリクエストはfailします。
 
EncryptionContextは、3つの利点を提供します:
  1. 追加認証データ (AAD)
  2. 監査証跡
  3. 認証コンテキスト
ここでは、最初の利点であるAADにフォーカスしまが、これら3つの利点は全て従来の"関連データを伴う認証つき暗号化(AEAD)"の暗号化方式によってもたらされます。
 

AEADとは?

セキュリティのベストプラクティスは、秘匿データが秘密の状態を保ち(機密性)、変更されていないこと(完全性/信憑性)を要求することです。残念ながら、(AES-CBCのような)多くの古い暗号化形式は完全性の保証が有りません。よって、復号化や再暗号化無しにメッセージの意味を変更出来てしまうという潜在的な脆弱性をユーザーにもたらします。こういう状況を避けるため、AEAD暗号化を利用することが可能です。AEAD暗号化は1つのコンセプトの2つの関連性のあるパーツに別れます:認証つき暗号化("AEAD"の"AE"部分)と関連データ("AEAD"の"AD"部分)です。これらのパーツを1つづつ見ていきます。

認証つき暗号化(AE)

認証つき暗号化を利用すれば暗号文自身が改ざんを防ぎます。認証つき暗号化はKMSに組み込まれています。KMSを利用してメッセージを復号化するためには認証されたユーザーがメッセージを作成する必要があります。これは、暗号文に対する"署名"が提供されると考えて頂いても良いです。

例として、改ざんされた暗号文を受信した場合にKMSがInvalidCiphertextExceptionを投げる以下のコードを見て下さい。

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

import com.amazonaws.services.kms.*;
import com.amazonaws.services.kms.model.*;

public class Example1 {
  public static void main(final String[] args) {
    final AWSKMS kms = new AWSKMSClient();

    final String plaintext = "My very secret message";
    final byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
    System.out.println("Plaintext: " + plaintext);

    // データの暗号化
    final EncryptRequest encReq = new EncryptRequest();
    encReq.setKeyId("alias/EcDemo");
    encReq.setPlaintext(ByteBuffer.wrap(plaintextBytes));
    final ByteBuffer ciphertext = kms.encrypt(encReq).getCiphertextBlob();

    // データの復号化
    final DecryptRequest decReq1 = new DecryptRequest();
    decReq1.setCiphertextBlob(ciphertext);
    final ByteBuffer decrypted = kms.decrypt(decReq1).getPlaintext();
    final String decryptedStr = new String(decrypted.array(), StandardCharsets.UTF_8);
    System.out.println("Decrypted: " + decryptedStr);

    // 暗号文の改ざん
    final byte[] tamperedCt = ciphertext.array().clone();
    // 最後から24バイト目の1バイト分の全てのビットを反転
    tamperedCt[tamperedCt.length - 24] ^= 0xff; 

    final DecryptRequest decReq2 = new DecryptRequest();
    decReq2.setCiphertextBlob(ByteBuffer.wrap(tamperedCt));

    try {
      kms.decrypt(decReq2).getPlaintext();
    } catch (final InvalidCiphertextException ex) {
      ex.printStackTrace();
    }
  }
}

関連データ(AD)

認証つき暗号化が暗号文自身の改ざんを防いでも、先ほどのコードにはメッセージのコンテキストが保護されないという問題があります。暗号化データは、完全に自己完結型であることはめったに無く、暗号化されていないコンテキストに依存します。例えば、暗号文をある場所から別の場所へコピーすることによって、誰かがコンテキストを変更し、システムで利用することができるかもしれません。

この問題を解決するため、(KMSを含む)最近の殆どの認証つき暗号化手法では、AADをサポートしています。AADは暗号文には直接含まれませんが、AADの完全性はAEAD暗号化を用いて保護されます。これは、追加データをカバーするように暗号化テキストに対する署名を拡張したものと考えることができます。一般的に、AADは何ら秘匿情報を含まないものであり、秘匿情報を理解するための文脈情報です。

 

EnCryptionContextとは?

EncryptionContextは、KMSにおけるAADの実装です。暗号文に関連付けられた平文データが改ざんに対して保護されていることを確認するためにその利用を強く推奨します。一般的にAADに利用するデータは、ヘッダー情報、同一レコード内の暗号化されていないデータベースフィールド、ファイル名、その他のメタデータを含みます。EncryptionContextは、AWS CloudTrail内にJSON形式の平文データとして保管され、情報は格納するバケットにアクセスできる誰もが見ることができるため、EncryptionContextにはセンシティブでないデータのみを含めるということを覚えておくことが重要です。

次のシナリオは、EncryptionContextをAADとして利用する例です。この例では、ユーザーが自分の住所を保存して参照できる共有連絡帳を持っていることを想像して下さい。住所データをAmazon DynamoDBのテーブルに保管する前に暗号化したいとします。(テーブルはString型のハッシュキーとしてEmailAddresをもち、住所は一致するemailアドレスに関連付けられているものとします。)

まず最初に、間違った方法でセキュアでない実装をしてみます。(読者が誤って利用するのを防ぐために、利用すべきでないメソッドについてはコメントアウトしてあります。) このセキュアでない実装においては、ユーザーMalloryがDynamoDBのテーブルを変更することができる場合、Aliceの住所を置き換える事が可能です。Malloryは単純にレコード間の暗号化された住所を置き換えるだけで、暗号化キーのアクセスさえ無しにこれが出来てしまいます。彼女は何の暗号化も復号化も要求されません。環境によっては、これは住所を暗号化するという目的に対して完全な欠点になりえます。レコードを置き換えた後に、Malloryがまるで自分の住所をみるように簡単にAliceの住所を参照でき、Aliceが自分のために注文した商品はMalloryの住所に代わりに届けられる事になります。

次のコードは、この意図的にセキュアでない実装を示しています。

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;

import com.amazonaws.services.dynamodbv2.*;
import com.amazonaws.services.kms.*;
import com.amazonaws.services.kms.model.*;

public class Example2 {
  private static final String ADDRESS = "Address";
  private static final String EMAIL = "EmailAddress";
  private static final String TABLE = "EcDemoAddresses";
  final static AWSKMS kms = new AWSKMSClient();
  final static AmazonDynamoDB ddb = new AmazonDynamoDBClient();

  public static void main(final String[] args) {
    // Alice が住所を保存
    saveAddress("alice@example.com", "Alice Lovelace, 123 Anystreet Rd., Anytown, USA");
    // Malloryが住所を保存 
    saveAddress("mallory@example.com",
        "Mallory Evesdotir, 321 Evilstreed Ave., Despair, USA");

    // 保存された住所を出力
    System.out.println("Alice's Address: " + getAddress("alice@example.com"));
    System.out.println("Mallory's Address: " + getAddress("mallory@example.com"));

    // Malloryが暗号化された住所を交換することでデータベースを改ざん
    // 暗号文を全く変更する必要が無いことに注意
    // 最初に、DynamoDBからレコードを取得
    final Map<String, AttributeValue> mallorysRecord = ddb
        .getItem(
            TABLE,
            Collections.singletonMap(EMAIL,
                new AttributeValue().withS("mallory@example.com"))).getItem();
    final Map<String, AttributeValue> alicesRecord = ddb.getItem(TABLE,
        Collections.singletonMap(EMAIL, new AttributeValue().withS("alice@example.com")))
        .getItem();

    // 次に暗号化された住所を抽出
    final ByteBuffer mallorysEncryptedAddress = mallorysRecord.get(ADDRESS).getB();
    final ByteBuffer alicesEncryptedAddress = alicesRecord.get(ADDRESS).getB();

    // 暗号化されたデータを入れ替え
    mallorysRecord.put(ADDRESS, new AttributeValue().withB(alicesEncryptedAddress));
    alicesRecord.put(ADDRESS, new AttributeValue().withB(mallorysEncryptedAddress));

    // 最後にDynamoDBに保管し直す
    ddb.putItem(TABLE, mallorysRecord);
    ddb.putItem(TABLE, alicesRecord);

    // Aliceが自分のアドレスを利用しようとする  (何かを出荷するよう手配すると)
    // 代わりにMalloryに送られる
    System.out.println("Alice's Address: " + getAddress("alice@example.com"));
    // 同様にMalloryが自分の住所を見ようとすると、代わりにAliceの住所が見える
    System.out.println("Mallory's Address: " + getAddress("mallory@example.com"));
  }

// DO NOT USE:   private static void saveAddress(final String email, final String address) {
// DO NOT USE:     final EncryptRequest enc = new EncryptRequest();
// DO NOT USE:     enc.setKeyId("alias/EcDemo");
// DO NOT USE:     enc.setPlaintext(ByteBuffer.wrap(address.getBytes(StandardCharsets.UTF_8)));
// DO NOT USE:     final ByteBuffer ciphertext = kms.encrypt(enc).getCiphertextBlob();
// DO NOT USE: 
// DO NOT USE:     final Map<String, AttributeValue> item = new HashMap<>();
// DO NOT USE:     item.put(EMAIL, new AttributeValue().withS(email));
// DO NOT USE:     item.put(ADDRESS, new AttributeValue().withB(ciphertext));
// DO NOT USE:     ddb.putItem(TABLE, item);
// DO NOT USE:   }
// DO NOT USE: 
// DO NOT USE:   private static String getAddress(final String email) {
// DO NOT USE:     final Map<String, AttributeValue> item = ddb.getItem(TABLE,
// DO NOT USE:         Collections.singletonMap(EMAIL, new AttributeValue().withS(email))).getItem();
// DO NOT USE:     final DecryptRequest dec = new DecryptRequest();
// DO NOT USE:     dec.setCiphertextBlob(item.get(ADDRESS).getB());
// DO NOT USE:     final ByteBuffer plaintext = kms.decrypt(dec).getPlaintext();
// DO NOT USE:     return new String(plaintext.array(), StandardCharsets.UTF_8);
// DO NOT USE:   }
}

この意図的にセキュアでない実装において、Malloryは依然として暗号文を修正する権限がなくともシステムを攻撃する事ができます。誤って解釈されるように暗号文のコンテキストを変更できるため、彼女は攻撃を実施することが出来ます。このケースでは、"単に"アドレスを変えるだけです。しかしながら、同じ攻撃でセンシティブデータを露出したり、アカウントの乗っ取りさえ可能なことは明白です。

この問題は、暗号化された住所に関連付けられているemailアドレスをEncryptionContextとして含めることでこの問題を修正することが出来ます。これにより、システムが改ざんされたレコードの復号化を行う際にInvalidCiphertextExceptionが投げられ、脅威は緩和されます。

これは、暗号化の際に提示されたEncryptionContextパラメーター(この例ではAliceのemailアドレス)と、復号化の際に提示されるEncryptionContextパラメーター(この例ではMalloryのemailアドレス)が一致しないためです。

次のコードはセキュリティの実装を改善します。
private static void saveAddress(final String email, final String address) {
  final EncryptRequest enc = new EncryptRequest();
  enc.setKeyId("alias/EcDemo");
  enc.setPlaintext(ByteBuffer.wrap(address.getBytes(StandardCharsets.UTF_8)));
  enc.setEncryptionContext(Collections.singletonMap(EMAIL, email));
  final ByteBuffer ciphertext = kms.encrypt(enc).getCiphertextBlob();

  final Map<String, AttributeValue> item = new HashMap<>();
  item.put(EMAIL, new AttributeValue().withS(email));
  item.put(ADDRESS, new AttributeValue().withB(ciphertext));
  ddb.putItem(TABLE, item);
}

private static String getAddress(final String email) {
  final Map<String, AttributeValue> item = ddb.getItem(TABLE,
      Collections.singletonMap(EMAIL, new AttributeValue().withS(email))).getItem();
  final DecryptRequest dec = new DecryptRequest();
  dec.setCiphertextBlob(item.get(ADDRESS).getB());
  dec.setEncryptionContext(Collections.singletonMap(EMAIL, email));
  final ByteBuffer plaintext = kms.decrypt(dec).getPlaintext();
  return new String(plaintext.array(), StandardCharsets.UTF_8);
}

もちろん、レコード全体をあるDynamoDBのテーブルから他のテーブルへ移すなど、攻撃者ができうることは他にもあります。これが、後で解読する必要がある暗号文と関連する情報の全てをEncryptionContextに含めておくべき理由です。暗号文の位置を一意に識別できるのに最低限十分な情報(例えば、URI、ファイルパス、データベースのテーブルとプライマリーキー)を常に含めておくのが良いルールです。

最良のコードは、自分の関心(暗号化であることはめったにありません)に集中することができ、暗号化コードを専門のグループに任せることができて、自分で書く必要がないコードです。このケースでは、aws-dynamodb-encryption-javaライブラリを利用することが可能です。このライブラリには、DynamoDBHashKey(および利用可能な場合のRangeKey)だけではなく、テーブル名と利用されている暗号化アルゴリズムを含むEncryptionContextを使用します。

最後のコードサンプルは例題アプリケーションにおいてaws-dynamodb-encryption-javaライブラリを利用した改善されたよりセキュアな実装です。

import java.nio.ByteBuffer;
import java.security.*;
import java.util.*;

import com.amazonaws.services.dynamodbv2.*;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.*;
import com.amazonaws.services.dynamodbv2.datamodeling.encryption.
providers.DirectKmsMaterialProvider;
import com.amazonaws.services.dynamodbv2.model.AttributeValue;
import com.amazonaws.services.kms.*;

public class Example4 {
  private static final String ADDRESS = "Address";
  private static final String EMAIL = "EmailAddress";
  private static final String TABLE = "EcDemoAddresses";
  final static AWSKMS kms = new AWSKMSClient();
  final static AmazonDynamoDB ddb = new AmazonDynamoDBClient();

  // aws-dynamodb-encryption-java ライブラリのセットアップ
  final static DynamoDBEncryptor cryptor = DynamoDBEncryptor.getInstance(
      new DirectKmsMaterialProvider(kms, "alias/EcDemo"));
  // 同じ名前ではあるがDynamoDb EncryptionContextは 単にKMSのEncryptionContex
// だけではなくDynamoDBEncryptorの鍵とアルゴリズム選択のガイド
// その他に利用される final static EncryptionContext ddbCtx = new EncryptionContext.Builder() .withTableName(TABLE) .withHashKeyName(EMAIL) .build(); public static void main(final String[] args) throws GeneralSecurityException { // Aliceが住所を保存 saveAddress("alice@example.com", "Alice Lovelace, 123 Anystreet Rd., Anytown, USA"); // Malloryが住所を保存 saveAddress("mallory@example.com", "Mallory Evesdotir, 321 Evilstreed Ave., Despair, USA"); // 保存された住所の出力 System.out.println("Alice's Address: " + getAddress("alice@example.com")); System.out.println("Mallory's Address: " + getAddress("mallory@example.com"));     // Malloryが暗号化された住所を交換することでデータベースを改ざん
    // 暗号文を全く変更する必要が無いことに注意
    // 最初に、DynamoDBからレコードを取得 final Map<String, AttributeValue> mallorysRecord = ddb .getItem( TABLE, Collections.singletonMap(EMAIL, new AttributeValue().withS("mallory@example.com"))).getItem(); final Map<String, AttributeValue> alicesRecord = ddb.getItem(TABLE, Collections.singletonMap(EMAIL, new AttributeValue().withS("alice@example.com"))) .getItem();     // 次に暗号化された住所を抽出
  final ByteBuffer mallorysEncryptedAddress = mallorysRecord.get(ADDRESS).getB(); final ByteBuffer alicesEncryptedAddress = alicesRecord.get(ADDRESS).getB();     // 暗号化されたデータを入れ替え
  mallorysRecord.put(ADDRESS, new AttributeValue().withB(alicesEncryptedAddress)); alicesRecord.put(ADDRESS, new AttributeValue().withB(mallorysEncryptedAddress));     // 最後にDynamoDBに保管し直す
  ddb.putItem(TABLE, mallorysRecord); ddb.putItem(TABLE, alicesRecord); // Aliceが住所を利用しようとすると、改ざんされたデータの復号化を試行
// SignatureExceptionを受け取る try { System.out.println("Alice's Address: " + getAddress("alice@example.com")); // Malloryが自分のアドレスを見ようとすると同様になる
System.out.println("Mallory's Address: " + getAddress("mallory@example.com")); } catch (final SignatureException ex) { ex.printStackTrace(); } } private static void saveAddress(final String email, final String address) throws GeneralSecurityException { final Map<String, AttributeValue> item = new HashMap<>(); item.put(EMAIL, new AttributeValue().withS(email)); item.put(ADDRESS, new AttributeValue().withS(address)); final Map<String, AttributeValue> encryptedItem = cryptor.encryptAllFieldsExcept( item, ddbCtx, EMAIL); ddb.putItem(TABLE, encryptedItem); } private static String getAddress(final String email) throws GeneralSecurityException { final Map<String, AttributeValue> encryptedItem = ddb.getItem(TABLE, Collections.singletonMap(EMAIL, new AttributeValue().withS(email))).getItem(); final Map<String, AttributeValue> item = cryptor.decryptAllFieldsExcept( encryptedItem, ddbCtx, EMAIL); return item.get(ADDRESS).getS(); }

AEADは過去20年間の暗号化方式の中でより重要な進歩の一つです。 ここでは単にAADがどれほどシステムのセキュリティにとってクリティカルかという例をお見せしました。私の個人的な経験では、KMSを利用して暗号化される大部分のデータは、関連付けられたEncryptionContextをもつ必要があります。この強力なツールを最高の形で活用するため、お客様のシステムと新しい開発方法を見直すことをお勧めします。

もしこの記事に対して質問やコメントがある場合は、KMS forumまでお問い合わせ下さい。

- Greg(翻訳はSA布目が担当しました。元記事はこちら)

コメント

Twitter, Facebook

このブログの最新情報はTwitterFacebookでもお知らせしています。お気軽にフォローください。

2018年4 月

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30