Amazon CloudFront Field-Level Encryptionを利用してエッジサーバーでフォームデータを保護する
ども、大瀧です。 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 : 先ほど作成したProfile
testprofile
を選択
- Content typeはフォームデータを対象とするので、
後はCloudFrontディストリビューションのビヘイビアで、作成したEncryption Configurationを有効にします。
これでOKです。
動作確認
サーバーサイドはPHPで以下のスクリプトをDocument Rootに配置しました。オリジンプロトコルがHTTPSのみなので、ELBにACMのSSL証明書を用意しました。
<?php var_dump($_POST);
では、curl
コマンドでリクエストを送ってみます。今回はフィールド名secret
が対象なので、それを含む場合とそうでない場合で比較します。まずは適当なフィールド名で。
$ curl \ -d "param1=value1¶m2=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¶m2=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で秘密鍵をベタ書きして実装してみました。
# 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
脚注
- ビューワープロトコルの制約によりオリジンにHTTPリクエストが来ることは無いため、実質HTTPS Onlyと同等になります ↩