Amazon CloudFront Field-Level Encryptionを利用してエッジサーバーでフォームデータを保護する

2017.12.18

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

ども、大瀧です。 CloudFrontの新機能としてField-Level Encryptionがリリースされました。試してみた様子をレポートします。

Field-Level Encryptionとは

CloudFrontはCDNサービスとしてWebサイトやWebアプリのキャッシュおよびリバースプロキシサーバーとして動作します。Field-Level EncryptionはHTMLフォームのフィールド単位で暗号化を施す仕組みです。ユニークなのは、トラフィックを転送するリバースプロキシで動作するところです。

秘匿情報の保護は、Webシステムの設計においてしばしば課題になります。Web/APサーバーのサーバーサイドアプリケーションで暗号化を施し、データベースなどに格納するのが一般的だと思いますが、暗号キーの安全な管理やマイクロサービスアーキテクチャでのサービス間の秘匿情報の受け渡しなど、実装上の課題は様々です。Field-Level Encryptionはサーバーに届く手前の早い段階で暗号化を施せること、AWS Encryption SDKによる標準化された暗号/復号プロセスを踏めることで秘匿情報の安全な管理機能を提供します。

Field-Level Encryptionの要件

ドキュメントには記載されていないようですが、本日時点で以下の制約があります。

  • オリジンプロトコルポリシーがHTTPS OnlyもしくはMatch Viewer *1であること
  • ビューワープロトコルポリシーがRedirect HTTP to HTTPSもしくはHTTPS Onlyであること
  • 保護対象になるのはPOSTのリクエストボディーのみ。クエリストリングやHTTPヘッダは保護対象外

設定手順

1. RSAキーペアの生成

Field-Level EncryptionはRSA公開鍵暗号を利用します。まずは、opensslコマンドでRSAのキーペア(秘密鍵private_key.pemファイルと公開鍵public_key.pemファイル)を生成します。

$ openssl genrsa -out private_key.pem 2048
Generating RSA private key, 2048 bit long modulus
..............+++
.........................................+++
e is 65537 (0x10001)
$ openssl rsa -pubout -in private_key.pem -out public_key.pem
writing RSA key
suzaku:Desktop ryuta$ cat public_key.pem
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxN6Yyl9sDNxXbUotsW4I
vr/Q0KLvsF38cGd+GgUF2mfq/JBJ2YbcfsAfjVcnorRGUSvpb2vpA5fYi1buoC1Q
oKW2adsPtum1MfdWe79nq8BalRijQqekVej0D5o5SUQ1MN7jIfcIlZED29R/Ep6+
GiJfGxa8USb4mN5lRSoEYXPstL+QkBqZ7ov0+qhzONxn1uqABqWlrvMiOoNvgZ1P
6WlooWk6uqtNySoGJNWij7dcoFWDMw/WIL10TPM467b7CTby9uGhJHOshv31Sic/
CiwP3x7KID3Fvk50RX4iyNDueRpJa1aDZCe/jmlMt4CTioKVOjDIEYFGfuuYoimc
NQIDAQAB
-----END PUBLIC KEY-----
$

2. CloudFrontの構成

生成した公開鍵をCloudFrontにアップロードし、保護するフィールドや暗号化を適用するディストリビューションの設定を入れ込んでいきます。 AWS Management ConsoleのCloudFront管理画面にあるメニューから[Security] - [Public Key]を選択し、[Add public key]ボタンをクリックします。

[Key name]に鍵名(復号時に利用します)、[Key value]に公開鍵のテキストデータ、[Comment]には任意のコメントを入力し[Create]ボタンをクリックして公開鍵を登録します。

続いて、アップロードした公開鍵で暗号化する対象のリクエストおよびフィールドを指定するProfileとEncryption Configurationを作成します。メニューから[Security] - [Field-Level encryption]を選択し、[Create Profile]ボタンをクリック、以下の項目を入力し[Create profile]ボタンをクリックしてProfileを作成します。

  • Profile name : Profile名(今回はtestprofileと入力しました)
  • Comment : 任意のコメント
  • Public key name : 作成した公開鍵(testkey)を選択
  • Provider Name : プロバイダ名(復号時に利用します。今回はtestと入力)
  • Field name pattern to match : 暗号化を施すフィールド名(今回はsecretにしました)

続いて画面下方の[Create configuration]ボタンをクリックしEncryption Configuration作成画面を開き作成します。以下を入力します。その他の項目はデフォルトにしました。

  • Comment : 任意のコメント
  • Content type profile mappings : 暗号化を施すHTTPリクエストの指定
    • Content typeはフォームデータを対象とするので、application/x-www-form-urlencoded
    • Default profile ID : 先ほど作成したProfiletestprofileを選択

後はCloudFrontディストリビューションのビヘイビアで、作成したEncryption Configurationを有効にします。

これでOKです。

動作確認

サーバーサイドはPHPで以下のスクリプトをDocument Rootに配置しました。オリジンプロトコルがHTTPSのみなので、ELBにACMのSSL証明書を用意しました。

index.php

<?php var_dump($_POST);

では、curlコマンドでリクエストを送ってみます。今回はフィールド名secretが対象なので、それを含む場合とそうでない場合で比較します。まずは適当なフィールド名で。

$ curl \
  -d "param1=value1&param2=value2" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -X POST \
  https://XXXXXXXXXX.cloudfront.net/
array(2) {
  ["param1"]=>
  string(6) "value1"
  ["param2"]=>
  string(6) "value2"
}

フィールド名secretを含まないので、暗号化はされません。続いてフィールド名secretでリクエストしてみると...

$ curl \
  -d "secret=value1&param2=value2" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -X POST \
  https://XXXXXXXXXX.cloudfront.net/
array(2) {
  ["secret"]=>
  string(508) "AYABeI8DnewDsHO+jd9QtlfwjkkAAAABAAR0ZXN0AAd0ZXN0a2V5AQC3PfYmtDOS659D1X67ubCEaDKipm18r80qFTk18XDjOSUlZtPoNT5ZmRW7BFYAOx0O+ugXRYN7Sv2qYc+SjITx2KOH/rESVfRmhOY0RvT1e7dwyw8+5w6N130zP+fjKnjUrefAiJLIzQrK9X3ovPu8P4Ky6d20CiOyYOWF/i05OqYkbPbIzIXE/EISzUUs7cXKjOmdYjxxnjmkrR36/lwZSEoB2oNZ7+sMAutLg05AhCtJmCjfRzI83EBANzZd3d6SGNwTfEvquDrh5xLq5Ct6Kbnomx24Fqyfw5D3hXrWcwF30q4KqiYkEk2b+cxwUo7Cyfv/y/+ICnU6goIo3nBcAgAAAAAMAAAQAAAAAAAAAAAAAAAAAN10Q9AByneE1lAKUltMkWv/////AAAAAQAAAAAAAAAAAAAAAQAAAAaEwoMpR5jMHuzJMkNrF8CScrAPgW/T"
  ["param2"]=>
  string(6) "value2"
}
$

暗号化されました!

あとは、暗号化されたデータを秘密鍵を使用して復号してみます。CloudFrontでは、内部でAWS Encryption SDKを用いて暗号化しているとのことで、復号にもSDKを利用します。SDKでは、AWS Systems ManagerのパラメータストアとAWS Key Management Serviceの利用を前提としていますが、今回は動作確認のためこちらのサンプルコードを参考にしつつ、Pythonで秘密鍵をベタ書きして実装してみました。

decode.py

# coding: utf-8

import os
import aws_encryption_sdk
from aws_encryption_sdk.internal.crypto import WrappingKey
from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider
from aws_encryption_sdk.identifiers import WrappingAlgorithm, EncryptionKeyType

from Crypto.PublicKey import RSA
import base64

provider_id   = 'test'
PublicKeyName = 'testkey'

def decrypt_data(event, context):
    class SIFPrivateMasterKeyProvider(RawMasterKeyProvider):
        provider_id = provider_id

        def __new__(cls, *args, **kwargs):
            obj = super(SIFPrivateMasterKeyProvider, cls).__new__(cls)
            return obj

        def __init__(self, private_key_id, private_key_text):
            RawMasterKeyProvider.__init__(self)

            private_key = RSA.importKey(private_key_text)
            self._key = private_key.exportKey()

            RawMasterKeyProvider.add_master_key(self, private_key_id)

        def _get_raw_key(self, key_id):
            return WrappingKey(
                wrapping_algorithm=WrappingAlgorithm.RSA_OAEP_SHA256_MGF1,
                wrapping_key=self._key,
                wrapping_key_type=EncryptionKeyType.PRIVATE
            )

    def DecryptField(private_key, field_data):
        # add padding if needed base64 decoding
        field_data = field_data + '=' * (-len(field_data) % 4)
        # base64-decode to get binary ciphertext
        ciphertext = base64.b64decode(field_data)
        # decrypt ciphertext into plaintext
        plaintext, header = aws_encryption_sdk.decrypt(
            source=ciphertext,
            key_provider=sif_private_master_key_provider
        )
        return plaintext

    private_key_text = '''
-----BEGIN RSA PRIVATE KEY-----
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  :
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
-----END RSA PRIVATE KEY-----
'''.strip()
    encrypted_text= 'AYABeI8DnewDsHO+jd9QtlfwjkkAAAABAAR0ZXN0AAd0ZXN0a2V5AQC3PfYmtDOS659D1X67ubCEaDKipm18r80qFTk18XDjOSUlZtPoNT5ZmRW7BFYAOx0O+ugXRYN7Sv2qYc+SjITx2KOH/rESVfRmhOY0RvT1e7dwyw8+5w6N130zP+fjKnjUrefAiJLIzQrK9X3ovPu8P4Ky6d20CiOyYOWF/i05OqYkbPbIzIXE/EISzUUs7cXKjOmdYjxxnjmkrR36/lwZSEoB2oNZ7+sMAutLg05AhCtJmCjfRzI83EBANzZd3d6SGNwTfEvquDrh5xLq5Ct6Kbnomx24Fqyfw5D3hXrWcwF30q4KqiYkEk2b+cxwUo7Cyfv/y/+ICnU6goIo3nBcAgAAAAAMAAAQAAAAAAAAAAAAAAAAAN10Q9AByneE1lAKUltMkWv/////AAAAAQAAAAAAAAAAAAAAAQAAAAaEwoMpR5jMHuzJMkNrF8CScrAPgW/T'
    sif_private_master_key_provider = SIFPrivateMasterKeyProvider(PublicKeyName, private_key_text)
    print(DecryptField( private_key_text, encrypted_text ))

def main():
	decrypt_data ("test", "test")

if __name__ == "__main__":
  main()

実行してみると...

$ pip install cryptography aws_encryption_sdk pycrypto
$ python decode.py
value1
$

正しく復号出来ました!

まとめ

CloudFront Field-Level Encryptionを利用したフォームデータの暗号化の様子をご紹介しました。結構便利に使える仕組みだと思うので、今回は割愛しましたがSystems Manager、KMSともどもいじっていただければと思います。

参考URL

脚注

  1. ビューワープロトコルの制約によりオリジンにHTTPリクエストが来ることは無いため、実質HTTPS Onlyと同等になります