diff --git a/.gitmodules b/.gitmodules index ff8103c56..870af609a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "test_vector_handlers/test/aws-crypto-tools-test-vector-framework"] path = test_vector_handlers/test/aws-crypto-tools-test-vector-framework - url = https://github.com/awslabs/aws-crypto-tools-test-vector-framework.git + url = https://github.com/awslabs/private-aws-crypto-tools-test-vector-framework-staging.git diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9f2f0553c..6fe2f108e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,15 @@ Changelog ********* +2.2.0 -- 2021-05-27 +=================== + +Features +-------- +* Improvements to the message decryption process + + See https://github.com/aws/aws-encryption-sdk-python/security/advisories/GHSA-x5h4-9gqw-942j. + 2.1.0 -- 2020-04-20 =================== diff --git a/buildspec.yml b/buildspec.yml index 4a6bdac42..639d56321 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -7,67 +7,42 @@ batch: buildspec: codebuild/py27/integ.yml - identifier: py27_examples buildspec: codebuild/py27/examples.yml - - identifier: py27_awses_1_7_1 - buildspec: codebuild/py27/awses_1.7.1.yml - - identifier: py27_awses_2_0_0 - buildspec: codebuild/py27/awses_2.0.0.yml - - identifier: py27_awses_latest - buildspec: codebuild/py27/awses_latest.yml + - identifier: py27_awses_local + buildspec: codebuild/py27/awses_local.yml - identifier: py35_integ buildspec: codebuild/py35/integ.yml - identifier: py35_examples buildspec: codebuild/py35/examples.yml - - identifier: py35_awses_1_7_1 - buildspec: codebuild/py35/awses_1.7.1.yml - - identifier: py35_awses_2_0_0 - buildspec: codebuild/py35/awses_2.0.0.yml - - identifier: py35_awses_latest - buildspec: codebuild/py35/awses_latest.yml + - identifier: py35_awses_local + buildspec: codebuild/py35/awses_local.yml - identifier: py36_integ buildspec: codebuild/py36/integ.yml - identifier: py36_examples buildspec: codebuild/py36/examples.yml - - identifier: py36_awses_1_7_1 - buildspec: codebuild/py36/awses_1.7.1.yml - - identifier: py36_awses_2_0_0 - buildspec: codebuild/py36/awses_2.0.0.yml - - identifier: py36_awses_latest - buildspec: codebuild/py36/awses_latest.yml + - identifier: py36_awses_local + buildspec: codebuild/py36/awses_local.yml - identifier: py37_integ buildspec: codebuild/py37/integ.yml - identifier: py37_examples buildspec: codebuild/py37/examples.yml - - identifier: py37_awses_1_7_1 - buildspec: codebuild/py37/awses_1.7.1.yml - - identifier: py37_awses_2_0_0 - buildspec: codebuild/py37/awses_2.0.0.yml - - identifier: py37_awses_latest - buildspec: codebuild/py37/awses_latest.yml + - identifier: py37_awses_local + buildspec: codebuild/py37/awses_local.yml - identifier: py38_integ buildspec: codebuild/py38/integ.yml - identifier: py38_examples buildspec: codebuild/py38/examples.yml - - identifier: py38_awses_1_7_1 - buildspec: codebuild/py38/awses_1.7.1.yml - - identifier: py38_awses_2_0_0 - buildspec: codebuild/py38/awses_2.0.0.yml - - identifier: py38_awses_latest - buildspec: codebuild/py38/awses_latest.yml + - identifier: py38_awses_local + buildspec: codebuild/py38/awses_local.yml - identifier: py39_integ buildspec: codebuild/py39/integ.yml - identifier: py39_examples buildspec: codebuild/py39/examples.yml - - identifier: py39_awses_1_7_1 - buildspec: codebuild/py39/awses_1.7.1.yml - - identifier: py39_awses_2_0_0 - buildspec: codebuild/py39/awses_2.0.0.yml - identifier: py39_awses_latest - buildspec: codebuild/py39/awses_latest.yml - identifier: code_coverage buildspec: codebuild/coverage/coverage.yml diff --git a/codebuild/py27/awses_latest.yml b/codebuild/py27/awses_latest.yml deleted file mode 100644 index a813060e8..000000000 --- a/codebuild/py27/awses_latest.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py27-awses_latest" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pip install tox - - cd test_vector_handlers - - tox diff --git a/codebuild/py27/awses_1.7.1.yml b/codebuild/py27/awses_local.yml similarity index 95% rename from codebuild/py27/awses_1.7.1.yml rename to codebuild/py27/awses_local.yml index 8f5cca0ec..2f84a43ab 100644 --- a/codebuild/py27/awses_1.7.1.yml +++ b/codebuild/py27/awses_local.yml @@ -2,7 +2,7 @@ version: 0.2 env: variables: - TOXENV: "py27-awses_1.7.1" + TOXENV: "py27-awses_local" AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- diff --git a/codebuild/py35/awses_1.7.1.yml b/codebuild/py35/awses_1.7.1.yml deleted file mode 100644 index d7c6e3bd4..000000000 --- a/codebuild/py35/awses_1.7.1.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py35-awses_1.7.1" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pyenv install 3.5.9 - - pyenv local 3.5.9 - - pip install tox tox-pyenv - - cd test_vector_handlers - - tox diff --git a/codebuild/py35/awses_latest.yml b/codebuild/py35/awses_latest.yml deleted file mode 100644 index d56efa94f..000000000 --- a/codebuild/py35/awses_latest.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py35-awses_latest" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pyenv install 3.5.9 - - pyenv local 3.5.9 - - pip install tox tox-pyenv - - cd test_vector_handlers - - tox diff --git a/codebuild/py35/awses_2.0.0.yml b/codebuild/py35/awses_local.yml similarity index 95% rename from codebuild/py35/awses_2.0.0.yml rename to codebuild/py35/awses_local.yml index ae47785fa..127e329f9 100644 --- a/codebuild/py35/awses_2.0.0.yml +++ b/codebuild/py35/awses_local.yml @@ -2,7 +2,7 @@ version: 0.2 env: variables: - TOXENV: "py35-awses_2.0.0" + TOXENV: "py35-awses_local" AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- diff --git a/codebuild/py36/awses_1.7.1.yml b/codebuild/py36/awses_1.7.1.yml deleted file mode 100644 index 80d2a67e3..000000000 --- a/codebuild/py36/awses_1.7.1.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py36-awses_1.7.1" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pip install tox - - cd test_vector_handlers - - tox diff --git a/codebuild/py36/awses_latest.yml b/codebuild/py36/awses_latest.yml deleted file mode 100644 index f4f141d28..000000000 --- a/codebuild/py36/awses_latest.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py36-awses_latest" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pip install tox - - cd test_vector_handlers - - tox diff --git a/codebuild/py27/awses_2.0.0.yml b/codebuild/py36/awses_local.yml similarity index 95% rename from codebuild/py27/awses_2.0.0.yml rename to codebuild/py36/awses_local.yml index bb667f4df..023dbd00d 100644 --- a/codebuild/py27/awses_2.0.0.yml +++ b/codebuild/py36/awses_local.yml @@ -2,7 +2,7 @@ version: 0.2 env: variables: - TOXENV: "py27-awses_2.0.0" + TOXENV: "py36-awses_local" AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- diff --git a/codebuild/py37/awses_1.7.1.yml b/codebuild/py37/awses_1.7.1.yml deleted file mode 100644 index 08584fb4b..000000000 --- a/codebuild/py37/awses_1.7.1.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py37-awses_1.7.1" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pyenv install 3.7.9 - - pyenv local 3.7.9 - - pip install tox tox-pyenv - - cd test_vector_handlers - - tox diff --git a/codebuild/py37/awses_latest.yml b/codebuild/py37/awses_latest.yml deleted file mode 100644 index ec882400b..000000000 --- a/codebuild/py37/awses_latest.yml +++ /dev/null @@ -1,23 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py37-awses_latest" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pyenv install 3.7.9 - - pyenv local 3.7.9 - - pip install tox tox-pyenv - - cd test_vector_handlers - - tox diff --git a/codebuild/py37/awses_2.0.0.yml b/codebuild/py37/awses_local.yml similarity index 95% rename from codebuild/py37/awses_2.0.0.yml rename to codebuild/py37/awses_local.yml index 3935d4b53..29ce46381 100644 --- a/codebuild/py37/awses_2.0.0.yml +++ b/codebuild/py37/awses_local.yml @@ -2,7 +2,7 @@ version: 0.2 env: variables: - TOXENV: "py37-awses_2.0.0" + TOXENV: "py37-awses_local" AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- diff --git a/codebuild/py38/awses_1.7.1.yml b/codebuild/py38/awses_1.7.1.yml deleted file mode 100644 index 450166b3f..000000000 --- a/codebuild/py38/awses_1.7.1.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py38-awses_1.7.1" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pip install tox - - cd test_vector_handlers - - tox diff --git a/codebuild/py38/awses_2.0.0.yml b/codebuild/py38/awses_2.0.0.yml deleted file mode 100644 index 5d7210748..000000000 --- a/codebuild/py38/awses_2.0.0.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py38-awses_2.0.0" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pip install tox - - cd test_vector_handlers - - tox diff --git a/codebuild/py38/awses_latest.yml b/codebuild/py38/awses_latest.yml deleted file mode 100644 index ba8c26514..000000000 --- a/codebuild/py38/awses_latest.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: 0.2 - -env: - variables: - TOXENV: "py38-awses_latest" - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- - arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f - AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- - arn:aws:kms:eu-central-1:658956600833:key/75414c93-5285-4b57-99c9-30c1cf0a22c2 - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" - AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" - -phases: - install: - runtime-versions: - python: latest - build: - commands: - - pip install tox - - cd test_vector_handlers - - tox diff --git a/codebuild/py36/awses_2.0.0.yml b/codebuild/py38/awses_local.yml similarity index 95% rename from codebuild/py36/awses_2.0.0.yml rename to codebuild/py38/awses_local.yml index c54afd266..7e5cdef40 100644 --- a/codebuild/py36/awses_2.0.0.yml +++ b/codebuild/py38/awses_local.yml @@ -2,7 +2,7 @@ version: 0.2 env: variables: - TOXENV: "py36-awses_2.0.0" + TOXENV: "py38-awses_local" AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID: >- arn:aws:kms:us-west-2:658956600833:key/b3537ef1-d8dc-4780-9f5a-55776cbb2f7f AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2: >- diff --git a/examples/src/basic_file_encryption_with_multiple_providers.py b/examples/src/basic_file_encryption_with_multiple_providers.py index 28b55d9de..6a1b7ccd1 100644 --- a/examples/src/basic_file_encryption_with_multiple_providers.py +++ b/examples/src/basic_file_encryption_with_multiple_providers.py @@ -103,18 +103,18 @@ def cycle_file(key_arn, source_plaintext_filename, botocore_session=None): ciphertext.write(chunk) # Decrypt the ciphertext with only the AWS KMS master key + # Buffer the data in memory before writing to disk to ensure the signature is verified first. with open(ciphertext_filename, "rb") as ciphertext, open(cycled_kms_plaintext_filename, "wb") as plaintext: with client.stream( source=ciphertext, mode="d", key_provider=aws_encryption_sdk.StrictAwsKmsMasterKeyProvider(**kms_kwargs) ) as kms_decryptor: - for chunk in kms_decryptor: - plaintext.write(chunk) + plaintext.write(kms_decryptor.read()) # Decrypt the ciphertext with only the static master key + # Buffer the data in memory before writing to disk to ensure the signature is verified first. with open(ciphertext_filename, "rb") as ciphertext, open(cycled_static_plaintext_filename, "wb") as plaintext: with client.stream(source=ciphertext, mode="d", key_provider=static_master_key_provider) as static_decryptor: - for chunk in static_decryptor: - plaintext.write(chunk) + plaintext.write(static_decryptor.read()) # Verify that the "cycled" (encrypted, then decrypted) plaintext is identical to the source plaintext assert filecmp.cmp(source_plaintext_filename, cycled_kms_plaintext_filename) diff --git a/examples/src/basic_file_encryption_with_raw_key_provider.py b/examples/src/basic_file_encryption_with_raw_key_provider.py index 0260c5cef..2d964c7b4 100644 --- a/examples/src/basic_file_encryption_with_raw_key_provider.py +++ b/examples/src/basic_file_encryption_with_raw_key_provider.py @@ -15,7 +15,7 @@ import os import aws_encryption_sdk -from aws_encryption_sdk.identifiers import CommitmentPolicy, EncryptionKeyType, WrappingAlgorithm +from aws_encryption_sdk.identifiers import Algorithm, CommitmentPolicy, EncryptionKeyType, WrappingAlgorithm from aws_encryption_sdk.internal.crypto.wrapping_keys import WrappingKey from aws_encryption_sdk.key_providers.raw import RawMasterKeyProvider @@ -66,14 +66,22 @@ def cycle_file(source_plaintext_filename): cycled_plaintext_filename = source_plaintext_filename + ".decrypted" # Encrypt the plaintext source data + # We can use an unsigning algorithm suite here under the assumption that the contexts that encrypt + # and decrypt are equally trusted. with open(source_plaintext_filename, "rb") as plaintext, open(ciphertext_filename, "wb") as ciphertext: - with client.stream(mode="e", source=plaintext, key_provider=master_key_provider) as encryptor: + with client.stream( + algorithm=Algorithm.AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + mode="e", + source=plaintext, + key_provider=master_key_provider, + ) as encryptor: for chunk in encryptor: ciphertext.write(chunk) # Decrypt the ciphertext + # We can use the recommended "decrypt-unsigned" streaming mode since we encrypted with an unsigned algorithm suite. with open(ciphertext_filename, "rb") as ciphertext, open(cycled_plaintext_filename, "wb") as plaintext: - with client.stream(mode="d", source=ciphertext, key_provider=master_key_provider) as decryptor: + with client.stream(mode="decrypt-unsigned", source=ciphertext, key_provider=master_key_provider) as decryptor: for chunk in decryptor: plaintext.write(chunk) diff --git a/examples/src/one_kms_cmk_streaming_data.py b/examples/src/one_kms_cmk_streaming_data.py index 62348a363..88eb4eb5c 100644 --- a/examples/src/one_kms_cmk_streaming_data.py +++ b/examples/src/one_kms_cmk_streaming_data.py @@ -49,10 +49,10 @@ def encrypt_decrypt_stream(key_arn, source_plaintext_filename, botocore_session= ciphertext.write(chunk) # Decrypt the encrypted message using the AWS Encryption SDK. + # Buffer the data in memory before writing to disk to ensure the signature is verified first. with open(ciphertext_filename, "rb") as ciphertext, open(decrypted_text_filename, "wb") as plaintext: with client.stream(source=ciphertext, mode="d", key_provider=kms_key_provider) as decryptor: - for chunk in decryptor: - plaintext.write(chunk) + plaintext.write(decryptor.read()) # Check if the original message and the decrypted message are the same assert filecmp.cmp(source_plaintext_filename, decrypted_text_filename) diff --git a/src/aws_encryption_sdk/__init__.py b/src/aws_encryption_sdk/__init__.py index 58c8e2320..0525f332d 100644 --- a/src/aws_encryption_sdk/__init__.py +++ b/src/aws_encryption_sdk/__init__.py @@ -13,11 +13,15 @@ """High level AWS Encryption SDK client functions.""" # Below are imported for ease of use by implementors +import warnings + import attr from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache # noqa from aws_encryption_sdk.caches.null import NullCryptoMaterialsCache # noqa +from aws_encryption_sdk.exceptions import AWSEncryptionSDKClientError # noqa from aws_encryption_sdk.identifiers import Algorithm, CommitmentPolicy, __version__ # noqa +from aws_encryption_sdk.internal.utils.signature import SignaturePolicy # noqa from aws_encryption_sdk.key_providers.kms import ( # noqa DiscoveryAwsKmsMasterKeyProvider, KMSMasterKeyProviderConfig, @@ -39,6 +43,8 @@ class EncryptionSDKClientConfig(object): :param commitment_policy: The commitment policy to apply to encryption and decryption requests :type commitment_policy: aws_encryption_sdk.materials_manager.identifiers.CommitmentPolicy + :param max_encrypted_data_keys: The maximum number of encrypted data keys to allow during encryption and decryption + :type max_encrypted_data_keys: None or positive int """ commitment_policy = attr.ib( @@ -46,6 +52,14 @@ class EncryptionSDKClientConfig(object): default=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, validator=attr.validators.instance_of(CommitmentPolicy), ) + max_encrypted_data_keys = attr.ib( + hash=True, validator=attr.validators.optional(attr.validators.instance_of(int)), default=None + ) + + def __attrs_post_init__(self): + """Applies post-processing which cannot be handled by attrs.""" + if self.max_encrypted_data_keys is not None and self.max_encrypted_data_keys < 1: + raise ValueError("max_encrypted_data_keys cannot be less than 1") class EncryptionSDKClient(object): @@ -63,6 +77,22 @@ def __new__(cls, **kwargs): instance.config = config return instance + def _set_config_kwargs(self, callee_name, kwargs_dict): + """ + Copy relevant StreamEncryptor/StreamDecryptor configuration from `self.config` into `kwargs`, + raising and exception if the keys already exist in `kwargs`. + """ + for key in ("commitment_policy", "max_encrypted_data_keys"): + if key in kwargs_dict: + warnings.warn( + "Invalid keyword argument '{key}' passed to {callee}. " + "Set this value by passing a 'config' to the EncryptionSDKClient constructor instead.".format( + key=key, callee=callee_name + ) + ) + kwargs_dict["commitment_policy"] = self.config.commitment_policy + kwargs_dict["max_encrypted_data_keys"] = self.config.max_encrypted_data_keys + def encrypt(self, **kwargs): """Encrypts and serializes provided plaintext. @@ -111,7 +141,8 @@ def encrypt(self, **kwargs): :returns: Tuple containing the encrypted ciphertext and the message header object :rtype: tuple of bytes and :class:`aws_encryption_sdk.structures.MessageHeader` """ - kwargs["commitment_policy"] = self.config.commitment_policy + self._set_config_kwargs("encrypt", kwargs) + kwargs["signature_policy"] = SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT with StreamEncryptor(**kwargs) as encryptor: ciphertext = encryptor.read() return ciphertext, encryptor.header @@ -157,7 +188,8 @@ def decrypt(self, **kwargs): :returns: Tuple containing the decrypted plaintext and the message header object :rtype: tuple of bytes and :class:`aws_encryption_sdk.structures.MessageHeader` """ - kwargs["commitment_policy"] = self.config.commitment_policy + self._set_config_kwargs("decrypt", kwargs) + kwargs["signature_policy"] = SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT with StreamDecryptor(**kwargs) as decryptor: plaintext = decryptor.read() return plaintext, decryptor.header @@ -214,13 +246,24 @@ def stream(self, **kwargs): or :class:`aws_encryption_sdk.streaming_client.StreamDecryptor` :raises ValueError: if supplied with an unsupported mode value """ - kwargs["commitment_policy"] = self.config.commitment_policy + self._set_config_kwargs("stream", kwargs) mode = kwargs.pop("mode") + + _signature_policy_map = { + "e": SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT, + "encrypt": SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT, + "d": SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT, + "decrypt": SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT, + "decrypt-unsigned": SignaturePolicy.ALLOW_ENCRYPT_FORBID_DECRYPT, + } + kwargs["signature_policy"] = _signature_policy_map[mode.lower()] + _stream_map = { "e": StreamEncryptor, "encrypt": StreamEncryptor, "d": StreamDecryptor, "decrypt": StreamDecryptor, + "decrypt-unsigned": StreamDecryptor, } try: return _stream_map[mode.lower()](**kwargs) diff --git a/src/aws_encryption_sdk/exceptions.py b/src/aws_encryption_sdk/exceptions.py index 98eb69e8e..ed4fea744 100644 --- a/src/aws_encryption_sdk/exceptions.py +++ b/src/aws_encryption_sdk/exceptions.py @@ -25,6 +25,22 @@ class CustomMaximumValueExceeded(SerializationError): """Exception class for use when values are found which exceed user-defined custom maximum values.""" +class MaxEncryptedDataKeysExceeded(CustomMaximumValueExceeded): + """ + Exception class for use when a message or encryption materials + contain more encrypted data keys than a configured maximum value. + """ + + def __init__(self, num_keys, max_keys): + """Prepares exception message.""" + super(MaxEncryptedDataKeysExceeded, self).__init__( + "Number of encrypted data keys found larger than configured value: {num_keys:d} > {max_keys:d}".format( + num_keys=num_keys, + max_keys=max_keys, + ) + ) + + class UnknownIdentityError(AWSEncryptionSDKClientError): """Exception class for unknown identity errors.""" diff --git a/src/aws_encryption_sdk/identifiers.py b/src/aws_encryption_sdk/identifiers.py index 90de9eb82..ab6bcadd8 100644 --- a/src/aws_encryption_sdk/identifiers.py +++ b/src/aws_encryption_sdk/identifiers.py @@ -27,7 +27,7 @@ # We only actually need these imports when running the mypy checks pass -__version__ = "2.1.0" +__version__ = "2.2.0" USER_AGENT_SUFFIX = "AwsEncryptionSdkPython/{}".format(__version__) @@ -261,6 +261,10 @@ def is_committing(self): upper_bytes = self.id_as_bytes()[0] return upper_bytes in (0x04, 0x05) + def is_signing(self): + """Determine whether this algorithm suite includes signing.""" + return self.signing_algorithm_info is not None + def message_id_length(self): """Returns the size of the message id.""" if self.message_format_version == 0x01: diff --git a/src/aws_encryption_sdk/internal/crypto/elliptic_curve.py b/src/aws_encryption_sdk/internal/crypto/elliptic_curve.py index 83e6b2def..fd96abf6f 100644 --- a/src/aws_encryption_sdk/internal/crypto/elliptic_curve.py +++ b/src/aws_encryption_sdk/internal/crypto/elliptic_curve.py @@ -123,6 +123,7 @@ def _ecc_decode_compressed_point(curve, compressed_point): y_order_map = {b"\x02": 0, b"\x03": 1} raw_x = compressed_point[1:] raw_x = to_bytes(raw_x) + # pylint gives a false positive that int_from_bytes is not callable x = int_from_bytes(raw_x, "big") # pylint: disable=not-callable raw_y = compressed_point[0] # In Python3, bytes index calls return int values rather than strings diff --git a/src/aws_encryption_sdk/internal/formatting/deserialize.py b/src/aws_encryption_sdk/internal/formatting/deserialize.py index d18ae60a7..d2cfb1095 100644 --- a/src/aws_encryption_sdk/internal/formatting/deserialize.py +++ b/src/aws_encryption_sdk/internal/formatting/deserialize.py @@ -19,7 +19,18 @@ from cryptography.exceptions import InvalidTag -from aws_encryption_sdk.exceptions import NotSupportedError, SerializationError, UnknownIdentityError +try: # Python 3.5.0 and 3.5.1 have incompatible typing modules + from typing import Union # noqa pylint: disable=unused-import +except ImportError: # pragma: no cover + # We only actually need these imports when running the mypy checks + pass + +from aws_encryption_sdk.exceptions import ( + MaxEncryptedDataKeysExceeded, + NotSupportedError, + SerializationError, + UnknownIdentityError, +) from aws_encryption_sdk.identifiers import ( AlgorithmSuite, ContentType, @@ -124,15 +135,18 @@ def _verified_algorithm_from_id(algorithm_id): return algorithm_suite -def _deserialize_encrypted_data_keys(stream): - # type: (IO) -> Set[EncryptedDataKey] +def deserialize_encrypted_data_keys(stream, max_encrypted_data_keys=None): + # type: (IO, Union[int, None]) -> Set[EncryptedDataKey] """Deserialize some encrypted data keys from a stream. :param stream: Stream from which to read encrypted data keys + :param max_encrypted_data_keys: Maximum number of encrypted data keys to deserialize :return: Loaded encrypted data keys :rtype: set of :class:`EncryptedDataKey` """ (encrypted_data_key_count,) = unpack_values(">H", stream) + if max_encrypted_data_keys and encrypted_data_key_count > max_encrypted_data_keys: + raise MaxEncryptedDataKeysExceeded(encrypted_data_key_count, max_encrypted_data_keys) encrypted_data_keys = set([]) for _ in range(encrypted_data_key_count): (key_provider_length,) = unpack_values(">H", stream) @@ -226,14 +240,16 @@ def _verified_frame_length(frame_length, content_type): return frame_length -def _deserialize_header_v1(header, tee_stream): - # type: (IO) -> MessageHeader +def _deserialize_header_v1(header, tee_stream, max_encrypted_data_keys): + # type: (IO, Union[int, None]) -> MessageHeader """Deserializes the header from a source stream in SerializationVersion.V1. :param header: A dictionary in which to store deserialized values :type header: dict :param tee_stream: The stream from which to read bytes :type tee_stream: aws_encryption_sdk.internal.utils.streams.TeeStream + :param max_encrypted_data_keys: Maximum number of encrypted keys to deserialize + :type max_encrypted_data_keys: None or positive int :returns: Deserialized MessageHeader object :rtype: :class:`aws_encryption_sdk.structures.MessageHeader` :raises NotSupportedError: if unsupported data types are found @@ -252,7 +268,7 @@ def _deserialize_header_v1(header, tee_stream): header["encryption_context"] = deserialize_encryption_context(tee_stream.read(ser_encryption_context_length)) - header["encrypted_data_keys"] = _deserialize_encrypted_data_keys(tee_stream) + header["encrypted_data_keys"] = deserialize_encrypted_data_keys(tee_stream, max_encrypted_data_keys) (content_type_id,) = unpack_values(">B", tee_stream) header["content_type"] = _verified_content_type_from_id(content_type_id) @@ -269,7 +285,7 @@ def _deserialize_header_v1(header, tee_stream): return MessageHeader(**header) -def _deserialize_header_v2(header, tee_stream): +def _deserialize_header_v2(header, tee_stream, max_encrypted_data_keys): # type: (IO) -> MessageHeader """Deserializes the header from a source stream in SerializationVersion.V2. @@ -277,6 +293,8 @@ def _deserialize_header_v2(header, tee_stream): :type header: dict :param tee_stream: The stream from which to read bytes :type tee_stream: aws_encryption_sdk.internal.utils.streams.TeeStream + :param max_encrypted_data_keys: Maximum number of encrypted keys to deserialize + :type max_encrypted_data_keys: None or positive int :returns: Deserialized MessageHeader object :rtype: :class:`aws_encryption_sdk.structures.MessageHeader` :raises NotSupportedError: if unsupported data types are found @@ -292,7 +310,7 @@ def _deserialize_header_v2(header, tee_stream): header["encryption_context"] = deserialize_encryption_context(tee_stream.read(ser_encryption_context_length)) - header["encrypted_data_keys"] = _deserialize_encrypted_data_keys(tee_stream) + header["encrypted_data_keys"] = deserialize_encrypted_data_keys(tee_stream, max_encrypted_data_keys) (content_type_id,) = unpack_values(">B", tee_stream) header["content_type"] = _verified_content_type_from_id(content_type_id) @@ -307,12 +325,14 @@ def _deserialize_header_v2(header, tee_stream): return MessageHeader(**header) -def deserialize_header(stream): - # type: (IO) -> MessageHeader +def deserialize_header(stream, max_encrypted_data_keys=None): + # type: (IO, Union[int, None]) -> MessageHeader """Deserializes the header from a source stream :param stream: Source data stream :type stream: io.BytesIO + :param max_encrypted_data_keys: Maximum number of encrypted keys to deserialize + :type max_encrypted_data_keys: None or positive int :returns: Deserialized MessageHeader object :rtype: :class:`aws_encryption_sdk.structures.MessageHeader` and bytes :raises NotSupportedError: if unsupported data types are found @@ -328,9 +348,9 @@ def deserialize_header(stream): header["version"] = version if version == SerializationVersion.V1: - return _deserialize_header_v1(header, tee_stream), tee.getvalue() + return _deserialize_header_v1(header, tee_stream, max_encrypted_data_keys), tee.getvalue() elif version == SerializationVersion.V2: - return _deserialize_header_v2(header, tee_stream), tee.getvalue() + return _deserialize_header_v2(header, tee_stream, max_encrypted_data_keys), tee.getvalue() else: raise NotSupportedError("Unrecognized message format version: {}".format(version)) diff --git a/src/aws_encryption_sdk/internal/utils/signature.py b/src/aws_encryption_sdk/internal/utils/signature.py new file mode 100644 index 000000000..74b481eb6 --- /dev/null +++ b/src/aws_encryption_sdk/internal/utils/signature.py @@ -0,0 +1,33 @@ +# Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. +"""Helper functions for validating signature policies and algorithms for the AWS Encryption SDK.""" + +from enum import Enum + +from aws_encryption_sdk.exceptions import ActionNotAllowedError + + +class SignaturePolicy(Enum): + """Controls algorithm suites that can be used on encryption and decryption.""" + + ALLOW_ENCRYPT_ALLOW_DECRYPT = 0 + ALLOW_ENCRYPT_FORBID_DECRYPT = 1 + + +def validate_signature_policy_on_decrypt(signature_policy, algorithm): + """Validates that the provided algorithm does not violate the signature policy for a decrypt request.""" + if signature_policy == SignaturePolicy.ALLOW_ENCRYPT_FORBID_DECRYPT and algorithm.is_signing(): + error_message = ( + "Configuration conflict. Cannot decrypt signed message in decrypt-unsigned mode. Algorithm ID was {}. " + ) + raise ActionNotAllowedError(error_message.format(algorithm.algorithm_id)) diff --git a/src/aws_encryption_sdk/streaming_client.py b/src/aws_encryption_sdk/streaming_client.py index 98b7c7fa6..ac215c259 100644 --- a/src/aws_encryption_sdk/streaming_client.py +++ b/src/aws_encryption_sdk/streaming_client.py @@ -28,6 +28,7 @@ AWSEncryptionSDKClientError, CustomMaximumValueExceeded, MasterKeyProviderError, + MaxEncryptedDataKeysExceeded, NotSupportedError, SerializationError, ) @@ -59,6 +60,7 @@ validate_commitment_policy_on_decrypt, validate_commitment_policy_on_encrypt, ) +from aws_encryption_sdk.internal.utils.signature import SignaturePolicy, validate_signature_policy_on_decrypt from aws_encryption_sdk.key_providers.base import MasterKeyProvider from aws_encryption_sdk.materials_managers import DecryptionMaterialsRequest, EncryptionMaterialsRequest from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager @@ -70,13 +72,15 @@ @attr.s(hash=True) @six.add_metaclass(abc.ABCMeta) -class _ClientConfig(object): +class _ClientConfig(object): # pylint: disable=too-many-instance-attributes """Parent configuration object for StreamEncryptor and StreamDecryptor objects. :param source: Source data to encrypt or decrypt :type source: str, bytes, io.IOBase, or file :param commitment_policy: The commitment policy to use during encryption and decryption :type commitment_policy: aws_encryption_sdk.identifiers.CommitmentPolicy + :param max_encrypted_data_keys: The maximum number of encrypted data keys to allow during encryption and decryption + :type max_encrypted_data_keys: None or positive int :param materials_manager: `CryptoMaterialsManager` from which to obtain cryptographic materials (either `materials_manager` or `key_provider` required) :type materials_manager: aws_encryption_sdk.materials_manager.base.CryptoMaterialsManager @@ -95,6 +99,14 @@ class _ClientConfig(object): hash=True, validator=attr.validators.instance_of(CommitmentPolicy), ) + signature_policy = attr.ib( + hash=True, + default=SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT, + validator=attr.validators.instance_of(SignaturePolicy), + ) + max_encrypted_data_keys = attr.ib( + hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(int)) + ) materials_manager = attr.ib( hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(CryptoMaterialsManager)) ) @@ -461,6 +473,10 @@ def _prep_message(self): ).format(requested=self.config.algorithm, provided=self._encryption_materials.algorithm) ) + num_keys = len(self._encryption_materials.encrypted_data_keys) + if self.config.max_encrypted_data_keys and num_keys > self.config.max_encrypted_data_keys: + raise MaxEncryptedDataKeysExceeded(num_keys, self.config.max_encrypted_data_keys) + if self._encryption_materials.signing_key is None: self.signer = None else: @@ -786,11 +802,13 @@ def _read_header(self): :rtype: tuple of aws_encryption_sdk.structures.MessageHeader and aws_encryption_sdk.internal.structures.MessageHeaderAuthentication :raises CustomMaximumValueExceeded: if frame length is greater than the custom max value + :raises CustomMaximumValueExceeded: if number of encrypted data keys is greater than the custom max value """ - header, raw_header = deserialize_header(self.source_stream) + header, raw_header = deserialize_header(self.source_stream, self.config.max_encrypted_data_keys) self.__unframed_bytes_read += len(raw_header) validate_commitment_policy_on_decrypt(self.config.commitment_policy, header.algorithm) + validate_signature_policy_on_decrypt(self.config.signature_policy, header.algorithm) if ( self.config.max_body_length is not None diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py index fea3cc278..228439450 100644 --- a/test/integration/integration_test_utils.py +++ b/test/integration/integration_test_utils.py @@ -76,3 +76,14 @@ def setup_kms_master_key_provider_with_botocore_session(cache=True): _KMS_MKP_BOTO = kms_master_key_provider return kms_master_key_provider + + +def setup_kms_master_key_provider_with_duplicate_keys(num_keys): + """Reads the test_values config file and builds the requested KMS Master Key Provider with multiple copies of + the requested key.""" + assert num_keys > 1 + cmk_arn = get_cmk_arn() + provider = StrictAwsKmsMasterKeyProvider(key_ids=[cmk_arn]) + for _ in range(num_keys - 1): + provider.add_master_key_provider(StrictAwsKmsMasterKeyProvider(key_ids=[cmk_arn])) + return provider diff --git a/test/integration/test_i_aws_encrytion_sdk_client.py b/test/integration/test_i_aws_encrytion_sdk_client.py index baa00ddb7..5dd13e4c6 100644 --- a/test/integration/test_i_aws_encrytion_sdk_client.py +++ b/test/integration/test_i_aws_encrytion_sdk_client.py @@ -18,7 +18,13 @@ from botocore.exceptions import BotoCoreError import aws_encryption_sdk -from aws_encryption_sdk.exceptions import DecryptKeyError, EncryptKeyError, MasterKeyProviderError +from aws_encryption_sdk.exceptions import ( + ActionNotAllowedError, + CustomMaximumValueExceeded, + DecryptKeyError, + EncryptKeyError, + MasterKeyProviderError, +) from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX, Algorithm, CommitmentPolicy from aws_encryption_sdk.internal.arn import arn_from_str from aws_encryption_sdk.key_providers.kms import ( @@ -32,6 +38,7 @@ get_cmk_arn, setup_kms_master_key_provider, setup_kms_master_key_provider_with_botocore_session, + setup_kms_master_key_provider_with_duplicate_keys, ) pytestmark = [pytest.mark.integ] @@ -748,3 +755,89 @@ def test_encrypt_failure_discovery_provider(self): frame_length=1024, ) excinfo.match("No Master Keys available from Master Key Provider") + + def test_decrypt_unsigned_success_unsigned_message(self): + """Test that "decrypt-unsigned" mode accepts unsigned messages.""" + ciphertext, _ = aws_encryption_sdk.EncryptionSDKClient().encrypt( + source=VALUES["plaintext_128"], + algorithm=Algorithm.AES_256_GCM_HKDF_SHA512_COMMIT_KEY, + key_provider=self.kms_master_key_provider, + encryption_context=VALUES["encryption_context"], + frame_length=1024, + ) + + with aws_encryption_sdk.EncryptionSDKClient().stream( + source=io.BytesIO(ciphertext), key_provider=self.kms_master_key_provider, mode="decrypt-unsigned" + ) as decryptor: + plaintext = decryptor.read() + assert plaintext == VALUES["plaintext_128"] + + def test_decrypt_unsigned_failure_signed_message(self): + """Test that "decrypt-unsigned" mode rejects signed messages.""" + ciphertext, _ = aws_encryption_sdk.EncryptionSDKClient().encrypt( + source=VALUES["plaintext_128"], + key_provider=self.kms_master_key_provider, + encryption_context=VALUES["encryption_context"], + frame_length=1024, + ) + + with aws_encryption_sdk.EncryptionSDKClient().stream( + source=io.BytesIO(ciphertext), key_provider=self.kms_master_key_provider, mode="decrypt-unsigned" + ) as decryptor: + with pytest.raises(ActionNotAllowedError) as excinfo: + decryptor.read() + excinfo.match("Configuration conflict. Cannot decrypt signed message in decrypt-unsigned mode.") + + @pytest.mark.parametrize("num_keys", (2, 3)) + def test_encrypt_cycle_within_max_encrypted_data_keys(self, num_keys): + """Test that the client can encrypt and decrypt messages with fewer + EDKs than the configured max.""" + provider = setup_kms_master_key_provider_with_duplicate_keys(num_keys) + client = aws_encryption_sdk.EncryptionSDKClient( + commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, + max_encrypted_data_keys=3, + ) + ciphertext, _ = client.encrypt( + source=VALUES["plaintext_128"], + key_provider=provider, + ) + plaintext, _ = client.decrypt( + source=ciphertext, + key_provider=provider, + ) + assert plaintext == VALUES["plaintext_128"] + + def test_encrypt_over_max_encrypted_data_keys(self): + """Test that the client refuses to encrypt when too many EDKs are provided.""" + provider = setup_kms_master_key_provider_with_duplicate_keys(4) + client = aws_encryption_sdk.EncryptionSDKClient( + commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, + max_encrypted_data_keys=3, + ) + with pytest.raises(CustomMaximumValueExceeded) as exc_info: + _, _ = client.encrypt( + source=VALUES["plaintext_128"], + key_provider=provider, + ) + exc_info.match("Number of encrypted data keys found larger than configured value") + + def test_decrypt_over_max_encrypted_data_keys(self): + """Test that the client refuses to decrypt a message with too many EDKs.""" + provider = setup_kms_master_key_provider_with_duplicate_keys(4) + encrypt_client = aws_encryption_sdk.EncryptionSDKClient( + commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, + ) + ciphertext, _ = encrypt_client.encrypt( + source=VALUES["plaintext_128"], + key_provider=provider, + ) + decrypt_client = aws_encryption_sdk.EncryptionSDKClient( + commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, + max_encrypted_data_keys=3, + ) + with pytest.raises(CustomMaximumValueExceeded) as exc_info: + _, _ = decrypt_client.decrypt( + source=ciphertext, + key_provider=provider, + ) + exc_info.match("Number of encrypted data keys found larger than configured value") diff --git a/test/unit/test_deserialize.py b/test/unit/test_deserialize.py index cef66b986..f5d257971 100644 --- a/test/unit/test_deserialize.py +++ b/test/unit/test_deserialize.py @@ -19,7 +19,12 @@ from mock import MagicMock, patch, sentinel import aws_encryption_sdk.internal.formatting.deserialize -from aws_encryption_sdk.exceptions import NotSupportedError, SerializationError, UnknownIdentityError +from aws_encryption_sdk.exceptions import ( + CustomMaximumValueExceeded, + NotSupportedError, + SerializationError, + UnknownIdentityError, +) from aws_encryption_sdk.identifiers import AlgorithmSuite, SerializationVersion from aws_encryption_sdk.internal.structures import EncryptedData @@ -420,3 +425,32 @@ def test_deserialize_wrapped_key_symmetric_wrapping_algorithm_incomplete_tag2(se ], ) excinfo.match("Malformed key info: incomplete ciphertext or tag") + + def test_deserialize_encrypted_data_keys_no_max_encrypted_data_keys(self): + edks = aws_encryption_sdk.internal.formatting.deserialize.deserialize_encrypted_data_keys( + edks_stream(2 ** 16 - 1), max_encrypted_data_keys=None + ) + assert len(edks) == 2 ** 16 - 1 + + @pytest.mark.parametrize("num_keys", (2, 3)) + def test_deserialize_encrypted_data_keys_within_max_encrypted_data_keys(self, num_keys): + edks = aws_encryption_sdk.internal.formatting.deserialize.deserialize_encrypted_data_keys( + edks_stream(num_keys), 3 + ) + assert len(edks) == num_keys + + def test_deserialize_encrypted_data_keys_over_max_encrypted_data_keys(self): + with pytest.raises(CustomMaximumValueExceeded) as excinfo: + aws_encryption_sdk.internal.formatting.deserialize.deserialize_encrypted_data_keys(edks_stream(4), 3) + excinfo.match("Number of encrypted data keys found larger than custom value") + + +def edks_stream(num_keys): + raw = bytearray(struct.pack(">H", num_keys)) + for i in range(num_keys): + # zero-length key provider ID and key provider info + raw.extend(b"\x00\x00\x00\x00") + # EDK ciphertext == index + raw.extend(b"\x00\x02") + raw.extend(struct.pack(">H", i)) + return io.BytesIO(raw) diff --git a/test/unit/test_encryption_client.py b/test/unit/test_encryption_client.py index 21139e17c..f463b04f1 100644 --- a/test/unit/test_encryption_client.py +++ b/test/unit/test_encryption_client.py @@ -11,11 +11,14 @@ # ANY KIND, either express or implied. See the License for the specific # language governing permissions and limitations under the License. """Unit test suite for aws_encryption_sdk.EncryptionSDKClient""" +import warnings + import pytest from mock import MagicMock import aws_encryption_sdk from aws_encryption_sdk import CommitmentPolicy +from aws_encryption_sdk.internal.utils.signature import SignaturePolicy from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -24,17 +27,37 @@ def test_init_defaults(): test = aws_encryption_sdk.EncryptionSDKClient() assert test.config.commitment_policy == CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT + assert test.config.max_encrypted_data_keys is None def test_init_success(): - test = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + test = aws_encryption_sdk.EncryptionSDKClient( + commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, + max_encrypted_data_keys=1, + ) assert test.config.commitment_policy == CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT + assert test.config.max_encrypted_data_keys == 1 + + +@pytest.mark.parametrize("max_encrypted_data_keys", (1, 10, 2 ** 16 - 1, 2 ** 16)) +def test_init_valid_max_encrypted_data_keys(max_encrypted_data_keys): + test = aws_encryption_sdk.EncryptionSDKClient(max_encrypted_data_keys=max_encrypted_data_keys) + assert test.config.max_encrypted_data_keys == max_encrypted_data_keys + + +@pytest.mark.parametrize("max_encrypted_data_keys", (0, -1)) +def test_init_invalid_max_encrypted_data_keys(max_encrypted_data_keys): + with pytest.raises(ValueError) as exc_info: + aws_encryption_sdk.EncryptionSDKClient(max_encrypted_data_keys=max_encrypted_data_keys) + exc_info.match("max_encrypted_data_keys cannot be less than 1") def test_client_encrypt(mocker): mocker.patch.object(aws_encryption_sdk, "StreamEncryptor") cmm = MagicMock(__class__=CryptoMaterialsManager) - client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + client = aws_encryption_sdk.EncryptionSDKClient( + commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, max_encrypted_data_keys=3 + ) kwargs = dict() kwargs["source"] = b"plaintext" @@ -42,13 +65,17 @@ def test_client_encrypt(mocker): client.encrypt(**kwargs) expected_kwargs = kwargs.copy() expected_kwargs["commitment_policy"] = CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT + expected_kwargs["signature_policy"] = SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT + expected_kwargs["max_encrypted_data_keys"] = 3 aws_encryption_sdk.StreamEncryptor.assert_called_once_with(**expected_kwargs) def test_client_decrypt(mocker): mocker.patch.object(aws_encryption_sdk, "StreamDecryptor") cmm = MagicMock(__class__=CryptoMaterialsManager) - client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + client = aws_encryption_sdk.EncryptionSDKClient( + commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, max_encrypted_data_keys=3 + ) kwargs = dict() kwargs["source"] = b"ciphertext" @@ -56,6 +83,8 @@ def test_client_decrypt(mocker): client.decrypt(**kwargs) expected_kwargs = kwargs.copy() expected_kwargs["commitment_policy"] = CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT + expected_kwargs["signature_policy"] = SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT + expected_kwargs["max_encrypted_data_keys"] = 3 aws_encryption_sdk.StreamDecryptor.assert_called_once_with(**expected_kwargs) @@ -73,6 +102,8 @@ def test_client_stream_encrypt(mocker, mode_string): expected_kwargs = kwargs.copy() expected_kwargs.pop("mode") expected_kwargs["commitment_policy"] = CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT + expected_kwargs["signature_policy"] = SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT + expected_kwargs["max_encrypted_data_keys"] = None aws_encryption_sdk.StreamEncryptor.assert_called_once_with(**expected_kwargs) @@ -90,4 +121,28 @@ def test_client_stream_decrypt(mocker, mode_string): expected_kwargs = kwargs.copy() expected_kwargs.pop("mode") expected_kwargs["commitment_policy"] = CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT + expected_kwargs["signature_policy"] = SignaturePolicy.ALLOW_ENCRYPT_ALLOW_DECRYPT + expected_kwargs["max_encrypted_data_keys"] = None aws_encryption_sdk.StreamDecryptor.assert_called_once_with(**expected_kwargs) + + +@pytest.mark.parametrize("method", ("encrypt", "decrypt", "stream")) +@pytest.mark.parametrize("key", ("commitment_policy", "max_encrypted_data_keys")) +def test_client_bad_kwargs(mocker, method, key): + mocker.patch.object(aws_encryption_sdk, "StreamEncryptor") + + cmm = MagicMock(__class__=CryptoMaterialsManager) + kwargs = dict() + kwargs[key] = "foobar" + kwargs["source"] = b"ciphertext" + kwargs["materials_manager"] = cmm + client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + client.encrypt(**kwargs) + assert len(w) == 1 + assert issubclass(w[-1].category, UserWarning) + + message = str(w[-1].message) + assert "Invalid keyword argument" in message + assert "Set this value by passing a 'config' to the EncryptionSDKClient constructor instead" in message diff --git a/test/unit/test_streaming_client_configs.py b/test/unit/test_streaming_client_configs.py index db254f38a..426f8f85f 100644 --- a/test/unit/test_streaming_client_configs.py +++ b/test/unit/test_streaming_client_configs.py @@ -74,6 +74,12 @@ def _new_master_key(self, key_id): materials_manager=FakeCryptoMaterialsManager(), commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, ), + dict( + source=b"", + materials_manager=FakeCryptoMaterialsManager(), + commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, + max_encrypted_data_keys=3, + ), ], EncryptorConfig: build_valid_kwargs_list( BASE_KWARGS, dict(encryption_context={}, algorithm=ALGORITHM, frame_length=8192) @@ -83,6 +89,11 @@ def _new_master_key(self, key_id): INVALID_KWARGS = { _ClientConfig: [ dict(source=b"", key_provider=FakeMasterKeyProvider(), source_length=10), + dict( + source=b"", + materials_manager=FakeCryptoMaterialsManager(), + max_encrypted_data_keys=0, + ), ], EncryptorConfig: [ dict(source=b"", materials_manager=FakeCryptoMaterialsManager(), encryption_context=None), @@ -112,6 +123,7 @@ def test_client_config_defaults(): test = _ClientConfig(**BASE_KWARGS) assert test.source_length is None assert test.line_length == LINE_LENGTH + assert test.max_encrypted_data_keys is None def test_encryptor_config_defaults(): diff --git a/test/unit/test_streaming_client_stream_decryptor.py b/test/unit/test_streaming_client_stream_decryptor.py index 7a3ffbfb6..157755094 100644 --- a/test/unit/test_streaming_client_stream_decryptor.py +++ b/test/unit/test_streaming_client_stream_decryptor.py @@ -46,7 +46,7 @@ def apply_fixtures(self): self.mock_header.algorithm = MagicMock( __class__=Algorithm, iv_len=12, is_committing=MagicMock(return_value=False) ) - self.mock_header.encrypted_data_keys = sentinel.encrypted_data_keys + self.mock_header.encrypted_data_keys = set((VALUES["encrypted_data_key_obj"],)) self.mock_header.encryption_context = sentinel.encryption_context self.mock_raw_header = b"some bytes" @@ -178,12 +178,12 @@ def test_read_header(self, mock_derive_datakey, mock_decrypt_materials_request, test_header, test_header_auth = test_decryptor._read_header() - self.mock_deserialize_header.assert_called_once_with(ct_stream) + self.mock_deserialize_header.assert_called_once_with(ct_stream, None) mock_verifier.from_key_bytes.assert_called_once_with( algorithm=self.mock_header.algorithm, key_bytes=sentinel.verification_key ) mock_decrypt_materials_request.assert_called_once_with( - encrypted_data_keys=sentinel.encrypted_data_keys, + encrypted_data_keys=self.mock_header.encrypted_data_keys, algorithm=self.mock_header.algorithm, encryption_context=sentinel.encryption_context, commitment_policy=mock_commitment_policy, @@ -280,7 +280,7 @@ def test_commitment_committing_algorithm_policy_allows_check_passes( test_decryptor.source_stream = self.mock_input_stream test_decryptor._stream_length = len(VALUES["data_128"]) test_decryptor._read_header() - self.mock_deserialize_header.assert_called_once_with(self.mock_input_stream) + self.mock_deserialize_header.assert_called_once_with(self.mock_input_stream, None) @patch("aws_encryption_sdk.streaming_client.Verifier") @patch("aws_encryption_sdk.streaming_client.DecryptionMaterialsRequest") @@ -326,7 +326,7 @@ def test_commitment_uncommitting_algorithm_policy_allows( commitment_policy=policy, ) test_decryptor._read_header() - self.mock_deserialize_header.assert_called_once_with(self.mock_input_stream) + self.mock_deserialize_header.assert_called_once_with(self.mock_input_stream, None) self.mock_compare_digest.assert_not_called() @patch("aws_encryption_sdk.streaming_client.Verifier") diff --git a/test/unit/test_streaming_client_stream_encryptor.py b/test/unit/test_streaming_client_stream_encryptor.py index 3d45c6f70..5bfd0c903 100644 --- a/test/unit/test_streaming_client_stream_encryptor.py +++ b/test/unit/test_streaming_client_stream_encryptor.py @@ -20,11 +20,13 @@ import aws_encryption_sdk.internal.defaults from aws_encryption_sdk.exceptions import ( ActionNotAllowedError, + CustomMaximumValueExceeded, MasterKeyProviderError, NotSupportedError, SerializationError, ) from aws_encryption_sdk.identifiers import Algorithm, CommitmentPolicy, ContentType, SerializationVersion +from aws_encryption_sdk.internal.utils.signature import SignaturePolicy from aws_encryption_sdk.key_providers.base import MasterKey, MasterKeyProvider from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager from aws_encryption_sdk.streaming_client import StreamEncryptor @@ -147,6 +149,7 @@ def apply_fixtures(self): self.mock_serialize_frame_patcher = patch("aws_encryption_sdk.streaming_client.serialize_frame") self.mock_serialize_frame = self.mock_serialize_frame_patcher.start() self.mock_commitment_policy = MagicMock(__class__=CommitmentPolicy) + self.mock_signature_policy = MagicMock(__class__=SignaturePolicy) yield # Run tearDown self.mock_content_type_patcher.stop() @@ -171,6 +174,7 @@ def test_init(self): frame_length=self.mock_frame_length, algorithm=self.mock_algorithm, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) assert test_encryptor.sequence_number == 1 self.mock_content_type.assert_called_once_with(self.mock_frame_length) @@ -185,6 +189,7 @@ def test_init_non_framed_message_too_large(self): algorithm=self.mock_algorithm, source_length=aws_encryption_sdk.internal.defaults.MAX_NON_FRAMED_SIZE + 1, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) excinfo.match("Source too large for non-framed message") @@ -196,6 +201,7 @@ def test_prep_message_no_master_keys(self): frame_length=self.mock_frame_length, source_length=5, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.content_type = ContentType.FRAMED_DATA @@ -214,6 +220,7 @@ def test_prep_message_primary_master_key_not_in_master_keys(self): frame_length=self.mock_frame_length, source_length=5, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.content_type = ContentType.FRAMED_DATA @@ -221,6 +228,47 @@ def test_prep_message_primary_master_key_not_in_master_keys(self): test_encryptor._prep_message() excinfo.match("Primary Master Key not in provided Master Keys") + def test_prep_message_no_max_encrypted_data_keys(self): + test_encryptor = StreamEncryptor( + source=io.BytesIO(self.plaintext), + materials_manager=self.mock_materials_manager, + frame_length=self.mock_frame_length, + source_length=5, + commitment_policy=self.mock_commitment_policy, + ) + self.mock_encryption_materials.encrypted_data_keys.__len__.return_value = 2 ** 16 - 1 + test_encryptor.content_type = ContentType.FRAMED_DATA + test_encryptor._prep_message() + + @pytest.mark.parametrize("num_keys", (2, 3)) + def test_prep_message_within_max_encrypted_data_keys(self, num_keys): + test_encryptor = StreamEncryptor( + source=io.BytesIO(self.plaintext), + materials_manager=self.mock_materials_manager, + frame_length=self.mock_frame_length, + source_length=5, + commitment_policy=self.mock_commitment_policy, + max_encrypted_data_keys=3, + ) + self.mock_encryption_materials.encrypted_data_keys.__len__.return_value = num_keys + test_encryptor.content_type = ContentType.FRAMED_DATA + test_encryptor._prep_message() + + def test_prep_message_over_max_encrypted_data_keys(self): + test_encryptor = StreamEncryptor( + source=io.BytesIO(self.plaintext), + materials_manager=self.mock_materials_manager, + frame_length=self.mock_frame_length, + source_length=5, + commitment_policy=self.mock_commitment_policy, + max_encrypted_data_keys=3, + ) + self.mock_encryption_materials.encrypted_data_keys.__len__.return_value = 4 + test_encryptor.content_type = ContentType.FRAMED_DATA + with pytest.raises(CustomMaximumValueExceeded) as excinfo: + test_encryptor._prep_message() + excinfo.match("Number of encrypted data keys found larger than configured value") + def test_prep_message_algorithm_change(self): self.mock_encryption_materials.algorithm = Algorithm.AES_256_GCM_IV12_TAG16 test_encryptor = StreamEncryptor( @@ -229,6 +277,7 @@ def test_prep_message_algorithm_change(self): algorithm=Algorithm.AES_128_GCM_IV12_TAG16, source_length=128, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) with pytest.raises(ActionNotAllowedError) as excinfo: test_encryptor._prep_message() @@ -257,6 +306,7 @@ def test_prep_message_framed_message( source_length=5, encryption_context=VALUES["encryption_context"], commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.content_type = ContentType.FRAMED_DATA test_encryption_context = {aws_encryption_sdk.internal.defaults.ENCODED_SIGNER_KEY: sentinel.decoded_bytes} @@ -310,6 +360,7 @@ def test_prep_message_non_framed_message(self, mock_write_header, mock_prep_non_ materials_manager=self.mock_materials_manager, frame_length=self.mock_frame_length, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.content_type = ContentType.NO_FRAMING test_encryptor._prep_message() @@ -323,6 +374,7 @@ def test_prep_message_no_signer(self): frame_length=self.mock_frame_length, algorithm=Algorithm.AES_128_GCM_IV12_TAG16, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.content_type = ContentType.FRAMED_DATA test_encryptor._prep_message() @@ -416,6 +468,7 @@ def test_write_header(self): algorithm=aws_encryption_sdk.internal.defaults.ALGORITHM, frame_length=self.mock_frame_length, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = sentinel.signer test_encryptor.content_type = sentinel.content_type @@ -444,6 +497,7 @@ def test_prep_non_framed(self, mock_non_framed_iv): source=io.BytesIO(self.plaintext), materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = sentinel.signer test_encryptor._encryption_materials = self.mock_encryption_materials @@ -481,6 +535,7 @@ def test_read_bytes_to_non_framed_body(self): source=pt_stream, materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = MagicMock() test_encryptor.encryptor = MagicMock() @@ -500,6 +555,7 @@ def test_read_bytes_to_non_framed_body_too_large(self): source=pt_stream, materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.bytes_read = aws_encryption_sdk.internal.defaults.MAX_NON_FRAMED_SIZE test_encryptor._StreamEncryptor__unframed_plaintext_cache = pt_stream @@ -513,6 +569,7 @@ def test_read_bytes_to_non_framed_body_close(self): source=io.BytesIO(self.plaintext), materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = MagicMock() test_encryptor._encryption_materials = self.mock_encryption_materials @@ -540,6 +597,7 @@ def test_read_bytes_to_non_framed_body_no_signer(self): materials_manager=self.mock_materials_manager, algorithm=Algorithm.AES_128_GCM_IV12_TAG16, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor._header = MagicMock() test_encryptor.signer = None @@ -561,6 +619,7 @@ def test_read_bytes_less_than_buffer(self, mock_read_non_framed, mock_read_frame source=pt_stream, materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.output_buffer = b"1234567" test_encryptor._read_bytes(5) @@ -575,6 +634,7 @@ def test_read_bytes_completed(self, mock_read_non_framed, mock_read_framed): source=pt_stream, materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor._StreamEncryptor__message_complete = True test_encryptor._read_bytes(5) @@ -589,6 +649,7 @@ def test_read_bytes_framed(self, mock_read_non_framed, mock_read_framed): source=pt_stream, materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.content_type = ContentType.FRAMED_DATA test_encryptor._read_bytes(5) @@ -603,6 +664,7 @@ def test_read_bytes_non_framed(self, mock_read_non_framed, mock_read_framed): source=pt_stream, materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.content_type = ContentType.NO_FRAMING test_encryptor._read_bytes(5) @@ -617,6 +679,7 @@ def test_read_bytes_unsupported_type(self, mock_read_non_framed, mock_read_frame source=pt_stream, materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor._encryption_materials = self.mock_encryption_materials test_encryptor._header = MagicMock() @@ -635,6 +698,7 @@ def test_read_bytes_to_framed_body_single_frame_read(self): materials_manager=self.mock_materials_manager, frame_length=128, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = sentinel.signer test_encryptor._encryption_materials = self.mock_encryption_materials @@ -665,6 +729,7 @@ def test_read_bytes_to_framed_body_single_frame_with_final(self): materials_manager=self.mock_materials_manager, frame_length=50, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = sentinel.signer test_encryptor._encryption_materials = self.mock_encryption_materials @@ -716,6 +781,7 @@ def test_read_bytes_to_framed_body_multi_frame_read(self): materials_manager=self.mock_materials_manager, frame_length=frame_length, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = sentinel.signer test_encryptor._encryption_materials = self.mock_encryption_materials @@ -791,6 +857,7 @@ def test_read_bytes_to_framed_body_close(self): materials_manager=self.mock_materials_manager, frame_length=len(self.plaintext), commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = sentinel.signer test_encryptor._encryption_materials = self.mock_encryption_materials @@ -810,6 +877,7 @@ def test_read_bytes_to_framed_body_close_no_signer(self): frame_length=len(self.plaintext), algorithm=Algorithm.AES_128_GCM_IV12_TAG16, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor.signer = None test_encryptor._encryption_materials = self.mock_encryption_materials @@ -829,6 +897,7 @@ def test_close(self, mock_close): source=pt_stream, materials_manager=self.mock_materials_manager, commitment_policy=self.mock_commitment_policy, + signature_policy=self.mock_signature_policy, ) test_encryptor._derived_data_key = sentinel.derived_data_key diff --git a/test_vector_handlers/requirements.txt b/test_vector_handlers/requirements.txt index b89dccb69..a913110a0 100644 --- a/test_vector_handlers/requirements.txt +++ b/test_vector_handlers/requirements.txt @@ -1,4 +1,4 @@ attrs >= 17.4.0 -aws-encryption-sdk>=2.0.0 +aws-encryption-sdk>=2.2.0 pytest>=3.3.1 six diff --git a/test_vector_handlers/src/awses_test_vectors/commands/full_message_decrypt_generate.py b/test_vector_handlers/src/awses_test_vectors/commands/full_message_decrypt_generate.py index a2a122cda..5d8b94893 100644 --- a/test_vector_handlers/src/awses_test_vectors/commands/full_message_decrypt_generate.py +++ b/test_vector_handlers/src/awses_test_vectors/commands/full_message_decrypt_generate.py @@ -25,9 +25,7 @@ def cli(args=None): # type: (Optional[Iterable[str]]) -> None """CLI entry point for generating AWS Encryption SDK Decrypt Message manifests.""" - parser = argparse.ArgumentParser( - description="Build a decrypt manifest from keys and decrypt generation manifests" - ) + parser = argparse.ArgumentParser(description="Build a decrypt manifest from keys and decrypt generation manifests") parser.add_argument("--output", required=True, help="Directory in which to store results") parser.add_argument( "--input", required=True, type=argparse.FileType("r"), help="Existing full message decrypt generation manifest" diff --git a/test_vector_handlers/src/awses_test_vectors/commands/full_message_encrypt.py b/test_vector_handlers/src/awses_test_vectors/commands/full_message_encrypt.py index 971b992ec..2b8b92f3c 100644 --- a/test_vector_handlers/src/awses_test_vectors/commands/full_message_encrypt.py +++ b/test_vector_handlers/src/awses_test_vectors/commands/full_message_encrypt.py @@ -25,9 +25,7 @@ def cli(args=None): # type: (Optional[Iterable[str]]) -> None """CLI entry point for processing AWS Encryption SDK Encrypt Message manifests.""" - parser = argparse.ArgumentParser( - description="Build ciphertexts from keys and encrypt manifests" - ) + parser = argparse.ArgumentParser(description="Build ciphertexts from keys and encrypt manifests") parser.add_argument( "--input", required=True, type=argparse.FileType("r"), help="Existing full message encrypt manifest" ) diff --git a/test_vector_handlers/src/awses_test_vectors/manifests/full_message/decrypt.py b/test_vector_handlers/src/awses_test_vectors/manifests/full_message/decrypt.py index c56bbd6c3..8e4cc9588 100644 --- a/test_vector_handlers/src/awses_test_vectors/manifests/full_message/decrypt.py +++ b/test_vector_handlers/src/awses_test_vectors/manifests/full_message/decrypt.py @@ -17,6 +17,7 @@ """ import json import os +from enum import Enum import attr import aws_encryption_sdk @@ -170,6 +171,12 @@ def result_spec(self): return {self.matcher_tag: self.matcher.matcher_spec} +class DecryptionMethod(Enum): + """Enumeration of decryption methods.""" + + UNSIGNED_ONLY_STREAM = "streaming-unsigned-only" + + @attr.s(init=False) class MessageDecryptionTestScenario(object): # pylint: disable=too-many-arguments @@ -179,6 +186,7 @@ class MessageDecryptionTestScenario(object): :param str ciphertext_uri: URI locating ciphertext data :param bytes ciphertext: Binary ciphertext data + :param boolean must_fail: Whether decryption is expected to fail :param master_key_specs: Iterable of master key specifications :type master_key_specs: iterable of :class:`MasterKeySpec` :param MasterKeyProvider master_key_provider: @@ -192,6 +200,9 @@ class MessageDecryptionTestScenario(object): master_key_specs = attr.ib(validator=iterable_validator(list, MasterKeySpec)) master_key_provider = attr.ib(validator=attr.validators.instance_of(MasterKeyProvider)) result = attr.ib(validator=attr.validators.instance_of(MessageDecryptionTestResult)) + decryption_method = attr.ib( + default=None, validator=attr.validators.optional(attr.validators.instance_of(DecryptionMethod)) + ) description = attr.ib( default=None, validator=attr.validators.optional(attr.validators.instance_of(six.string_types)) ) @@ -203,6 +214,7 @@ def __init__( result, # type: MessageDecryptionTestResult master_key_specs, # type: Iterable[MasterKeySpec] master_key_provider, # type: MasterKeyProvider + decryption_method=None, # type: Optional[DecryptionMethod] description=None, # type: Optional[str] ): # noqa=D107 # type: (...) -> None @@ -215,6 +227,7 @@ def __init__( self.result = result self.master_key_specs = master_key_specs self.master_key_provider = master_key_provider + self.decryption_method = decryption_method self.description = description attr.validate(self) @@ -239,6 +252,8 @@ def from_scenario( raw_master_key_specs = scenario["master-keys"] # type: Iterable[MASTER_KEY_SPEC] master_key_specs = [MasterKeySpec.from_scenario(spec) for spec in raw_master_key_specs] master_key_provider = master_key_provider_from_master_key_specs(keys, master_key_specs) + decryption_method_spec = scenario.get("decryption-method") + decryption_method = DecryptionMethod(decryption_method_spec) if decryption_method_spec else None result_spec = scenario["result"] result = MessageDecryptionTestResult.from_result_spec(result_spec, plaintext_reader) @@ -248,6 +263,7 @@ def from_scenario( master_key_specs=master_key_specs, master_key_provider=master_key_provider, result=result, + decryption_method=decryption_method, description=scenario.get("description"), ) @@ -264,6 +280,8 @@ def scenario_spec(self): "master-keys": [spec.scenario_spec for spec in self.master_key_specs], "result": self.result.result_spec, } + if self.decryption_method is not None: + spec["decryption-method"] = self.decryption_method.value if self.description is not None: spec["description"] = self.description return spec @@ -295,8 +313,11 @@ def run(self, name): :param str name: Descriptive name for this scenario to use in any logging or errors """ - self.result.matcher.match(name, self._one_shot_decrypt) - self.result.matcher.match(name, self._streaming_decrypt) + if self.decryption_method == DecryptionMethod.UNSIGNED_ONLY_STREAM: + self.result.matcher.match(name, self._streaming_decrypt_unsigned) + else: + self.result.matcher.match(name, self._one_shot_decrypt) + self.result.matcher.match(name, self._streaming_decrypt) @attr.s(init=False) @@ -340,7 +361,6 @@ def __init__( # Workaround pending resolution of attrs/mypy interaction. # https://github.com/python/mypy/issues/2088 # https://github.com/python-attrs/attrs/issues/215 - """Initialize MessageDecryptionManifest.""" # noqa pylint: disable=W0105 self.keys_uri = keys_uri self.keys = keys self.test_scenarios = test_scenarios diff --git a/test_vector_handlers/src/awses_test_vectors/manifests/full_message/decrypt_generation.py b/test_vector_handlers/src/awses_test_vectors/manifests/full_message/decrypt_generation.py index e67e3ee81..ebe76824c 100644 --- a/test_vector_handlers/src/awses_test_vectors/manifests/full_message/decrypt_generation.py +++ b/test_vector_handlers/src/awses_test_vectors/manifests/full_message/decrypt_generation.py @@ -18,6 +18,7 @@ import json import os import uuid +from copy import copy import attr import six @@ -35,14 +36,20 @@ validate_manifest_type, ) from awses_test_vectors.manifests.full_message.decrypt import ( + DecryptionMethod, MessageDecryptionManifest, MessageDecryptionTestResult, MessageDecryptionTestScenario, ) from awses_test_vectors.manifests.full_message.encrypt import MessageEncryptionTestScenario from awses_test_vectors.manifests.keys import KeysManifest -from awses_test_vectors.manifests.master_key import MasterKeySpec, master_key_provider_from_master_key_specs +try: + from aws_encryption_sdk.identifiers import AlgorithmSuite +except ImportError: + from aws_encryption_sdk.identifiers import Algorithm as AlgorithmSuite + +from awses_test_vectors.manifests.master_key import MasterKeySpec, master_key_provider_from_master_key_specs try: # Python 3.5.0 and 3.5.1 have incompatible typing modules from typing import IO, Callable, Dict, Iterable, Optional # noqa pylint: disable=unused-import @@ -55,7 +62,7 @@ # We only actually need these imports when running the mypy checks pass -SUPPORTED_VERSIONS = (1,) +SUPPORTED_VERSIONS = (2,) class TamperingMethod: @@ -66,6 +73,12 @@ def from_tampering_spec(cls, spec): """Load from a tampering specification""" if spec is None: return TamperingMethod() + if spec == "truncate": + return TruncateTamperingMethod() + if spec == "mutate": + return MutateTamperingMethod() + if spec == "half-sign": + return HalfSigningTamperingMethod() ((tampering_tag, tampering_values_spec),) = spec.items() if tampering_tag == "change-edk-provider-info": return ChangeEDKProviderInfoTamperingMethod.from_values_spec(tampering_values_spec) @@ -87,9 +100,7 @@ def run_scenario_with_tampering(self, ciphertext_writer, generation_scenario, pl plaintext_uri=plaintext_uri, plaintext=generation_scenario.encryption_scenario.plaintext ) return [ - generation_scenario.decryption_test_scenario_pair( - ciphertext_writer, ciphertext_to_decrypt, expected_result - ) + generation_scenario.decryption_test_scenario_pair(ciphertext_writer, ciphertext_to_decrypt, expected_result) ] @@ -167,6 +178,127 @@ def decrypt_materials(self, request): return self.wrapped_default_cmm.decrypt_materials(request) +BITS_PER_BYTE = 8 + + +class TruncateTamperingMethod(TamperingMethod): + """Tampering method that truncates a good message at every byte (except zero).""" + + # pylint: disable=R0201 + def run_scenario_with_tampering(self, ciphertext_writer, generation_scenario, _plaintext_uri): + """ + Run a given scenario, tampering with the input or the result. + + return: a list of (ciphertext, result) pairs. + """ + ciphertext_to_decrypt = generation_scenario.encryption_scenario.run() + return [ + generation_scenario.decryption_test_scenario_pair( + ciphertext_writer, + TruncateTamperingMethod.flip_bit(ciphertext_to_decrypt, bit), + MessageDecryptionTestResult.expect_error("Bit {} flipped".format(bit)), + ) + for bit in range(0, len(ciphertext_to_decrypt) * BITS_PER_BYTE) + ] + + @classmethod + def flip_bit(cls, ciphertext, bit): + """Flip only the given bit in the given ciphertext""" + byte_index, bit_index = divmod(bit, BITS_PER_BYTE) + result = bytearray(ciphertext) + result[byte_index] ^= 1 << (BITS_PER_BYTE - bit_index - 1) + return bytes(result) + + +class MutateTamperingMethod(TamperingMethod): + """Tampering method that produces a message with a single bit flipped, for every possible bit.""" + + # pylint: disable=R0201 + def run_scenario_with_tampering(self, ciphertext_writer, generation_scenario, _plaintext_uri): + """ + Run a given scenario, tampering with the input or the result. + + return: a list of (ciphertext, result) pairs. + """ + ciphertext_to_decrypt = generation_scenario.encryption_scenario.run() + return [ + generation_scenario.decryption_test_scenario_pair( + ciphertext_writer, + ciphertext_to_decrypt[0:length], + MessageDecryptionTestResult.expect_error("Truncated at byte {}".format(length)), + ) + for length in range(1, len(ciphertext_to_decrypt)) + ] + + +class HalfSigningTamperingMethod(TamperingMethod): + """Tampering method that changes the provider info on all EDKs.""" + + # pylint: disable=R0201 + def run_scenario_with_tampering(self, ciphertext_writer, generation_scenario, _plaintext_uri): + """ + Run a given scenario, tampering with the input or the result. + + return: a list of (ciphertext, result) pairs. + """ + tampering_materials_manager = HalfSigningCryptoMaterialsManager( + generation_scenario.encryption_scenario.master_key_provider + ) + ciphertext_to_decrypt = generation_scenario.encryption_scenario.run(tampering_materials_manager) + expected_result = MessageDecryptionTestResult.expect_error( + "Unsigned message using a data key with a public key" + ) + return [ + generation_scenario.decryption_test_scenario_pair(ciphertext_writer, ciphertext_to_decrypt, expected_result) + ] + + +class HalfSigningCryptoMaterialsManager(CryptoMaterialsManager): + """ + Custom CMM that generates materials for an unsigned algorithm suite + that includes the "aws-crypto-public-key" encryption context. + + THIS IS ONLY USED TO CREATE INVALID MESSAGES and should never be used in + production! It is imitating what a malicious decryptor without encryption + permissions might do, to attempt to forge an unsigned message from a decrypted + signed message, and therefore this is an important case for ESDKs to reject. + """ + + wrapped_default_cmm = attr.ib(validator=attr.validators.instance_of(CryptoMaterialsManager)) + + def __init__(self, master_key_provider): + """ + Create a new CMM that wraps a new DefaultCryptoMaterialsManager + based on the given master key provider. + """ + self.wrapped_default_cmm = DefaultCryptoMaterialsManager(master_key_provider) + + def get_encryption_materials(self, request): + """ + Generate half-signing materials by requesting signing materials + from the wrapped default CMM, and then changing the algorithm suite + and removing the signing key from teh result. + """ + if request.algorithm == AlgorithmSuite.AES_256_GCM_HKDF_SHA512_COMMIT_KEY: + signing_request = copy(request) + signing_request.algorithm = AlgorithmSuite.AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384 + + result = self.wrapped_default_cmm.get_encryption_materials(signing_request) + result.algorithm = request.algorithm + result.signing_key = None + + return result + + raise NotImplementedError( + "The half-sign tampering method is only supported on the " + "AES_256_GCM_HKDF_SHA512_COMMIT_KEY algorithm suite." + ) + + def decrypt_materials(self, request): + """Thunks to the wrapped default CMM""" + return self.wrapped_default_cmm.decrypt_materials(request) + + @attr.s class MessageDecryptionTestScenarioGenerator(object): # pylint: disable=too-many-instance-attributes @@ -177,6 +309,7 @@ class MessageDecryptionTestScenarioGenerator(object): :param MessageEncryptionTestScenario encryption_scenario: Encryption parameters :param tampering_method: Optional method used to tamper with the ciphertext :type tampering_method: :class:`TamperingMethod` + :param decryption_method: :param decryption_master_key_specs: Iterable of master key specifications :type decryption_master_key_specs: iterable of :class:`MasterKeySpec` :param MasterKeyProvider decryption_master_key_provider: @@ -185,6 +318,7 @@ class MessageDecryptionTestScenarioGenerator(object): encryption_scenario = attr.ib(validator=attr.validators.instance_of(MessageEncryptionTestScenario)) tampering_method = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(TamperingMethod))) + decryption_method = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(DecryptionMethod))) decryption_master_key_specs = attr.ib(validator=iterable_validator(list, MasterKeySpec)) decryption_master_key_provider = attr.ib(validator=attr.validators.instance_of(MasterKeyProvider)) result = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(MessageDecryptionTestResult))) @@ -203,6 +337,8 @@ def from_scenario(cls, scenario, keys, plaintexts): encryption_scenario = MessageEncryptionTestScenario.from_scenario(encryption_scenario_spec, keys, plaintexts) tampering = scenario.get("tampering") tampering_method = TamperingMethod.from_tampering_spec(tampering) + decryption_method_spec = scenario.get("decryption-method") + decryption_method = DecryptionMethod(decryption_method_spec) if decryption_method_spec else None if "decryption-master-keys" in scenario: decryption_master_key_specs = [ MasterKeySpec.from_scenario(spec) for spec in scenario["decryption-master-keys"] @@ -219,6 +355,7 @@ def from_scenario(cls, scenario, keys, plaintexts): return cls( encryption_scenario=encryption_scenario, tampering_method=tampering_method, + decryption_method=decryption_method, decryption_master_key_specs=decryption_master_key_specs, decryption_master_key_provider=decryption_master_key_provider, result=result, @@ -236,9 +373,7 @@ def run(self, ciphertext_writer, plaintext_uri): """ return dict(self.tampering_method.run_scenario_with_tampering(ciphertext_writer, self, plaintext_uri)) - def decryption_test_scenario_pair( - self, ciphertext_writer, ciphertext_to_decrypt, expected_result - ): + def decryption_test_scenario_pair(self, ciphertext_writer, ciphertext_to_decrypt, expected_result): """Create a new (name, decryption scenario) pair""" ciphertext_name = str(uuid.uuid4()) ciphertext_uri = ciphertext_writer(ciphertext_name, ciphertext_to_decrypt) @@ -250,6 +385,7 @@ def decryption_test_scenario_pair( ciphertext=ciphertext_to_decrypt, master_key_specs=self.decryption_master_key_specs, master_key_provider=self.decryption_master_key_provider, + decryption_method=self.decryption_method, result=expected_result, ), ) diff --git a/test_vector_handlers/src/awses_test_vectors/manifests/keys.py b/test_vector_handlers/src/awses_test_vectors/manifests/keys.py index 3b0c51799..783ae9da6 100644 --- a/test_vector_handlers/src/awses_test_vectors/manifests/keys.py +++ b/test_vector_handlers/src/awses_test_vectors/manifests/keys.py @@ -146,7 +146,6 @@ def __init__( # Workaround pending resolution of attrs/mypy interaction. # https://github.com/python/mypy/issues/2088 # https://github.com/python-attrs/attrs/issues/215 - """Initialize ManualKeySpec.""" # noqa pylint: disable=W0105 self.algorithm = algorithm self.type_name = type_name self.bits = bits diff --git a/test_vector_handlers/test/aws-crypto-tools-test-vector-framework b/test_vector_handlers/test/aws-crypto-tools-test-vector-framework index 8169eb057..753b83496 160000 --- a/test_vector_handlers/test/aws-crypto-tools-test-vector-framework +++ b/test_vector_handlers/test/aws-crypto-tools-test-vector-framework @@ -1 +1 @@ -Subproject commit 8169eb057370d18ceb3abb3062213b9a8d55289c +Subproject commit 753b83496aabfe34b8a179d60a62f05975d396bf diff --git a/test_vector_handlers/test/integration/integration_test_utils.py b/test_vector_handlers/test/integration/integration_test_utils.py index e024dcb54..fbe6cf7b7 100644 --- a/test_vector_handlers/test/integration/integration_test_utils.py +++ b/test_vector_handlers/test/integration/integration_test_utils.py @@ -33,5 +33,5 @@ def full_message_encrypt_vectors(): @pytest.fixture def full_message_decrypt_generation_vectors(): return os.path.join( - vectors_dir(), "features", "CANONICAL-GENERATED-MANIFESTS", "0006-awses-message-decryption-generation.v1.json" + vectors_dir(), "features", "CANONICAL-GENERATED-MANIFESTS", "0006-awses-message-decryption-generation.v2.json" ) diff --git a/test_vector_handlers/tox.ini b/test_vector_handlers/tox.ini index 16c8cf779..8aebe09da 100644 --- a/test_vector_handlers/tox.ini +++ b/test_vector_handlers/tox.ini @@ -1,6 +1,8 @@ [tox] envlist = - py{27,34,35,36,37}-awses_{1.7.1,2.0.0,latest}, + # The test vectors depend on new features now, + # so until release we can only effectively test the local version of the ESDK. + py{27,34,35,36,37}-awses_local, # 1.2.0 and 1.2.max are being difficult because of attrs bandit, doc8, readme, docs, {flake8,pylint}{,-tests}, @@ -48,17 +50,14 @@ passenv = sitepackages = False deps = -rtest/requirements.txt - awses_1.3.3: -rcompatibility-requirements/1.3.3 - awses_1.3.max: -rcompatibility-requirements/1.3.max - awses_1.7.1: -rcompatibility-requirements/1.7.1 - awses_2.0.0: -rcompatibility-requirements/2.0.0 - awses_latest: -rcompatibility-requirements/latest -commands = {[testenv:base-command]commands} + .. +commands = + {[testenv:base-command]commands} [testenv:full-encrypt] basepython = python3 sitepackages = False -deps = aws-encryption-sdk +deps = .. commands = awses-full-message-encrypt {posargs} [testenv:full-decrypt-generate] @@ -119,6 +118,7 @@ commands = [testenv:flake8] basepython = python3 deps = + .. flake8 flake8-docstrings # https://github.com/JBKahn/flake8-print/pull/30 @@ -133,6 +133,7 @@ commands = [testenv:flake8-tests] basepython = {[testenv:flake8]basepython} deps = + .. flake8 commands = flake8 \ @@ -147,6 +148,7 @@ commands = basepython = python3 deps = -rtest/requirements.txt + .. pyflakes pylint commands = @@ -199,7 +201,9 @@ commands = seed-isort-config [testenv:isort] basepython = python3 -deps = isort +deps = + isort + .. commands = isort -rc \ src \ test \ @@ -217,6 +221,7 @@ basepython = python3 deps = {[testenv:blacken]deps} {[testenv:isort]deps} + .. commands = {[testenv:blacken]commands} {[testenv:isort]commands} @@ -230,12 +235,15 @@ commands = doc8 doc/index.rst README.rst CHANGELOG.rst [testenv:readme] basepython = python3 -deps = readme_renderer +deps = + .. + readme_renderer commands = python setup.py check -r -s [testenv:bandit] basepython = python3 deps = + .. bandit>=1.5.1 commands = bandit -r src/awses_test_vectors/