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ファイル)を生成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | $ openssl genrsa -out private_key.pem 2048Generating RSA private key, 2048 bit long modulus..............+++.........................................+++e is 65537 (0x10001)$ openssl rsa -pubout -in private_key.pem -out public_key.pemwriting RSA keysuzaku:Desktop ryuta$ cat public_key.pem-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxN6Yyl9sDNxXbUotsW4Ivr/Q0KLvsF38cGd+GgUF2mfq/JBJ2YbcfsAfjVcnorRGUSvpb2vpA5fYi1buoC1QoKW2adsPtum1MfdWe79nq8BalRijQqekVej0D5o5SUQ1MN7jIfcIlZED29R/Ep6+GiJfGxa8USb4mN5lRSoEYXPstL+QkBqZ7ov0+qhzONxn1uqABqWlrvMiOoNvgZ1P6WlooWk6uqtNySoGJNWij7dcoFWDMw/WIL10TPM467b7CTby9uGhJHOshv31Sic/CiwP3x7KID3Fvk50RX4iyNDueRpJa1aDZCe/jmlMt4CTioKVOjDIEYFGfuuYoimcNQIDAQAB-----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証明書を用意しました。
1 | <?php var_dump($_POST); |
では、curlコマンドでリクエストを送ってみます。今回はフィールド名secretが対象なので、それを含む場合とそうでない場合で比較します。まずは適当なフィールド名で。
1 2 3 4 5 6 7 8 9 10 11 | $ 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でリクエストしてみると...
1 2 3 4 5 6 7 8 9 10 11 12 | $ 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で秘密鍵をベタ書きして実装してみました。
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | # coding: utf-8import osimport aws_encryption_sdkfrom aws_encryption_sdk.internal.crypto import WrappingKeyfrom aws_encryption_sdk.key_providers.raw import RawMasterKeyProviderfrom aws_encryption_sdk.identifiers import WrappingAlgorithm, EncryptionKeyTypefrom Crypto.PublicKey import RSAimport base64provider_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.pyvalue1$ |
正しく復号出来ました!
まとめ
CloudFront Field-Level Encryptionを利用したフォームデータの暗号化の様子をご紹介しました。結構便利に使える仕組みだと思うので、今回は割愛しましたがSystem Manager、KMSともどもいじっていただければと思います。