diff --git a/.github/workflows/ci_tests.yaml b/.github/workflows/ci_tests.yaml index 3e035d9dd..a58bdb332 100644 --- a/.github/workflows/ci_tests.yaml +++ b/.github/workflows/ci_tests.yaml @@ -12,6 +12,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: | + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: | + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 jobs: tests: diff --git a/.gitmodules b/.gitmodules index 870af609a..766c4e198 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [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/private-aws-crypto-tools-test-vector-framework-staging.git +[submodule "aws-encryption-sdk-specification"] + path = aws-encryption-sdk-specification + url = https://github.com/awslabs/private-aws-encryption-sdk-specification-staging.git +[submodule "test_vector_handlers/test/aws-encryption-sdk-test-vectors"] + path = test_vector_handlers/test/aws-encryption-sdk-test-vectors + url = https://github.com/awslabs/private-aws-encryption-sdk-test-vectors-staging.git diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6fe2f108e..af92211cc 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,24 @@ Changelog ********* +2.3.0 -- 2021-06-16 +=================== + +Features +-------- +* AWS KMS multi-Region Key support + + Added new the master key MRKAwareKMSMasterKey + and the new master key providers MRKAwareStrictAwsKmsMasterKeyProvider + and MRKAwareDiscoveryAwsKmsMasterKeyProvider + that support AWS KMS multi-Region Keys. + + See https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html + for more details about AWS KMS multi-Region Keys. + See https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/configure.html#config-mrks + for more details about how the AWS Encryption SDK interoperates + with AWS KMS multi-Region keys. + 2.2.0 -- 2021-05-27 =================== diff --git a/aws-encryption-sdk-specification b/aws-encryption-sdk-specification new file mode 160000 index 000000000..af5b3e1b2 --- /dev/null +++ b/aws-encryption-sdk-specification @@ -0,0 +1 @@ +Subproject commit af5b3e1b25f932daeeedbbdd168edc9089eabca0 diff --git a/buildspec.yml b/buildspec.yml index 639d56321..c1ccff093 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -46,3 +46,6 @@ batch: - identifier: code_coverage buildspec: codebuild/coverage/coverage.yml + + - identifier: compliance + buildspec: codebuild/compliance/compliance.yml diff --git a/codebuild/compliance/compliance.yml b/codebuild/compliance/compliance.yml new file mode 100644 index 000000000..606a7006a --- /dev/null +++ b/codebuild/compliance/compliance.yml @@ -0,0 +1,9 @@ +version: 0.2 + +phases: + install: + runtime-versions: + nodejs: latest + build: + commands: + - aws-encryption-sdk-specification/util/test_conditions -s 'src/**/**/*.py' -s 'compliance_exceptions/*.py' -t 'test/**/*.py' diff --git a/codebuild/py27/awses_local.yml b/codebuild/py27/awses_local.yml index 2f84a43ab..4087f588a 100644 --- a/codebuild/py27/awses_local.yml +++ b/codebuild/py27/awses_local.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" diff --git a/codebuild/py27/examples.yml b/codebuild/py27/examples.yml index 19091ebdb..50570869d 100644 --- a/codebuild/py27/examples.yml +++ b/codebuild/py27/examples.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py27/integ.yml b/codebuild/py27/integ.yml index 497226f01..82d264aa0 100644 --- a/codebuild/py27/integ.yml +++ b/codebuild/py27/integ.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py35/awses_local.yml b/codebuild/py35/awses_local.yml index 127e329f9..71583417a 100644 --- a/codebuild/py35/awses_local.yml +++ b/codebuild/py35/awses_local.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" diff --git a/codebuild/py35/examples.yml b/codebuild/py35/examples.yml index b700465ad..109f2764c 100644 --- a/codebuild/py35/examples.yml +++ b/codebuild/py35/examples.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py35/integ.yml b/codebuild/py35/integ.yml index b7e9ba2d7..0014378ba 100644 --- a/codebuild/py35/integ.yml +++ b/codebuild/py35/integ.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py36/awses_local.yml b/codebuild/py36/awses_local.yml index 023dbd00d..dba6a28ba 100644 --- a/codebuild/py36/awses_local.yml +++ b/codebuild/py36/awses_local.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" diff --git a/codebuild/py36/examples.yml b/codebuild/py36/examples.yml index efd098578..1bd091d11 100644 --- a/codebuild/py36/examples.yml +++ b/codebuild/py36/examples.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py36/integ.yml b/codebuild/py36/integ.yml index 021741dbe..35124c835 100644 --- a/codebuild/py36/integ.yml +++ b/codebuild/py36/integ.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py37/awses_local.yml b/codebuild/py37/awses_local.yml index 29ce46381..958823841 100644 --- a/codebuild/py37/awses_local.yml +++ b/codebuild/py37/awses_local.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" diff --git a/codebuild/py37/examples.yml b/codebuild/py37/examples.yml index a43ac5b84..cbe251cf0 100644 --- a/codebuild/py37/examples.yml +++ b/codebuild/py37/examples.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py37/integ.yml b/codebuild/py37/integ.yml index 7f886c213..27ce515d0 100644 --- a/codebuild/py37/integ.yml +++ b/codebuild/py37/integ.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py38/awses_local.yml b/codebuild/py38/awses_local.yml index 7e5cdef40..fe84d714d 100644 --- a/codebuild/py38/awses_local.yml +++ b/codebuild/py38/awses_local.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" diff --git a/codebuild/py38/examples.yml b/codebuild/py38/examples.yml index 7033cb3a3..180c606f4 100644 --- a/codebuild/py38/examples.yml +++ b/codebuild/py38/examples.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py38/integ.yml b/codebuild/py38/integ.yml index 7ab243334..6af55e839 100644 --- a/codebuild/py38/integ.yml +++ b/codebuild/py38/integ.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py39/awses_1.7.1.yml b/codebuild/py39/awses_1.7.1.yml index 2ab614cfb..4da84dfa5 100644 --- a/codebuild/py39/awses_1.7.1.yml +++ b/codebuild/py39/awses_1.7.1.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" diff --git a/codebuild/py39/awses_2.0.0.yml b/codebuild/py39/awses_2.0.0.yml index ed4f0e37b..cf8f90b36 100644 --- a/codebuild/py39/awses_2.0.0.yml +++ b/codebuild/py39/awses_2.0.0.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" diff --git a/codebuild/py39/awses_latest.yml b/codebuild/py39/awses_latest.yml index 21b37c2bd..9a2b12190 100644 --- a/codebuild/py39/awses_latest.yml +++ b/codebuild/py39/awses_latest.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_API_DEPLOYMENT_ID: "xi1mwx3ttb" AWS_ENCRYPTION_SDK_PYTHON_DECRYPT_ORACLE_REGION: "us-west-2" diff --git a/codebuild/py39/examples.yml b/codebuild/py39/examples.yml index 892cdaa63..38b1f01c7 100644 --- a/codebuild/py39/examples.yml +++ b/codebuild/py39/examples.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/codebuild/py39/integ.yml b/codebuild/py39/integ.yml index c652c7b25..49e8f6d1c 100644 --- a/codebuild/py39/integ.yml +++ b/codebuild/py39/integ.yml @@ -7,6 +7,10 @@ env: 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_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1: >- + arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2: >- + arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7 phases: install: diff --git a/compliance_exceptions/aws-kms-mrk-aware-master-key-provider.py b/compliance_exceptions/aws-kms-mrk-aware-master-key-provider.py new file mode 100644 index 000000000..6d1a71201 --- /dev/null +++ b/compliance_exceptions/aws-kms-mrk-aware-master-key-provider.py @@ -0,0 +1,79 @@ +# Due to how Python MasterKeys and MasterKeyProviders are set up, +# there are some parts of the Java-focused spec which are non-applicable + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +# //= type=exception +# //# The regional client +# //# supplier MUST be defined in discovery mode. +# // The Python implementation does not include a client supplier as a configuration option. +# // Instead a list of regions may be passed. If not passed, a default region will be used. +# // This behavior is true even of Discovery MKPs. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +# //= type=exception +# //# The function MUST only provide master keys if the input provider id +# //# equals "aws-kms". +# // Python does not take in provider ID as input to this new_master_key. +# // Each MK determines on it's own whether to process based on provider ID in owns_data_key + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +# //= type=exception +# //# An AWS KMS client +# //# MUST be obtained by calling the regional client supplier with this +# //# AWS Region. +# // Python doesn't use a client-supplier, but _client(new_key_id) will grab a client +# // based on the region in new_key_id, which is always the behavior we want. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +# //= type=exception +# //# The set of encrypted data keys MUST first be filtered to match this +# //# master key's configuration. +# // Each MK is responsible for defining whether an EDK matches it's configuration in +# // as part of _decrypt_data_key. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +# //= type=exception +# //# In strict mode, the requested AWS KMS key ARN MUST match a member of the configured key ids by using AWS +# //# KMS MRK Match for Decrypt (aws-kms-mrk-match-for-decrypt.md#implementation) otherwise this function MUST error. +# // Python isn't concerned with ensuring the configured key ids match during new_master_key, given that +# // Python doesn't filter EDKs before creating the master keys for decryption. Each MK is responsible for raising +# // an error if the EDK isn't an MRK aware match. For encryption, the keys are pre-populated based on the configured +# // keys, which again makes any check non-applicable. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 +# //= type=exception +# //# On initialization the caller MUST provide: +# // Strict and discovery modes and their corresponding inputs are split +# // into two different classes. Additionally, +# // Python does not take in a regional client supplier, +# // but instead takes in a list of regions to create clients out of. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 +# //= type=exception +# //# Finally if the +# //# provider info is identified as a multi-Region key (aws-kms-key- +# //# arn.md#identifying-an-aws-kms-multi-region-key) the AWS Region MUST +# //# be the region from the AWS KMS key in the configured key ids matched +# //# to the requested AWS KMS key by using AWS KMS MRK Match for Decrypt +# //# (aws-kms-mrk-match-for-decrypt.md#implementation). +# // This is not relevant due to the fact that Strict MRK Aware MKPs will create an MK for +# // each configured key ID on initialization, each with +# // a client that matches the region in the configured key ID. +# // During decryption, the region from the EDK's provider info does +# // not figure into what client region to use. +# // The MKs the MKP vends should always have a client region that matches the key ID + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +# //= type=exception +# //# If this attempt results in an error, then +# //# these errors MUST be collected. +# // Python logs errors instead of collecting them. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 +# //= type=exception +# //# Additionally +# //# each provider info MUST be a valid AWS KMS ARN (aws-kms-key-arn.md#a- +# //# valid-aws-kms-arn) with a resource type of "key". +# // Python MKPs do not filter before using each MK to decrypt. Each MK is +# // Individually responsible for throwing if it shouldn't be used for decrypt. + diff --git a/compliance_exceptions/aws-kms-mrk-aware-master-key.py b/compliance_exceptions/aws-kms-mrk-aware-master-key.py new file mode 100644 index 000000000..812fe70ea --- /dev/null +++ b/compliance_exceptions/aws-kms-mrk-aware-master-key.py @@ -0,0 +1,26 @@ +# Due to how Python MasterKeys and MasterKeyProviders are set up, +# there are some parts of the Java-focused spec which are non-applicable + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +# //= type=exception +# //# For each encrypted data key in the filtered set, one at a time, the +# //# master key MUST attempt to decrypt the data key. +# // Python MKs only ever attempt one EDK during one Decrypt Data Key call + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 +# //= type=exception +# //# This configuration SHOULD be on initialization and SHOULD be immutable. +# // Python does not provide a good way of making fields immutable + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +# //= type=exception +# //# If this attempt results in an error, then these errors MUST be collected. +# // Python logs errors instead of collecting them. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 +# //= type=exception +# //# The set of encrypted data keys MUST first be filtered to match this +# //# master key's configuration. +# // Python MKs only ever deal with one EDK at a time. They are responsible +# // for determining whether they should attempt to decrypt with owns_data_key. + diff --git a/compliance_exceptions/aws-kms-mrk-aware-multi-keyrings.py b/compliance_exceptions/aws-kms-mrk-aware-multi-keyrings.py new file mode 100644 index 000000000..3c5a10b19 --- /dev/null +++ b/compliance_exceptions/aws-kms-mrk-aware-multi-keyrings.py @@ -0,0 +1,115 @@ +# The AWS Encryption SDK - Python does not implement Keyrings + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +# //= type=exception +# //# The caller MUST provide: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +# //= type=exception +# //# If an empty set of Region is provided this function MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +# //= type=exception +# //# If +# //# any element of the set of regions is null or an empty string this +# //# function MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +# //= type=exception +# //# If a regional client supplier is not passed, +# //# then a default MUST be created that takes a region string and +# //# generates a default AWS SDK client for the given region. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +# //= type=exception +# //# A set of AWS KMS clients MUST be created by calling regional client +# //# supplier for each region in the input set of regions. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +# //= type=exception +# //# Then a set of AWS KMS MRK Aware Symmetric Region Discovery Keyring +# //# (aws-kms-mrk-aware-symmetric-region-discovery-keyring.md) MUST be +# //# created for each AWS KMS client by initializing each keyring with + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +# //= type=exception +# //# Then a Multi-Keyring (../multi-keyring.md#inputs) MUST be initialize +# //# by using this set of discovery keyrings as the child keyrings +# //# (../multi-keyring.md#child-keyrings). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.5 +# //= type=exception +# //# This Multi-Keyring MUST be +# //# this functions output. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# The caller MUST provide: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# If any of the AWS KMS key identifiers is null or an empty string this +# //# function MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# At least one non-null or non-empty string AWS +# //# KMS key identifiers exists in the input this function MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# If +# //# a regional client supplier is not passed, then a default MUST be +# //# created that takes a region string and generates a default AWS SDK +# //# client for the given region. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# If there is a generator input then the generator keyring MUST be a +# //# AWS KMS MRK Aware Symmetric Keyring (aws-kms-mrk-aware-symmetric- +# //# keyring.md) initialized with + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# * The AWS KMS client that MUST be created by the regional client +# //# supplier when called with the region part of the generator ARN or +# //# a signal for the AWS SDK to select the default region. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# If there is a set of child identifiers then a set of AWS KMS MRK +# //# Aware Symmetric Keyring (aws-kms-mrk-aware-symmetric-keyring.md) MUST +# //# be created for each AWS KMS key identifier by initialized each +# //# keyring with + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# * The AWS KMS client that MUST be created by the regional client +# //# supplier when called with the region part of the AWS KMS key +# //# identifier or a signal for the AWS SDK to select the default +# //# region. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# NOTE: The AWS Encryption SDK SHOULD NOT attempt to evaluate its own +# //# default region. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# Then a Multi-Keyring (../multi-keyring.md#inputs) MUST be initialize +# //# by using this generator keyring as the generator keyring (../multi- +# //# keyring.md#generator-keyring) and this set of child keyrings as the +# //# child keyrings (../multi-keyring.md#child-keyrings). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# This Multi- +# //# Keyring MUST be this functions output. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-multi-keyrings.txt#2.6 +# //= type=exception +# //# All +# //# AWS KMS identifiers are passed to Assert AWS KMS MRK are unique (aws- +# //# kms-mrk-are-unique.md#Implementation) and the function MUST return +# //# success otherwise this MUST fail. + diff --git a/compliance_exceptions/aws-kms-mrk-aware-symmetric-keyring.py b/compliance_exceptions/aws-kms-mrk-aware-symmetric-keyring.py new file mode 100644 index 000000000..7976386b7 --- /dev/null +++ b/compliance_exceptions/aws-kms-mrk-aware-symmetric-keyring.py @@ -0,0 +1,218 @@ +# The AWS Encryption SDK - Python does not implement Keyrings + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.5 +# //= type=exception +# //# MUST implement the AWS Encryption SDK Keyring interface (../keyring- +# //# interface.md#interface) + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 +# //= type=exception +# //# On initialization the caller MUST provide: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 +# //= type=exception +# //# The AWS KMS key identifier MUST NOT be null or empty. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 +# //= type=exception +# //# The AWS KMS +# //# SDK client MUST NOT be null. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# OnEncrypt MUST take encryption materials (structures.md#encryption- +# //# materials) as input. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If the input encryption materials (structures.md#encryption- +# //# materials) do not contain a plaintext data key OnEncrypt MUST attempt +# //# to generate a new plaintext data key by calling AWS KMS +# //# GenerateDataKey (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_GenerateDataKey.html). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If the keyring calls AWS KMS GenerateDataKeys, it MUST use the +# //# configured AWS KMS client to make the call. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# The keyring MUST call +# //# AWS KMS GenerateDataKeys with a request constructed as follows: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If the call to AWS KMS GenerateDataKey +# //# (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_GenerateDataKey.html) does not succeed, OnEncrypt MUST NOT modify +# //# the encryption materials (structures.md#encryption-materials) and +# //# MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If the Generate Data Key call succeeds, OnEncrypt MUST verify that +# //# the response "Plaintext" length matches the specification of the +# //# algorithm suite (algorithm-suites.md)'s Key Derivation Input Length +# //# field. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# The Generate Data Key response's "KeyId" MUST be A valid AWS +# //# KMS key ARN (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region- +# //# key). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If verified, OnEncrypt MUST do the following with the response +# //# from AWS KMS GenerateDataKey +# //# (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_GenerateDataKey.html): + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# * OnEncrypt MUST output the modified encryption materials +# //# (structures.md#encryption-materials) + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# Given a plaintext data key in the encryption materials +# //# (structures.md#encryption-materials), OnEncrypt MUST attempt to +# //# encrypt the plaintext data key using the configured AWS KMS key +# //# identifier. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# The keyring MUST call AWS KMS Encrypt +# //# (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_Encrypt.html) using the configured AWS KMS client. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# The keyring +# //# MUST AWS KMS Encrypt call with a request constructed as follows: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If the call to AWS KMS Encrypt +# //# (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_Encrypt.html) does not succeed, OnEncrypt MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If the Encrypt call succeeds The response's "KeyId" MUST be A valid +# //# AWS KMS key ARN (aws-kms-key-arn.md#identifying-an-aws-kms-multi- +# //# region-key). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If verified, OnEncrypt MUST do the following with the +# //# response from AWS KMS Encrypt +# //# (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_Encrypt.html): + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.7 +# //= type=exception +# //# If all Encrypt calls succeed, OnEncrypt MUST output the modified +# //# encryption materials (structures.md#encryption-materials). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# OnDecrypt MUST take decryption materials (structures.md#decryption- +# //# materials) and a list of encrypted data keys +# //# (structures.md#encrypted-data-key) as input. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# The set of encrypted data keys MUST first be filtered to match this +# //# keyring's configuration. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# * Its provider ID MUST exactly match the value "aws-kms". + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# * The the function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match- +# //# for-decrypt.md#implementation) called with the configured AWS KMS +# //# key identifier and the provider info MUST return "true". + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# For each encrypted data key in the filtered set, one at a time, the +# //# OnDecrypt MUST attempt to decrypt the data key. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# If this attempt +# //# results in an error, then these errors MUST be collected. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# To attempt to decrypt a particular encrypted data key +# //# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS +# //# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_Decrypt.html) with the configured AWS KMS client. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# When calling AWS KMS Decrypt +# //# (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_Decrypt.html), the keyring MUST call with a request constructed +# //# as follows: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# * The "KeyId" field in the response MUST equal the configured AWS +# //# KMS key identifier. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# * The length of the response's "Plaintext" MUST equal the key +# //# derivation input length (algorithm-suites.md#key-derivation-input- +# //# length) specified by the algorithm suite (algorithm-suites.md) +# //# included in the input decryption materials +# //# (structures.md#decryption-materials). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# If the response does not satisfies these requirements then an error +# //# MUST be collected and the next encrypted data key in the filtered set +# //# MUST be attempted. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# If the response does satisfies these requirements then OnDecrypt MUST +# //# do the following with the response: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# If OnDecrypt fails to successfully decrypt any encrypted data key +# //# (structures.md#encrypted-data-key), then it MUST yield an error that +# //# includes all the collected errors. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# If OnDecrypt fails to successfully decrypt any encrypted data key +# //# (structures.md#encrypted-data-key), then it MUST yield an error that +# //# includes all the collected errors. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.6 +# //= type=exception +# //# The AWS KMS +# //# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- +# //# valid-aws-kms-identifier). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key- +# //# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or +# //# OnDecrypt MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-keyring.txt#2.8 +# //= type=exception +# //# If the decryption materials (structures.md#decryption-materials) +# //# already contained a valid plaintext data key OnDecrypt MUST +# //# immediately return the unmodified decryption materials +# //# (structures.md#decryption-materials). + diff --git a/compliance_exceptions/aws-kms-mrk-aware-symmetric-region-discovery-keyring.py b/compliance_exceptions/aws-kms-mrk-aware-symmetric-region-discovery-keyring.py new file mode 100644 index 000000000..bd289fe73 --- /dev/null +++ b/compliance_exceptions/aws-kms-mrk-aware-symmetric-region-discovery-keyring.py @@ -0,0 +1,146 @@ +# The AWS Encryption SDK - Python does not implement Keyrings + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.5 +# //= type=exception +# //# MUST implement that AWS Encryption SDK Keyring interface (../keyring- +# //# interface.md#interface) + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 +# //= type=exception +# //# On initialization the caller MUST provide: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 +# //= type=exception +# //# The keyring MUST know what Region the AWS KMS client is in. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 +# //= type=exception +# //# It +# //# SHOULD obtain this information directly from the client as opposed to +# //# having an additional parameter. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 +# //= type=exception +# //# However if it can not, then it MUST +# //# NOT create the client itself. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.6 +# //= type=exception +# //# It SHOULD have a Region parameter and +# //# SHOULD try to identify mismatched configurations. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.7 +# //= type=exception +# //# This function MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# OnDecrypt MUST take decryption materials (structures.md#decryption- +# //# materials) and a list of encrypted data keys +# //# (structures.md#encrypted-data-key) as input. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# The set of encrypted data keys MUST first be filtered to match this +# //# keyring's configuration. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * Its provider ID MUST exactly match the value "aws-kms". + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * If a discovery filter is configured, its partition and the +# //# provider info partition MUST match. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * If a discovery filter is configured, its set of accounts MUST +# //# contain the provider info account. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * If the provider info is not identified as a multi-Region key (aws- +# //# kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then the +# //# provider info's Region MUST match the AWS KMS client region. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * If the provider info is not identified as a multi-Region key (aws- +# //# kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then the +# //# provider info's Region MUST match the AWS KMS client region. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# For each encrypted data key in the filtered set, one at a time, the +# //# OnDecrypt MUST attempt to decrypt the data key. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# To attempt to decrypt a particular encrypted data key +# //# (structures.md#encrypted-data-key), OnDecrypt MUST call AWS KMS +# //# Decrypt (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_Decrypt.html) with the configured AWS KMS client. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# When calling AWS KMS Decrypt +# //# (https://docs.aws.amazon.com/kms/latest/APIReference/ +# //# API_Decrypt.html), the keyring MUST call with a request constructed +# //# as follows: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * "KeyId": If the provider info's resource type is "key" and its +# //# resource is a multi-Region key then a new ARN MUST be created +# //# where the region part MUST equal the AWS KMS client region and +# //# every other part MUST equal the provider info. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# Otherwise it MUST +# //# be the provider info. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * The "KeyId" field in the response MUST equal the requested "KeyId" + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * The length of the response's "Plaintext" MUST equal the key +# //# derivation input length (algorithm-suites.md#key-derivation-input- +# //# length) specified by the algorithm suite (algorithm-suites.md) +# //# included in the input decryption materials +# //# (structures.md#decryption-materials). + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# If the response does not satisfies these requirements then an error +# //# is collected and the next encrypted data key in the filtered set MUST +# //# be attempted. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# Since the response does satisfies these requirements then OnDecrypt +# //# MUST do the following with the response: + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# If OnDecrypt fails to successfully decrypt any encrypted data key +# //# (structures.md#encrypted-data-key), then it MUST yield an error that +# //# includes all collected errors. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# * The provider info MUST be a valid AWS KMS ARN (aws-kms-key- +# //# arn.md#a-valid-aws-kms-arn) with a resource type of "key" or +# //# OnDecrypt MUST fail. + +# //= compliance/framework/aws-kms/aws-kms-mrk-aware-symmetric-region-discovery-keyring.txt#2.8 +# //= type=exception +# //# If the decryption materials (structures.md#decryption-materials) +# //# already contained a valid plaintext data key OnDecrypt MUST +# //# immediately return the unmodified decryption materials +# //# (structures.md#decryption-materials). + + diff --git a/examples/src/mrk_aware_kms_provider.py b/examples/src/mrk_aware_kms_provider.py new file mode 100644 index 000000000..da04c7d62 --- /dev/null +++ b/examples/src/mrk_aware_kms_provider.py @@ -0,0 +1,93 @@ +# Copyright 2021 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. +"""Example showing encryption of a value already in memory using one KMS CMK, then decryption of the ciphertext using +a DiscoveryAwsKmsMasterKeyProvider. +""" + +import aws_encryption_sdk +from aws_encryption_sdk import CommitmentPolicy +from aws_encryption_sdk.internal.arn import arn_from_str +from aws_encryption_sdk.key_providers.kms import ( + DiscoveryFilter, + MRKAwareDiscoveryAwsKmsMasterKeyProvider, + MRKAwareStrictAwsKmsMasterKeyProvider, +) + + +def encrypt_decrypt(mrk_arn, mrk_arn_second_region, source_plaintext): + """Illustrates usage of KMS Multi-Region Keys. + + :param str mrk_arn: Amazon Resource Name (ARN) of the first KMS MRK + :param str mrk_arn_second_region: Amazon Resource Name (ARN) of a related KMS MRK in a different region + :param bytes source_plaintext: Data to encrypt + """ + # Encrypt in the first region + + # Set up an encryption client with an explicit commitment policy. Note that if you do not explicitly choose a + # commitment policy, REQUIRE_ENCRYPT_REQUIRE_DECRYPT is used by default. + client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT) + + # For this example, set mrk_arn to be a Multi-Region key. + # Multi-Region keys have a distinctive key ID that begins with 'mrk'. + # For example: "arn:aws:kms:us-east-1:111122223333:key/mrk-1234abcd12ab34cd56ef1234567890ab". + + # Create a Strict Multi-Region Key Aware Master Key Provider which targets the Multi-Region key ARN. + kwargs = dict(key_ids=[mrk_arn]) + strict_key_provider = MRKAwareStrictAwsKmsMasterKeyProvider(**kwargs) + + # Encrypt the plaintext using the AWS Encryption SDK. It returns the encrypted message and the header + ciphertext, _ = client.encrypt(source=source_plaintext, key_provider=strict_key_provider) + + # Decrypt in a second region + + # For this example, set mrk_arn_second_region to be Multi-Region key related to key_arn. + # Related multi-Region keys have the same key ID. Their key ARNs differs only in the Region field. + # For example: "arn:aws:kms:us-west-2:111122223333:key/mrk-1234abcd12ab34cd56ef1234567890ab" + + # Create a Strict Multi-Region Key Aware Master Key Provider which targets the Multi-Region key in the second region + kwargs = dict(key_ids=[mrk_arn_second_region]) + strict_key_provider_region_2 = MRKAwareStrictAwsKmsMasterKeyProvider(**kwargs) + + # Decrypt your ciphertext + plaintext, _ = client.decrypt(source=ciphertext, key_provider=strict_key_provider_region_2) + + # Verify that the original message and the decrypted message are the same + assert source_plaintext == plaintext + + # Decrypt in discovery mode in a second region + + # First determine what region you want to perform discovery in, as well as what + # accounts and partition you want to allow if using a Discovery Filter. + # In this example, we just want to use whatever region, account, and partition + # our second key is in, in order to ensure we can discover it. + # Note that the ARN itself is never used in the configuration. + arn = arn_from_str(mrk_arn_second_region) + discovery_region = arn.region + filter_accounts = [arn.account_id] + filter_partition = arn.partition + + # Configure a Discovery Region and optional Discovery Filter + decrypt_kwargs = dict( + discovery_filter=DiscoveryFilter(account_ids=filter_accounts, partition=filter_partition), + discovery_region=discovery_region, + ) + + # Create an MRK-aware master key provider in discovery mode that targets the second region. + # This will cause the provider to try to decrypt using this region whenever it encounters an MRK. + discovery_key_provider = MRKAwareDiscoveryAwsKmsMasterKeyProvider(**decrypt_kwargs) + + # Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header. + plaintext, _ = client.decrypt(source=ciphertext, key_provider=discovery_key_provider) + + # Verify that the original message and the decrypted message are the same + assert source_plaintext == plaintext diff --git a/examples/test/examples_test_utils.py b/examples/test/examples_test_utils.py index 6844ae0f4..8a51f21c8 100644 --- a/examples/test/examples_test_utils.py +++ b/examples/test/examples_test_utils.py @@ -47,4 +47,9 @@ ) -from integration_test_utils import get_cmk_arn, get_second_cmk_arn # noqa pylint: disable=unused-import,import-error +from integration_test_utils import ( # noqa pylint: disable=unused-import,import-error + get_cmk_arn, + get_second_cmk_arn, + get_mrk_arn, + get_second_mrk_arn, +) diff --git a/examples/test/test_i_mrk_aware_kms_provider.py b/examples/test/test_i_mrk_aware_kms_provider.py new file mode 100644 index 000000000..8e7a003f8 --- /dev/null +++ b/examples/test/test_i_mrk_aware_kms_provider.py @@ -0,0 +1,29 @@ +# Copyright 2021 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. +"""Unit test suite for the encryption and decryption using one KMS CMK example.""" + +import pytest + +from ..src.mrk_aware_kms_provider import encrypt_decrypt +from .examples_test_utils import get_mrk_arn, get_second_mrk_arn +from .examples_test_utils import static_plaintext + + +pytestmark = [pytest.mark.examples] + + +def test_discovery_kms_provider(): + plaintext = static_plaintext + cmk_arn_1 = get_mrk_arn() + cmk_arn_2 = get_second_mrk_arn() + encrypt_decrypt(mrk_arn=cmk_arn_1, mrk_arn_second_region=cmk_arn_2, source_plaintext=plaintext) diff --git a/src/aws_encryption_sdk/identifiers.py b/src/aws_encryption_sdk/identifiers.py index ab6bcadd8..c3c4d38b0 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.2.0" +__version__ = "2.3.0" USER_AGENT_SUFFIX = "AwsEncryptionSdkPython/{}".format(__version__) diff --git a/src/aws_encryption_sdk/internal/arn.py b/src/aws_encryption_sdk/internal/arn.py index ea2fc3ad7..4efe6b7d5 100644 --- a/src/aws_encryption_sdk/internal/arn.py +++ b/src/aws_encryption_sdk/internal/arn.py @@ -36,8 +36,92 @@ def __init__(self, partition, service, region, account_id, resource_type, resour self.resource_type = resource_type self.resource_id = resource_id + def to_string(self): + """Returns the string format of the ARN.""" + return ":".join( + [ + "arn", + self.partition, + self.service, + self.region, + self.account_id, + "/".join([self.resource_type, self.resource_id]), + ] + ) -def arn_from_str(arn_str): + def indicates_multi_region_key(self): + """Returns True if this ARN indicates a multi-region key, otherwise False""" + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //# If resource type is "alias", this is an AWS KMS alias ARN and MUST + # //# return false. + + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //# If resource type is "key" and resource ID does not start with "mrk-", + # //# this is a (single-region) AWS KMS key ARN and MUST return false. + + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //# If resource type is "key" and resource ID starts with + # //# "mrk-", this is a AWS KMS multi-Region key ARN and MUST return true. + + return self.resource_type == "key" and self.resource_id.startswith("mrk-") + + +def is_valid_mrk_arn_str(arn_str): + """Determines whether a string can be interpreted as + a valid MRK ARN + + :param str arn_str: The string to parse. + :returns: a bool representing whether this key ARN indicates an MRK + :rtype: bool + :raises MalformedArnError: if the string fails to parse as an ARN + """ + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //# This function MUST take a single AWS KMS ARN + + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //# If the input is an invalid AWS KMS ARN this function MUST error. + arn = arn_from_str(arn_str) + return arn.indicates_multi_region_key() + + +def is_valid_mrk_identifier(id_str): + """Determines whether a string can be interpreted as + a valid MRK identifier; either an MRK arn or a raw resource ID for an MRK. + + :param str id_str: The string to parse. + :returns: a bool representing whether this key identifier indicates an MRK + :rtype: bool + :raises MalformedArnError: if the string starts with "arn:" but fails to parse as an ARN + """ + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //# This function MUST take a single AWS KMS identifier + + if id_str.startswith("arn:"): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //# If the input starts with "arn:", this MUST return the output of + # //# identifying an an AWS KMS multi-Region ARN (aws-kms-key- + # //# arn.md#identifying-an-an-aws-kms-multi-region-arn) called with this + # //# input. + return is_valid_mrk_arn_str(id_str) + elif id_str.startswith("alias/"): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //# If the input starts with "alias/", this an AWS KMS alias and not a + # //# multi-Region key id and MUST return false. + return False + elif id_str.startswith("mrk-"): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //# If the input starts with "mrk-", this is a multi-Region key id and + # //# MUST return true. + return True + else: + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //# If + # //# the input does not start with any of the above, this is a multi- + # //# Region key id and MUST return false. + return False + + +def arn_from_str(arn_str): # noqa: C901 """Parses an input string as an ARN. :param str arn_str: The string to parse. @@ -47,20 +131,58 @@ def arn_from_str(arn_str): """ elements = arn_str.split(":", 5) - if elements[0] != "arn": - raise MalformedArnError("Resource {} could not be parsed as an ARN".format(arn_str)) - try: + if elements[0] != "arn": + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //# MUST start with string "arn" + raise MalformedArnError("Missing 'arn' string") + partition = elements[1] service = elements[2] region = elements[3] account = elements[4] + if not partition: + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //# The partition MUST be a non-empty + raise MalformedArnError("Missing partition") + + if not account: + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //# The account MUST be a non-empty string + raise MalformedArnError("Missing account") + + if not region: + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //# The region MUST be a non-empty string + raise MalformedArnError("Missing region") + + if service != "kms": + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //# The service MUST be the string "kms" + raise MalformedArnError("Unknown service") + resource = elements[5] + if not resource: + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //# The resource section MUST be non-empty and MUST be split by a + # //# single "/" any additional "/" are included in the resource id + raise MalformedArnError("Missing resource") + resource_elements = resource.split("/", 1) resource_type = resource_elements[0] resource_id = resource_elements[1] + if resource_type not in ("alias", "key"): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //# The resource type MUST be either "alias" or "key" + raise MalformedArnError("Unknown resource type") + + if not resource_id: + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //# The resource id MUST be a non-empty string + raise MalformedArnError("Missing resource id") + return Arn(partition, service, region, account, resource_type, resource_id) - except IndexError: - raise MalformedArnError("Resource {} could not be parsed as an ARN".format(arn_str)) + except (IndexError, MalformedArnError) as exc: + raise MalformedArnError("Resource {} could not be parsed as an ARN: {}".format(arn_str, exc.args[0])) diff --git a/src/aws_encryption_sdk/key_providers/base.py b/src/aws_encryption_sdk/key_providers/base.py index e67219a3e..d0871e802 100644 --- a/src/aws_encryption_sdk/key_providers/base.py +++ b/src/aws_encryption_sdk/key_providers/base.py @@ -229,12 +229,22 @@ def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): master_key = None _LOGGER.debug("starting decrypt data key attempt") for member in [self] + self._members: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //# To match the encrypted data key's + # //# provider ID MUST exactly match the value "aws-kms". if member.provider_id == encrypted_data_key.key_provider.provider_id: _LOGGER.debug("attempting to locate master key from key provider: %s", member.provider_id) if isinstance(member, MasterKey): _LOGGER.debug("using existing master key") master_key = member elif self.vend_masterkey_on_decrypt: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //# For each encrypted data key in the filtered set, one at a time, the + # //# master key provider MUST call Get Master Key (aws-kms-mrk-aware- + # //# master-key-provider.md#get-master-key) with the encrypted data key's + # //# provider info as the AWS KMS key ARN. + # We attempt to decrypt with pre-populated self._members for strict MKPs/MKs + # and vend new MKs for Discovery MPKs/MKs. try: _LOGGER.debug("attempting to add master key: %s", encrypted_data_key.key_provider.key_info) master_key = member.master_key_for_decrypt(encrypted_data_key.key_provider.key_info) @@ -249,6 +259,12 @@ def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): _LOGGER.debug( "attempting to decrypt data key with provider %s", encrypted_data_key.key_provider.key_info ) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //# It MUST call Decrypt Data Key + # //# (aws-kms-mrk-aware-master-key.md#decrypt-data-key) on this master key + # //# with the input algorithm, this single encrypted data key, and the + # //# input encryption context. + data_key = master_key.decrypt_data_key(encrypted_data_key, algorithm, encryption_context) except (IncorrectMasterKeyError, DecryptKeyError) as error: _LOGGER.debug( @@ -257,8 +273,27 @@ def decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context): master_key.key_provider, ) continue + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# If the AWS KMS response satisfies the requirements then it MUST be + # //# use and this function MUST return and not attempt to decrypt any more + # //# encrypted data keys. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //# If the decrypt data key call is + # //# successful, then this function MUST return this result and not + # //# attempt to decrypt any more encrypted data keys. + break # If this point is reached without throwing any errors, the data key has been decrypted if not data_key: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# If all the input encrypted data keys have been processed then this + # //# function MUST yield an error that includes all the collected errors. + # Note the latter half of "includes all collected errors" is not satisfied + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //# If all the input encrypted data keys have been processed then this + # //# function MUST yield an error that includes all the collected errors. + # Note the latter half of "includes all collected errors" is not satisfied raise DecryptKeyError("Unable to decrypt data key") return data_key @@ -296,6 +331,8 @@ class MasterKeyConfig(object): :param bytes key_id: Key ID for Master Key """ + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //# The AWS KMS key identifier MUST NOT be null or empty. key_id = attr.ib(hash=True, validator=attr.validators.instance_of((six.string_types, bytes)), converter=to_bytes) def __attrs_post_init__(self): @@ -418,6 +455,10 @@ def generate_data_key(self, algorithm, encryption_context): """ _LOGGER.info("generating data key with encryption context: %s", encryption_context) generated_data_key = self._generate_data_key(algorithm=algorithm, encryption_context=encryption_context) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //# If the call succeeds the AWS KMS Generate Data Key response's + # //# "Plaintext" MUST match the key derivation input length specified by + # //# the algorithm suite included in the input. aws_encryption_sdk.internal.utils.source_data_key_length_check( source_data_key=generated_data_key, algorithm=algorithm ) diff --git a/src/aws_encryption_sdk/key_providers/kms.py b/src/aws_encryption_sdk/key_providers/kms.py index 0ff71ff3e..ab18c62ed 100644 --- a/src/aws_encryption_sdk/key_providers/kms.py +++ b/src/aws_encryption_sdk/key_providers/kms.py @@ -13,6 +13,7 @@ """Master Key Providers for use with AWS KMS""" import abc import functools +import itertools import logging import attr @@ -28,11 +29,12 @@ DecryptKeyError, EncryptKeyError, GenerateKeyError, + MalformedArnError, MasterKeyProviderError, UnknownRegionError, ) from aws_encryption_sdk.identifiers import USER_AGENT_SUFFIX -from aws_encryption_sdk.internal.arn import arn_from_str +from aws_encryption_sdk.internal.arn import arn_from_str, is_valid_mrk_identifier from aws_encryption_sdk.internal.str_ops import to_str from aws_encryption_sdk.key_providers.base import MasterKey, MasterKeyConfig, MasterKeyProvider, MasterKeyProviderConfig from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo @@ -62,6 +64,70 @@ def _region_from_key_id(key_id, default_region=None): return region_name +def _key_resource_match(key1, key2): + """Given two KMS key identifiers, determines whether they use the same key type resource ID. + This method works with either bare key IDs or key ARNs; if an input cannot be parsed as an ARN + it is assumed to be a bare key ID. Will output false if either input is an alias arn. + """ + try: + arn1 = arn_from_str(key1) + if arn1.resource_type == "alias": + return False + resource_id_1 = arn1.resource_id + except MalformedArnError: + # We need to handle the case where the key id is not ARNs, + # treat it as a bare id + resource_id_1 = key1 + try: + arn2 = arn_from_str(key2) + if arn2.resource_type == "alias": + return False + resource_id_2 = arn2.resource_id + except MalformedArnError: + # We need to handle the case where the key id is not ARNs, + # treat it as a bare id + resource_id_2 = key2 + + return resource_id_1 == resource_id_2 + + +def _check_mrk_arns_equal(key1, key2): + """Given two KMS key arns, determines whether they refer to related KMS MRKs. + Returns an error if inputs are not equal and either input cannot be parsed as an ARN. + """ + # //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + # //# The caller MUST provide: + if key1 == key2: + # //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + # //# If both identifiers are identical, this function MUST return "true". + return True + + # Note that we will fail here if the input keys are not ARNs at this point + arn1 = arn_from_str(key1) + arn2 = arn_from_str(key2) + + if not arn1.indicates_multi_region_key() or not arn2.indicates_multi_region_key(): + # //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + # //# Otherwise if either input is not identified as a multi-Region key + # //# (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then + # //# this function MUST return "false". + return False + + # //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + # //# Otherwise if both inputs are + # //# identified as a multi-Region keys (aws-kms-key-arn.md#identifying-an- + # //# aws-kms-multi-region-key), this function MUST return the result of + # //# comparing the "partition", "service", "accountId", "resourceType", + # //# and "resource" parts of both ARN inputs. + return ( + arn1.partition == arn2.partition + and arn1.service == arn2.service + and arn1.account_id == arn2.account_id + and arn1.resource_type == arn2.resource_type + and arn1.resource_id == arn2.resource_id + ) + + @attr.s(hash=True) class DiscoveryFilter(object): """DiscoveryFilter to control accounts and partitions that can be used by a KMS Master Key Provider. @@ -76,6 +142,378 @@ class DiscoveryFilter(object): partition = attr.ib(default=None, hash=True, validator=attr.validators.optional(attr.validators.instance_of(str))) +@attr.s(hash=True) +class KMSMasterKeyConfig(MasterKeyConfig): + """Configuration object for KMSMasterKey objects. + + :param str key_id: KMS CMK ID + :param client: Boto3 KMS client + :type client: botocore.client.KMS + :param list grant_tokens: List of grant tokens to pass to KMS on CMK operations + """ + + provider_id = _PROVIDER_ID + client = attr.ib(hash=True, validator=attr.validators.instance_of(botocore.client.BaseClient)) + grant_tokens = attr.ib( + hash=True, default=attr.Factory(tuple), validator=attr.validators.instance_of(tuple), converter=tuple + ) + + @client.default + def client_default(self): + """Create a client if one was not provided.""" + try: + region_name = _region_from_key_id(to_str(self.key_id)) + kwargs = dict(region_name=region_name) + except UnknownRegionError: + kwargs = {} + botocore_config = botocore.config.Config(user_agent_extra=USER_AGENT_SUFFIX) + return boto3.session.Session(**kwargs).client("kms", config=botocore_config) + + +class KMSMasterKey(MasterKey): + """Master Key class for KMS CMKs. + + :param config: Configuration object (config or individual parameters required) + :type config: aws_encryption_sdk.key_providers.kms.KMSMasterKeyConfig + :param bytes key_id: KMS CMK ID + :param client: Boto3 KMS client + :type client: botocore.client.KMS + :param list grant_tokens: List of grant tokens to pass to KMS on CMK operations + """ + + provider_id = _PROVIDER_ID + _config_class = KMSMasterKeyConfig + + def __init__(self, **kwargs): # pylint: disable=unused-argument + """Performs transformations needed for KMS.""" + self._key_id = to_str(self.key_id) # KMS client requires str, not bytes + + def _generate_data_key(self, algorithm, encryption_context=None): + """Generates data key and returns plaintext and ciphertext of key. + + :param algorithm: Algorithm on which to base data key + :type algorithm: aws_encryption_sdk.identifiers.Algorithm + :param dict encryption_context: Encryption context to pass to KMS + :returns: Generated data key + :rtype: aws_encryption_sdk.structures.DataKey + """ + kms_params = self._build_generate_data_key_request(algorithm, encryption_context) + # Catch any boto3 errors and normalize to expected EncryptKeyError + try: + response = self.config.client.generate_data_key(**kms_params) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //# The response's "Plaintext" MUST be the plaintext in the output. + plaintext = response["Plaintext"] + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //# The response's cipher text blob MUST be used as the returned as the + # //# ciphertext for the encrypted data key in the output. + ciphertext = response["CiphertextBlob"] + key_id = response["KeyId"] + except (ClientError, KeyError): + error_message = "Master Key {key_id} unable to generate data key".format(key_id=self._key_id) + _LOGGER.exception(error_message) + raise GenerateKeyError(error_message) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //# The response's "KeyId" MUST be valid. + # arn_from_str will error if given an invalid key ARN + try: + key_id_str = to_str(key_id) + arn_from_str(key_id_str) + except MalformedArnError: + error_message = "Retrieved an unexpected KeyID in response from KMS: {key_id}".format(key_id=key_id) + _LOGGER.exception(error_message) + raise GenerateKeyError(error_message) + + return DataKey( + key_provider=MasterKeyInfo(provider_id=self.provider_id, key_info=key_id), + data_key=plaintext, + encrypted_data_key=ciphertext, + ) + + def _encrypt_data_key(self, data_key, algorithm, encryption_context=None): + """Encrypts a data key and returns the ciphertext. + + :param data_key: Unencrypted data key + :type data_key: :class:`aws_encryption_sdk.structures.RawDataKey` + or :class:`aws_encryption_sdk.structures.DataKey` + :param algorithm: Placeholder to maintain API compatibility with parent + :param dict encryption_context: Encryption context to pass to KMS + :returns: Data key containing encrypted data key + :rtype: aws_encryption_sdk.structures.EncryptedDataKey + :raises EncryptKeyError: if Master Key is unable to encrypt data key + """ + kms_params = self._build_encrypt_request(data_key, encryption_context) + # Catch any boto3 errors and normalize to expected EncryptKeyError + try: + response = self.config.client.encrypt(**kms_params) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //# The response's cipher text blob MUST be used as the "ciphertext" for the + # //# encrypted data key. + ciphertext = response["CiphertextBlob"] + key_id = response["KeyId"] + except (ClientError, KeyError): + error_message = "Master Key {key_id} unable to encrypt data key".format(key_id=self._key_id) + _LOGGER.exception(error_message) + raise EncryptKeyError(error_message) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //# The AWS KMS Encrypt response MUST contain a valid "KeyId". + # arn_from_str will error if given an invalid key ARN + try: + key_id_str = to_str(key_id) + arn_from_str(key_id_str) + except MalformedArnError: + error_message = "Retrieved an unexpected KeyID in response from KMS: {key_id}".format(key_id=key_id) + _LOGGER.exception(error_message) + raise EncryptKeyError(error_message) + + return EncryptedDataKey( + key_provider=MasterKeyInfo(provider_id=self.provider_id, key_info=key_id), encrypted_data_key=ciphertext + ) + + def _decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context=None): + """Decrypts an encrypted data key and returns the plaintext. + + :param data_key: Encrypted data key + :type data_key: aws_encryption_sdk.structures.EncryptedDataKey + :type algorithm: `aws_encryption_sdk.identifiers.Algorithm` (not used for KMS) + :param dict encryption_context: Encryption context to use in decryption + :returns: Decrypted data key + :rtype: aws_encryption_sdk.structures.DataKey + :raises DecryptKeyError: if Master Key is unable to decrypt data key + """ + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# Additionally each provider info MUST be a valid AWS KMS ARN + # //# (aws-kms-key-arn.md#a-valid-aws-kms-arn) with a resource type of + # //# "key". + edk_key_id = to_str(encrypted_data_key.key_provider.key_info) + edk_arn = arn_from_str(edk_key_id) + if not edk_arn.resource_type == "key": + error_message = "AWS KMS Provider EDK contains unexpected key_id: {key_id}".format(key_id=edk_key_id) + _LOGGER.exception(error_message) + raise DecryptKeyError(error_message) + + self._validate_allowed_to_decrypt(edk_key_id) + kms_params = self._build_decrypt_request(encrypted_data_key, encryption_context) + # Catch any boto3 errors and normalize to expected DecryptKeyError + try: + response = self.config.client.decrypt(**kms_params) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# If the call succeeds then the response's "KeyId" MUST be equal to the + # //# configured AWS KMS key identifier otherwise the function MUST collect + # //# an error. + # Note that Python logs but does not collect errors + returned_key_id = response["KeyId"] + if returned_key_id != self._key_id: + error_message = "AWS KMS returned unexpected key_id {returned} (expected {key_id})".format( + returned=returned_key_id, key_id=self._key_id + ) + _LOGGER.exception(error_message) + raise DecryptKeyError(error_message) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# The response's "Plaintext"'s length MUST equal the length + # //# required by the requested algorithm suite otherwise the function MUST + # //# collect an error. + # Note that Python logs but does not collect errors + plaintext = response["Plaintext"] + if len(plaintext) != algorithm.data_key_len: + error_message = "Plaintext length ({len1}) does not match algorithm's expected length ({len2})".format( + len1=len(plaintext), len2=algorithm.data_key_len + ) + raise DecryptKeyError(error_message) + + except (ClientError, KeyError): + error_message = "Master Key {key_id} unable to decrypt data key".format(key_id=self._key_id) + _LOGGER.exception(error_message) + raise DecryptKeyError(error_message) + return DataKey( + key_provider=self.key_provider, data_key=plaintext, encrypted_data_key=encrypted_data_key.encrypted_data_key + ) + + def _build_decrypt_request(self, encrypted_data_key, encryption_context): + """Prepares a decrypt request to send to KMS.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# To decrypt the encrypted data key this master key MUST use the + # //# configured AWS KMS client to make an AWS KMS Decrypt + # //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + # //# API_Decrypt.html) request constructed as follows: + kms_params = {"CiphertextBlob": encrypted_data_key.encrypted_data_key, "KeyId": self._key_id} + if encryption_context: + kms_params["EncryptionContext"] = encryption_context + if self.config.grant_tokens: + kms_params["GrantTokens"] = self.config.grant_tokens + return kms_params + + def _build_generate_data_key_request(self, algorithm, encryption_context): + """Prepares a generate data key request to send to KMS.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //# This master key MUST use the configured AWS KMS client to make an AWS KMS + # //# GenerateDatakey (https://docs.aws.amazon.com/kms/latest/APIReference/ + # //# API_GenerateDataKey.html) request constructed as follows: + kms_params = {"KeyId": self._key_id, "NumberOfBytes": algorithm.kdf_input_len} + if encryption_context is not None: + kms_params["EncryptionContext"] = encryption_context + if self.config.grant_tokens: + kms_params["GrantTokens"] = self.config.grant_tokens + return kms_params + + def _build_encrypt_request(self, data_key, encryption_context): + """Prepares an encrypt request to send to KMS.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //# The master key MUST use the configured AWS KMS client to make an AWS KMS Encrypt + # //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + # //# API_Encrypt.html) request constructed as follows: + kms_params = {"KeyId": self._key_id, "Plaintext": data_key.data_key} + if encryption_context: + kms_params["EncryptionContext"] = encryption_context + if self.config.grant_tokens: + kms_params["GrantTokens"] = self.config.grant_tokens + return kms_params + + def _validate_allowed_to_decrypt(self, edk_key_id): + """Checks that this provider is allowed to decrypt with the given key id.""" + if edk_key_id != self._key_id: + raise DecryptKeyError( + "Cannot decrypt EDK wrapped by key_id={}, because it does not match this " + "provider's key_id={}".format(edk_key_id, self._key_id) + ) + + +@attr.s(hash=True) +class MRKAwareKMSMasterKeyConfig(MasterKeyConfig): + """Configuration object for MRKAwareKMSMasterKey objects. Mostly the same as KMSMasterKey, except the + client parameter is required rather than optional. + + :param str key_id: KMS CMK ID + :param client: Boto3 KMS client + :type client: botocore.client.KMS + :param list grant_tokens: List of grant tokens to pass to KMS on CMK operations + """ + + provider_id = _PROVIDER_ID + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //# The AWS KMS SDK client MUST not be null. + client = attr.ib(hash=True, validator=attr.validators.instance_of(botocore.client.BaseClient)) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //# The master key MUST be able to be + # //# configured with an optional list of Grant Tokens. + grant_tokens = attr.ib( + hash=True, default=attr.Factory(tuple), validator=attr.validators.instance_of(tuple), converter=tuple + ) + + +class MRKAwareKMSMasterKey(KMSMasterKey): + """Master Key class for KMS MRKAware CMKs. The logic for this class is almost entirely the same as a normal + KMSMasterKey ("single-region key"). The primary difference is that this class is more flexible in what ciphertexts + it will try to decrypt; specifically, it knows how to treat related multi-region keys as identical for the + purposes of checking whether it is allowed to decrypt. + + :param config: Configuration object (config or individual parameters required) + :type config: aws_encryption_sdk.key_providers.kms.KMSMasterKeyConfig + :param bytes key_id: KMS CMK ID + :param client: Boto3 KMS client + :type client: botocore.client.KMS + :param list grant_tokens: List of grant tokens to pass to KMS on CMK operations + """ + + # The following are true because MRKAwareKMSMasterKey transitively extends MasterKey: + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.5 + # //# MUST implement the Master Key Interface (../master-key- + # //# interface.md#interface) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.7 + # //# MUST be unchanged from the Master Key interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.8 + # //# MUST be unchanged from the Master Key interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# The inputs MUST be the same as the Master Key Decrypt Data Key + # //# (../master-key-interface.md#decrypt-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# The output MUST be the same as the Master Key Decrypt Data Key + # //# (../master-key-interface.md#decrypt-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //# The inputs MUST be the same as the Master Key Generate Data Key + # //# (../master-key-interface.md#generate-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //# The output MUST be the same as the Master Key Generate Data Key + # //# (../master-key-interface.md#generate-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //# The inputs MUST be the same as the Master Key Encrypt Data Key + # //# (../master-key-interface.md#encrypt-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //# The output MUST be the same as the Master Key Encrypt Data Key + # //# (../master-key-interface.md#encrypt-data-key) interface. + + provider_id = _PROVIDER_ID + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //# On initialization, the caller MUST provide: + _config_class = MRKAwareKMSMasterKeyConfig + + def __init__(self, **kwargs): + """Sets configuration required by this provider type.""" + super(MRKAwareKMSMasterKey, self).__init__(**kwargs) + + self.validate_config() + + def validate_config(self): + """Validates the provided configuration.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //# The AWS KMS + # //# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- + # //# valid-aws-kms-identifier). + # If it starts with "arn:" ensure it's a valid arn by attempting to parse it. + # Otherwise, we don't do any validation on bare ids or bare aliases. + if self._key_id.startswith("arn:"): + arn_from_str(self._key_id) + + def _validate_allowed_to_decrypt(self, edk_key_id): + """Checks that this provider is allowed to decrypt with the given key id. + + Compared to the default KMS provider, this checks for MRK equality between the edk and the configured key id + rather than strict string equality. + """ + if not _check_mrk_arns_equal(edk_key_id, self._key_id): + raise DecryptKeyError( + "Cannot decrypt EDK wrapped by key_id={}, because it does not match this " + "provider's key_id={}".format(edk_key_id, self._key_id) + ) + + def owns_data_key(self, data_key): + """Determines if data_key object is owned by this MasterKey. This method overrides the method from the base + class, because for MRKs we need to check for MRK equality on the key ids rather than exact string equality. + + :param data_key: Data key to evaluate + :type data_key: :class:`aws_encryption_sdk.structures.DataKey`, + :class:`aws_encryption_sdk.structures.RawDataKey`, + or :class:`aws_encryption_sdk.structures.EncryptedDataKey` + :returns: Boolean statement of ownership + :rtype: bool + """ + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //# To match the encrypted data key's + # //# provider ID MUST exactly match the value "aws-kms" and the the + # //# function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match-for- + # //# decrypt.md#implementation) called with the configured AWS KMS key + # //# identifier and the encrypted data key's provider info MUST return + # //# "true". + if data_key.key_provider.provider_id == self.key_provider.provider_id and _check_mrk_arns_equal( + to_str(data_key.key_provider.key_info), to_str(self.key_provider.key_info) + ): + return True + return False + + @attr.s(hash=True) class KMSMasterKeyProviderConfig(MasterKeyProviderConfig): """Configuration object for KMSMasterKeyProvider objects. @@ -84,8 +522,10 @@ class KMSMasterKeyProviderConfig(MasterKeyProviderConfig): :type botocore_session: botocore.session.Session :param list key_ids: List of KMS CMK IDs with which to pre-populate provider (optional) :param list region_names: List of regions for which to pre-populate clients (optional) + :param list grant_tokens: List of grant tokens to pass to KMS on CMK operations :param DiscoveryFilter discovery_filter: Filter indicating AWS accounts and partitions whose keys will be trusted for decryption + :param str discovery_region: The region to be used for discovery for MRK-aware providers """ botocore_session = attr.ib( @@ -99,9 +539,15 @@ class KMSMasterKeyProviderConfig(MasterKeyProviderConfig): region_names = attr.ib( hash=True, default=attr.Factory(tuple), validator=attr.validators.instance_of(tuple), converter=tuple ) + grant_tokens = attr.ib( + hash=True, default=attr.Factory(tuple), validator=attr.validators.instance_of(tuple), converter=tuple + ) discovery_filter = attr.ib( hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(DiscoveryFilter)) ) + discovery_region = attr.ib( + hash=True, default=None, validator=attr.validators.optional(attr.validators.instance_of(six.string_types)) + ) @six.add_metaclass(abc.ABCMeta) @@ -110,12 +556,47 @@ class BaseKMSMasterKeyProvider(MasterKeyProvider): .. note:: Cannot be instantiated directly. Callers should use one of the implementing classes. - """ + # The following are true because both MRKAwareDiscoveryAwsKmsMasterKeyProvider + # and MRKAwareDiscoveryAwsKmsMasterKeyProvider transitively extend BaseKMSMasterKeyProvider, + # which extends MasterKeyProvider + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.5 + # //# MUST implement the Master Key Provider Interface (../master-key- + # //# provider-interface.md#interface) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //# The input MUST be the same as the Master Key Provider Get Master Key + # //# (../master-key-provider-interface.md#get-master-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //# The output MUST be the same as the Master Key Provider Get Master Key + # //# (../master-key-provider-interface.md#get-master-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + # //# The input MUST be the same as the Master Key Provider Get Master Keys + # //# For Encryption (../master-key-provider-interface.md#get-master-keys- + # //# for-encryption) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + # //# The output MUST be the same as the Master Key Provider Get Master + # //# Keys For Encryption (../master-key-provider-interface.md#get-master- + # //# keys-for-encryption) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //# The input MUST be the same as the Master Key Provider Decrypt Data + # //# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //# The output MUST be the same as the Master Key Provider Decrypt Data + # //# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + provider_id = _PROVIDER_ID _config_class = KMSMasterKeyProviderConfig default_region = None + master_key_class = KMSMasterKey + master_key_config_class = KMSMasterKeyConfig def __init__(self, **kwargs): # pylint: disable=unused-argument """Prepares mutable attributes.""" @@ -134,8 +615,6 @@ def _process_config(self): """Traverses the config and adds master keys and regional clients as needed.""" self._user_agent_adding_config = botocore.config.Config(user_agent_extra=USER_AGENT_SUFFIX) - self.validate_config() - if self.config.region_names: self.add_regional_clients_from_list(self.config.region_names) self.default_region = self.config.region_names[0] @@ -144,6 +623,14 @@ def _process_config(self): if self.default_region is not None: self.add_regional_client(self.default_region) + self.validate_config() + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + # //# If the configured mode is strict this function MUST return a + # //# list of master keys obtained by calling Get Master Key (aws-kms-mrk- + # //# aware-master-key-provider.md#get-master-key) for each AWS KMS key + # //# identifier in the configured key ids + # Note that Python creates the keys to be used for encryption on init of MKPs if self.config.key_ids: self.add_master_keys_from_list(self.config.key_ids) @@ -199,6 +686,15 @@ def _client(self, key_id): :param str key_id: KMS CMK ID """ + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=exception + # //# If the requested AWS KMS key identifier is not a well formed ARN the + # //# AWS Region MUST be the configured default region this SHOULD be + # //# obtained from the AWS SDK. + # _region_from_key_id only does a ':' split and does not determine + # whether an ARN may otherwise we well formed. This results in + # the region "us-west-2" being used for input "not:an:arn:us-west-2" + # instead of the default region. region_name = _region_from_key_id(key_id, self.default_region) self.add_regional_client(region_name) return self._regional_clients[region_name] @@ -221,9 +717,26 @@ def _new_master_key(self, key_id): arn.partition != self.config.discovery_filter.partition or arn.account_id not in self.config.discovery_filter.account_ids ): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //# In discovery mode if a discovery filter is configured the requested AWS + # //# KMS key ARN's "partition" MUST match the discovery filter's + # //# "partition" and the AWS KMS key ARN's "account" MUST exist in the + # //# discovery filter's account id set. raise MasterKeyProviderError("Key {} not allowed by this Master Key Provider".format(key_id)) + return self._new_master_key_impl(key_id) + + def _new_master_key_impl(self, key_id): + """The actual creation of new master keys. Separated out from _new_master_key so that we can share the + validation logic while also allowing subclasses to implement different logic for instantiation of the key + itself. + """ + _key_id = to_str(key_id) # KMS client requires str, not bytes - return KMSMasterKey(config=KMSMasterKeyConfig(key_id=key_id, client=self._client(_key_id))) + return self.master_key_class( + config=self.master_key_config_class( + key_id=key_id, client=self._client(_key_id), grant_tokens=self.config.grant_tokens + ) + ) class StrictAwsKmsMasterKeyProvider(BaseKMSMasterKeyProvider): @@ -269,15 +782,97 @@ def __init__(self, **kwargs): def validate_config(self): """Validates the provided configuration.""" if not self.config.key_ids: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //# The key id list MUST NOT be empty or null in strict mode. raise ConfigMismatchError("To enable strict mode you must provide key ids") for key_id in self.config.key_ids: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //# The key id list MUST NOT contain any null or empty string values. if not key_id: raise ConfigMismatchError("Key ids must be valid AWS KMS ARNs") if self.config.discovery_filter: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //# A discovery filter MUST NOT be configured in strict mode. raise ConfigMismatchError("To enable discovery mode, use a DiscoveryAwsKmsMasterKeyProvider") + if self.config.discovery_region: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //# A default MRK Region MUST NOT be configured in strict mode. + raise ConfigMismatchError( + "To enable MRK-aware discovery mode, use a MRKAwareDiscoveryAwsKmsMasterKeyProvider" + ) + + +class MRKAwareStrictAwsKmsMasterKeyProvider(StrictAwsKmsMasterKeyProvider): + """A Strict Master Key Provider for KMS that has smarts for handling Multi-Region keys. + + TODO MORE + + :param config: Configuration object (optional) + :type config: aws_encryption_sdk.key_providers.kms.KMSMasterKeyProviderConfig + :param botocore_session: botocore session object (optional) + :type botocore_session: botocore.session.Session + :param list key_ids: List of KMS CMK IDs with which to pre-populate provider (optional) + :param list region_names: List of regions for which to pre-populate clients (optional) + """ + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //# In strict mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- + # //# master-key.md) MUST be returned configured with + # This MKP returns an AWS KMS MRK Aware Master Key, however the MK + # it returns is based on configured key ID with clients that match + # those configured key's regions. Python doesn't use a regional client + # supplier, and the MRK matching logic occurs as part of owns_data_key in the MK. + master_key_class = MRKAwareKMSMasterKey + master_key_config_class = MRKAwareKMSMasterKeyConfig + + def __init__(self, **kwargs): + """Sets configuration required by this provider type.""" + super(MRKAwareStrictAwsKmsMasterKeyProvider, self).__init__(**kwargs) + + self.validate_unique_mrks() + + def validate_unique_mrks(self): + """Make sure the set of configured key ids does not contain any related MRKs""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //# All AWS KMS + # //# key identifiers are be passed to Assert AWS KMS MRK are unique (aws- + # //# kms-mrk-are-unique.md#Implementation) and the function MUST return + # //# success. + + # //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + # //# The caller MUST provide: + + # //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + # //# If the list does not contain any multi-Region keys (aws-kms-key- + # //# arn.md#identifying-an-aws-kms-multi-region-key) this function MUST + # //# exit successfully. + mrk_identifiers = filter(is_valid_mrk_identifier, self.config.key_ids) + duplicate_ids = set() + for key1, key2 in itertools.combinations(mrk_identifiers, 2): + if key1 in duplicate_ids and key2 in duplicate_ids: + pass + if _key_resource_match(key1, key2): + if key1 not in duplicate_ids: + duplicate_ids.add(key1) + if key2 not in duplicate_ids: + duplicate_ids.add(key2) + + # //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + # //# If there are zero duplicate resource ids between the multi-region + # //# keys, this function MUST exit successfully + + # //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + # //# If any duplicate multi-region resource ids exist, this function MUST + # //# yield an error that includes all identifiers with duplicate resource + # //# ids not only the first duplicate found. + if len(duplicate_ids) > 0: + raise ConfigMismatchError( + "Configured key ids must be unique. Found related MRKs: {keys}".format(keys=", ".join(duplicate_ids)) + ) + class DiscoveryAwsKmsMasterKeyProvider(BaseKMSMasterKeyProvider): """Discovery Master Key Provider for KMS. This can only be used for decryption. It is configured with an optional @@ -311,6 +906,8 @@ def __init__(self, **kwargs): def validate_config(self): """Validates the provided configuration.""" if self.config.key_ids: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //# The key id list MUST be empty in discovery mode. raise ConfigMismatchError( "To explicitly identify which keys should be used, use a " "StrictAwsKmsMasterKeyProvider." ) @@ -326,152 +923,91 @@ def validate_config(self): "When specifying a discovery filter, account ids must be non-empty " "strings" ) + if self.config.discovery_region: + raise ConfigMismatchError( + "To enable MRK-aware discovery mode, use a MRKAwareDiscoveryAwsKmsMasterKeyProvider." + ) -@attr.s(hash=True) -class KMSMasterKeyConfig(MasterKeyConfig): - """Configuration object for MasterKey objects. - - :param str key_id: KMS CMK ID - :param client: Boto3 KMS client - :type client: botocore.client.KMS - :param list grant_tokens: List of grant tokens to pass to KMS on CMK operations - """ - - provider_id = _PROVIDER_ID - client = attr.ib(hash=True, validator=attr.validators.instance_of(botocore.client.BaseClient)) - grant_tokens = attr.ib( - hash=True, default=attr.Factory(tuple), validator=attr.validators.instance_of(tuple), converter=tuple - ) - @client.default - def client_default(self): - """Create a client if one was not provided.""" - try: - region_name = _region_from_key_id(to_str(self.key_id)) - kwargs = dict(region_name=region_name) - except UnknownRegionError: - kwargs = {} - botocore_config = botocore.config.Config(user_agent_extra=USER_AGENT_SUFFIX) - return boto3.session.Session(**kwargs).client("kms", config=botocore_config) +class MRKAwareDiscoveryAwsKmsMasterKeyProvider(DiscoveryAwsKmsMasterKeyProvider): + """Discovery Master Key Provider for KMS that has smarts for handling Multi-Region keys + TODO MORE -class KMSMasterKey(MasterKey): - """Master Key class for KMS CMKs. + .. note:: + If no botocore_session is provided, the default botocore session will be used. - :param config: Configuration object (config or individual parameters required) - :type config: aws_encryption_sdk.key_providers.kms.KMSMasterKeyConfig - :param bytes key_id: KMS CMK ID - :param client: Boto3 KMS client - :type client: botocore.client.KMS - :param list grant_tokens: List of grant tokens to pass to KMS on CMK operations + :param config: Configuration object (optional) + :type config: aws_encryption_sdk.key_providers.kms.KMSMasterKeyProviderConfig + :param botocore_session: botocore session object (optional) + :type botocore_session: botocore.session.Session + :param list key_ids: List of KMS CMK IDs with which to pre-populate provider (optional) + :param list region_names: List of regions for which to pre-populate clients (optional) """ - provider_id = _PROVIDER_ID - _config_class = KMSMasterKeyConfig - - def __init__(self, **kwargs): # pylint: disable=unused-argument - """Performs transformations needed for KMS.""" - self._key_id = to_str(self.key_id) # KMS client requires str, not bytes + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + # //# If the configured mode is discovery the function MUST return an empty + # //# list. + # This is true due to behaviors that MRKAwareDiscoveryAwsKmsMasterKeyProvider extend + # Note that Python creates the keys to be used for encryption on init of KMS MKPs, + # so this MKP will vend no encrypt keys as no key IDs are configured. - def _generate_data_key(self, algorithm, encryption_context=None): - """Generates data key and returns plaintext and ciphertext of key. - - :param algorithm: Algorithm on which to base data key - :type algorithm: aws_encryption_sdk.identifiers.Algorithm - :param dict encryption_context: Encryption context to pass to KMS - :returns: Generated data key - :rtype: aws_encryption_sdk.structures.DataKey - """ - kms_params = {"KeyId": self._key_id, "NumberOfBytes": algorithm.kdf_input_len} - if encryption_context is not None: - kms_params["EncryptionContext"] = encryption_context - if self.config.grant_tokens: - kms_params["GrantTokens"] = self.config.grant_tokens - # Catch any boto3 errors and normalize to expected EncryptKeyError - try: - response = self.config.client.generate_data_key(**kms_params) - plaintext = response["Plaintext"] - ciphertext = response["CiphertextBlob"] - key_id = response["KeyId"] - except (ClientError, KeyError): - error_message = "Master Key {key_id} unable to generate data key".format(key_id=self._key_id) - _LOGGER.exception(error_message) - raise GenerateKeyError(error_message) - return DataKey( - key_provider=MasterKeyInfo(provider_id=self.provider_id, key_info=key_id), - data_key=plaintext, - encrypted_data_key=ciphertext, - ) - - def _encrypt_data_key(self, data_key, algorithm, encryption_context=None): - """Encrypts a data key and returns the ciphertext. - - :param data_key: Unencrypted data key - :type data_key: :class:`aws_encryption_sdk.structures.RawDataKey` - or :class:`aws_encryption_sdk.structures.DataKey` - :param algorithm: Placeholder to maintain API compatibility with parent - :param dict encryption_context: Encryption context to pass to KMS - :returns: Data key containing encrypted data key - :rtype: aws_encryption_sdk.structures.EncryptedDataKey - :raises EncryptKeyError: if Master Key is unable to encrypt data key - """ - kms_params = {"KeyId": self._key_id, "Plaintext": data_key.data_key} - if encryption_context: - kms_params["EncryptionContext"] = encryption_context - if self.config.grant_tokens: - kms_params["GrantTokens"] = self.config.grant_tokens - # Catch any boto3 errors and normalize to expected EncryptKeyError - try: - response = self.config.client.encrypt(**kms_params) - ciphertext = response["CiphertextBlob"] - key_id = response["KeyId"] - except (ClientError, KeyError): - error_message = "Master Key {key_id} unable to encrypt data key".format(key_id=self._key_id) - _LOGGER.exception(error_message) - raise EncryptKeyError(error_message) - return EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=self.provider_id, key_info=key_id), encrypted_data_key=ciphertext - ) - - def _decrypt_data_key(self, encrypted_data_key, algorithm, encryption_context=None): - """Decrypts an encrypted data key and returns the plaintext. + def validate_config(self): + """Validates the provided configuration.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //# In discovery mode + # //# if a default MRK Region is not configured the AWS SDK Default Region + # //# MUST be used. + if not self.config.discovery_region: + if not self.default_region: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //# If an AWS SDK Default Region can not be obtained + # //# initialization MUST fail. + raise ConfigMismatchError( + "Failed to determine default discovery region; please provide an explicit discovery_region" + ) + self.config.discovery_region = self.default_region - :param data_key: Encrypted data key - :type data_key: aws_encryption_sdk.structures.EncryptedDataKey - :type algorithm: `aws_encryption_sdk.identifiers.Algorithm` (not used for KMS) - :param dict encryption_context: Encryption context to use in decryption - :returns: Decrypted data key - :rtype: aws_encryption_sdk.structures.DataKey - :raises DecryptKeyError: if Master Key is unable to decrypt data key + def _new_master_key_impl(self, key_id): + """Creation of new master keys. Compared to the base class, this class has smarts to use either the configured + discovery region or, if not present, the default SDK region, to create new keys. """ - edk_key_id = to_str(encrypted_data_key.key_provider.key_info) - if edk_key_id != self._key_id: - raise DecryptKeyError( - "Cannot decrypt EDK wrapped by key_id={}, because it does not match this " - "provider's key_id={}".format(edk_key_id, self._key_id) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //# In discovery mode, the requested + # //# AWS KMS key identifier MUST be a well formed AWS KMS ARN. + _key_id = to_str(key_id) + arn = arn_from_str(_key_id) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //# In discovery mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- + # //# master-key.md) MUST be returned configured with + # Note that in the MRK case we ensure the key ID passed along has the discovery region, + # and in both cases _client(...) will ensure that a client is created that matches the key's region. + + if not arn.resource_id.startswith("mrk"): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //# Otherwise if the requested AWS KMS key + # //# identifier is identified as a multi-Region key (aws-kms-key- + # //# arn.md#identifying-an-aws-kms-multi-region-key), then AWS Region MUST + # //# be the region from the AWS KMS key ARN stored in the provider info + # //# from the encrypted data key. + # Note that this could return a normal KMSMasterKey and retain the same behavior, + # however we opt to follow the spec here in order to bias towards consistency between + # implementations. + return MRKAwareKMSMasterKey( + config=MRKAwareKMSMasterKeyConfig( + key_id=_key_id, client=self._client(_key_id), grant_tokens=self.config.grant_tokens + ) ) - - kms_params = {"CiphertextBlob": encrypted_data_key.encrypted_data_key, "KeyId": edk_key_id} - if encryption_context: - kms_params["EncryptionContext"] = encryption_context - if self.config.grant_tokens: - kms_params["GrantTokens"] = self.config.grant_tokens - # Catch any boto3 errors and normalize to expected DecryptKeyError - try: - response = self.config.client.decrypt(**kms_params) - - returned_key_id = response["KeyId"] - if returned_key_id != edk_key_id: - error_message = "AWS KMS returned unexpected key_id {returned} (expected {key_id})".format( - returned=returned_key_id, key_id=edk_key_id + else: + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //# Otherwise if the mode is discovery then + # //# the AWS Region MUST be the discovery MRK region. + arn.region = self.config.discovery_region + new_key_id = arn.to_string() + + return MRKAwareKMSMasterKey( + config=MRKAwareKMSMasterKeyConfig( + key_id=new_key_id, client=self._client(new_key_id), grant_tokens=self.config.grant_tokens ) - _LOGGER.exception(error_message) - raise DecryptKeyError(error_message) - plaintext = response["Plaintext"] - except (ClientError, KeyError): - error_message = "Master Key {key_id} unable to decrypt data key".format(key_id=self._key_id) - _LOGGER.exception(error_message) - raise DecryptKeyError(error_message) - return DataKey( - key_provider=self.key_provider, data_key=plaintext, encrypted_data_key=encrypted_data_key.encrypted_data_key - ) + ) diff --git a/test/integration/README.rst b/test/integration/README.rst index 33ecbbedd..5e3891193 100644 --- a/test/integration/README.rst +++ b/test/integration/README.rst @@ -5,8 +5,17 @@ aws-encryption-sdk Integration Tests In order to run these integration tests successfully, these things must be configured. #. Ensure that AWS credentials are available in one of the `automatically discoverable credential locations`_. -#. Set environment variable ``AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID`` to valid - `AWS KMS key id`_ to use for integration tests. +#. Set environment the following environment variables to valid + `AWS KMS key id`_ to use for integration tests: + + * AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID + * AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2 + +#. Set environment the following environment variables to two related + AWS KMS Multi-Region key ids in different regions to use for integration tests: + + * AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1 + * AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2 .. _automatically discoverable credential locations: http://boto3.readthedocs.io/en/latest/guide/configuration.html .. _AWS KMS key id: http://docs.aws.amazon.com/kms/latest/APIReference/API_Encrypt.html diff --git a/test/integration/integration_test_utils.py b/test/integration/integration_test_utils.py index 228439450..5d40f8124 100644 --- a/test/integration/integration_test_utils.py +++ b/test/integration/integration_test_utils.py @@ -19,6 +19,8 @@ AWS_KMS_KEY_ID = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID" AWS_KMS_KEY_ID_2 = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2" +AWS_KMS_MRK_KEY_ID_1 = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1" +AWS_KMS_MRK_KEY_ID_2 = "AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2" _KMS_MKP = None _KMS_MKP_BOTO = None @@ -46,6 +48,16 @@ def get_second_cmk_arn(): return _get_single_cmk_arn(AWS_KMS_KEY_ID_2) +def get_mrk_arn(): + """Retrieves the target AWS KMS CMK ARN from environment variable.""" + return _get_single_cmk_arn(AWS_KMS_MRK_KEY_ID_1) + + +def get_second_mrk_arn(): + """Retrieves the target AWS KMS CMK ARN from environment variable.""" + return _get_single_cmk_arn(AWS_KMS_MRK_KEY_ID_2) + + def setup_kms_master_key_provider(cache=True): """Reads the test_values config file and builds the requested KMS Master Key Provider.""" global _KMS_MKP # pylint: disable=global-statement diff --git a/test/unit/test_arn.py b/test/unit/test_arn.py index bdb796f2e..141be7322 100644 --- a/test/unit/test_arn.py +++ b/test/unit/test_arn.py @@ -14,49 +14,134 @@ import pytest from aws_encryption_sdk.exceptions import MalformedArnError -from aws_encryption_sdk.internal.arn import Arn +from aws_encryption_sdk.internal.arn import arn_from_str, is_valid_mrk_arn_str, is_valid_mrk_identifier + +pytestmark = [pytest.mark.unit, pytest.mark.local] + +VALID_KMS_ARNS = [ + "arn:aws:kms:us-west-2:123456789012:key/12345678", + "arn:aws:kms:us-west-2:123456789012:key/mrk-12345678", + "arn:aws:kms:us-west-2:123456789012:alias/myAlias", +] + +VALID_KMS_IDENTIFIERS = [ + "alias/myAlias", + "12345678", + "mrk-12345678", +] + VALID_KMS_ARNS + +INVALID_KMS_IDENTIFIERS = [ + "arn::kms:us-west-2:123456789012:key/12345678", + "arn:aws:not-kms:us-west-2:123456789012:key/12345678", + "arn:aws:kms::123456789012:key/12345678", + "arn:aws:kms:us-west-2::key/12345678", + "arn:aws:kms:us-west-2:123456789012:", + "arn:aws:kms:us-west-2:123456789012:key/", + "arn:aws:kms:us-west-2:123456789012:alias/", + "arn:aws:kms:us-west-2:123456789012:/12345678", + "arn:aws:kms:us-west-2:123456789012:key:12345678", + "arn:aws:kms:us-west-2:123456789012:alias:myAlias", +] + +INVALID_KMS_ARNS = [ + ":aws:kms:us-west-2:123456789012:key/12345678", +] + INVALID_KMS_IDENTIFIERS class TestArn(object): def test_malformed_arn_missing_arn(self): - arn = "aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //= type=test + # //# MUST start with string "arn" + arn = ":aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" with pytest.raises(MalformedArnError) as excinfo: - Arn.from_str(arn) + arn_from_str(arn) excinfo.match("Resource {} could not be parsed as an ARN".format(arn)) + excinfo.match("Missing 'arn' string") - def test_malformed_arn_missing_service(self): - arn = "aws:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + def test_parse_key_arn_missing_partition(self): + arn = "arn::kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //= type=test + # //# The partition MUST be a non-empty with pytest.raises(MalformedArnError) as excinfo: - Arn.from_str(arn) + arn_from_str(arn) excinfo.match("Resource {} could not be parsed as an ARN".format(arn)) + excinfo.match("Missing partition") + + def test_malformed_arn_service_not_kms(self): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //= type=test + # //# The service MUST be the string "kms" + arn = "arn:aws:notkms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + + with pytest.raises(MalformedArnError) as excinfo: + arn_from_str(arn) + excinfo.match("Resource {} could not be parsed as an ARN".format(arn)) + excinfo.match("Unknown service") def test_malformed_arn_missing_region(self): - arn = "arn:aws:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //= type=test + # //# The region MUST be a non-empty string + arn = "arn:aws:kms::222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" with pytest.raises(MalformedArnError) as excinfo: - Arn.from_str(arn) + arn_from_str(arn) excinfo.match("Resource {} could not be parsed as an ARN".format(arn)) + excinfo.match("Missing region") def test_malformed_arn_missing_account(self): - arn = "arn:aws:us-east-1:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //= type=test + # //# The account MUST be a non-empty string + arn = "arn:aws:kms:us-east-1::key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" with pytest.raises(MalformedArnError) as excinfo: - Arn.from_str(arn) + arn_from_str(arn) excinfo.match("Resource {} could not be parsed as an ARN".format(arn)) + excinfo.match("Missing account") def test_malformed_arn_missing_resource_type(self): - arn = "arn:aws:us-east-1:222222222222" + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //= type=test + # //# The resource section MUST be non-empty and MUST be split by a + # //# single "/" any additional "/" are included in the resource id + arn = "arn:aws:kms:us-east-1:222222222222:" + + with pytest.raises(MalformedArnError) as excinfo: + arn_from_str(arn) + excinfo.match("Resource {} could not be parsed as an ARN".format(arn)) + excinfo.match("Missing resource") + + def test_malformed_arn_unknown_resource_type(self): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //= type=test + # //# The resource type MUST be either "alias" or "key" + arn = "arn:aws:kms:us-east-1:222222222222:s3bucket/foo" + + with pytest.raises(MalformedArnError) as excinfo: + arn_from_str(arn) + excinfo.match("Resource {} could not be parsed as an ARN".format(arn)) + excinfo.match("Unknown resource type") + + def test_malformed_arn_missing_resource_id(self): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.5 + # //= type=test + # //# The resource id MUST be a non-empty string + arn = "arn:aws:kms:us-east-1:222222222222:key/" with pytest.raises(MalformedArnError) as excinfo: - Arn.from_str(arn) + arn_from_str(arn) excinfo.match("Resource {} could not be parsed as an ARN".format(arn)) + excinfo.match("Missing resource id") def test_parse_key_arn_success(self): arn_str = "arn:aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" - arn = Arn.from_str(arn_str) + arn = arn_from_str(arn_str) assert arn.partition == "aws" assert arn.service == "kms" @@ -68,7 +153,7 @@ def test_parse_key_arn_success(self): def test_parse_alias_arn_success(self): arn_str = "arn:aws:kms:us-east-1:222222222222:alias/aws/service" - arn = Arn.from_str(arn_str) + arn = arn_from_str(arn_str) assert arn.partition == "aws" assert arn.service == "kms" @@ -76,3 +161,123 @@ def test_parse_alias_arn_success(self): assert arn.account_id == "222222222222" assert arn.resource_type == "alias" assert arn.resource_id == "aws/service" + + def test_arn_round_trip_key_id(self): + arn_str = "arn:aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + arn = arn_from_str(arn_str) + arn_str_2 = arn.to_string() + + assert arn_str == arn_str_2 + + def test_arn_round_trip_alias(self): + arn_str = "arn:aws:kms:us-east-1:222222222222:alias/aws/service" + arn = arn_from_str(arn_str) + arn_str_2 = arn.to_string() + + assert arn_str == arn_str_2 + + def test_mrk_arn_is_valid_mrk(self): + arn_str = "arn:aws:kms:us-east-1:222222222222:key/mrk-1234-5678-9012-34567890" + + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //= type=test + # //# This function MUST take a single AWS KMS ARN + + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //= type=test + # //# If resource type is "key" and resource ID starts with + # //# "mrk-", this is a AWS KMS multi-Region key ARN and MUST return true. + assert is_valid_mrk_arn_str(arn_str) + + def test_non_mrk_arn_is_not_valid_mrk(self): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //= type=test + # //# If resource type is "key" and resource ID does not start with "mrk-", + # //# this is a (single-region) AWS KMS key ARN and MUST return false. + arn_str = "arn:aws:kms:us-east-1:222222222222:key/1234-5678-9012-34567890" + + assert not is_valid_mrk_arn_str(arn_str) + + def test_alias_arn_is_not_valid_mrk(self): + arn_str = "arn:aws:kms:us-east-1:222222222222:alias/mrk-1234-5678-9012-34567890" + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //= type=test + # //# If resource type is "alias", this is an AWS KMS alias ARN and MUST + # //# return false. + assert not is_valid_mrk_arn_str(arn_str) + + @pytest.mark.parametrize( + "key_id", + INVALID_KMS_ARNS, + ) + def test_is_valid_mrk_arn_str_throw_on_invalid_arn(self, key_id): + with pytest.raises(MalformedArnError) as excinfo: + is_valid_mrk_arn_str(key_id) + excinfo.match("Resource {} could not be parsed as an ARN".format(key_id)) + + def test_is_valid_mrk_arn_str_throw_on_bare_id(self): + arn_str = "mrk-1234-5678-9012-34567890" + + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.8 + # //= type=test + # //# If the input is an invalid AWS KMS ARN this function MUST error. + with pytest.raises(MalformedArnError) as excinfo: + is_valid_mrk_arn_str(arn_str) + excinfo.match("Resource {} could not be parsed as an ARN".format(arn_str)) + excinfo.match("Missing 'arn' string") + + def test_mrk_arn_is_valid_mrk_identifier(self): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //= type=test + # //# This function MUST take a single AWS KMS identifier + + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //= type=test + # //# If the input starts with "arn:", this MUST return the output of + # //# identifying an an AWS KMS multi-Region ARN (aws-kms-key- + # //# arn.md#identifying-an-an-aws-kms-multi-region-arn) called with this + # //# input. + id_str = "arn:aws:kms:us-east-1:222222222222:key/mrk-1234-5678-9012-34567890" + assert is_valid_mrk_identifier(id_str) + + def test_bare_mrk_id_is_valid_mrk_identifier(self): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //= type=test + # //# If the input starts with "mrk-", this is a multi-Region key id and + # //# MUST return true. + id_str = "mrk-1234-5678-9012-34567890" + assert is_valid_mrk_identifier(id_str) + + def test_alias_arn_is_not_valid_mrk_identifier(self): + id_str = "arn:aws:kms:us-east-1:222222222222:alias/myAlias" + assert not is_valid_mrk_identifier(id_str) + + def test_bare_alias_is_not_valid_mrk_identifier(self): + id_str = "alias/myAlias" + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //= type=test + # //# If the input starts with "alias/", this an AWS KMS alias and not a + # //# multi-Region key id and MUST return false. + assert not is_valid_mrk_identifier(id_str) + + def test_bare_srk_id_is_not_valid_mrk_identifier(self): + # //= compliance/framework/aws-kms/aws-kms-key-arn.txt#2.9 + # //= type=test + # //# If + # //# the input does not start with any of the above, this is a multi- + # //# Region key id and MUST return false. + id_str = "1234-5678-9012-34567890" + assert not is_valid_mrk_identifier(id_str) + + def test_srk_arn_is_not_valid_mrk_identifier(self): + id_str = "arn:aws:kms:us-east-1:222222222222:key/1234-5678-9012-34567890" + assert not is_valid_mrk_identifier(id_str) + + @pytest.mark.parametrize( + "key_id", + INVALID_KMS_ARNS, + ) + def test_is_not_valid_mrk_identifier_throws_on_invalid_arn(self, key_id): + with pytest.raises(MalformedArnError) as excinfo: + is_valid_mrk_arn_str(key_id) + excinfo.match("Resource {} could not be parsed as an ARN".format(key_id)) diff --git a/test/unit/test_providers_base_master_key.py b/test/unit/test_providers_base_master_key.py index de8280ba0..86c290196 100644 --- a/test/unit/test_providers_base_master_key.py +++ b/test/unit/test_providers_base_master_key.py @@ -263,6 +263,12 @@ def test_generate_data_key(self): mock_master_key._generate_data_key.assert_called_once_with( algorithm=ALGORITHM, encryption_context=VALUES["encryption_context"] ) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //= type=test + # //# If the call succeeds the AWS KMS Generate Data Key response's + # //# "Plaintext" MUST match the key derivation input length specified by + # //# the algorithm suite included in the input. + self.mock_data_key_len_check.assert_called_once_with( source_data_key=sentinel.new_raw_data_key, algorithm=ALGORITHM ) @@ -296,16 +302,28 @@ def test_decrypt_data_key(self): mock_master_key._key_check = MagicMock() mock_master_key._decrypt_data_key = MagicMock(return_value=sentinel.raw_decrypted_data_key) - mock_master_key.decrypt_data_key( + decrypted_data_key = mock_master_key.decrypt_data_key( encrypted_data_key=sentinel.encrypted_data_key, algorithm=ALGORITHM, encryption_context=VALUES["encryption_context"], ) + assert decrypted_data_key == sentinel.raw_decrypted_data_key + self.mock_data_key_len_check.assert_called_once_with( source_data_key=sentinel.raw_decrypted_data_key, algorithm=ALGORITHM ) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //= type=test + # //# To match the encrypted data key's + # //# provider ID MUST exactly match the value "aws-kms". mock_master_key._key_check.assert_called_once_with(sentinel.encrypted_data_key) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# If the AWS KMS response satisfies the requirements then it MUST be + # //# use and this function MUST return and not attempt to decrypt any more + # //# encrypted data keys. mock_master_key._decrypt_data_key.assert_called_once_with( encrypted_data_key=sentinel.encrypted_data_key, algorithm=ALGORITHM, diff --git a/test/unit/test_providers_base_master_key_provider.py b/test/unit/test_providers_base_master_key_provider.py index e27e724be..a4b4e3832 100644 --- a/test/unit/test_providers_base_master_key_provider.py +++ b/test/unit/test_providers_base_master_key_provider.py @@ -240,6 +240,12 @@ def test_master_key_for_decrypt(self): ) mock_master_key_provider._new_master_key = MagicMock(return_value=sentinel.new_master_key) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //= type=test + # //# For each encrypted data key in the filtered set, one at a time, the + # //# master key provider MUST call Get Master Key (aws-kms-mrk-aware- + # //# master-key-provider.md#get-master-key) with the encrypted data key's + # //# provider info as the AWS KMS key ARN. test = mock_master_key_provider.master_key_for_decrypt(sentinel.key_info) mock_master_key_provider._new_master_key.assert_called_once_with(sentinel.key_info) @@ -265,6 +271,12 @@ def test_decrypt_data_key_successful(self): encryption_context=sentinel.encryption_context, ) mock_member.master_key_for_decrypt.assert_called_once_with(sentinel.key_info) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //= type=test + # //# It MUST call Decrypt Data Key + # //# (aws-kms-mrk-aware-master-key.md#decrypt-data-key) on this master key + # //# with the input algorithm, this single encrypted data key, and the + # //# input encryption context. mock_master_key.decrypt_data_key.assert_called_once_with( mock_encrypted_data_key, sentinel.algorithm, sentinel.encryption_context ) @@ -537,6 +549,12 @@ def test_decrypt_data_key_from_list_first_try(self): algorithm=sentinel.algorithm, encryption_context=sentinel.encryption_context, ) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //= type=test + # //# If the decrypt data key call is + # //# successful, then this function MUST return this result and not + # //# attempt to decrypt any more encrypted data keys. mock_decrypt_data_key.assert_called_once_with( sentinel.encrypted_data_key_a, sentinel.algorithm, sentinel.encryption_context ) @@ -575,6 +593,11 @@ def test_decrypt_data_key_from_list_unsuccessful(self): ) mock_master_key_provider.decrypt_data_key = MagicMock() mock_master_key_provider.decrypt_data_key.side_effect = (DecryptKeyError, DecryptKeyError) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //= type=test + # //# If all the input encrypted data keys have been processed then this + # //# function MUST yield an error that includes all the collected errors. + # Note that Python does not collect errors with pytest.raises(DecryptKeyError) as excinfo: mock_master_key_provider.decrypt_data_key_from_list( encrypted_data_keys=[sentinel.encrypted_data_key_a, sentinel.encrypted_data_key_b], diff --git a/test/unit/test_providers_kms_master_key.py b/test/unit/test_providers_kms_master_key.py index 1795e6c84..be91c96c7 100644 --- a/test/unit/test_providers_kms_master_key.py +++ b/test/unit/test_providers_kms_master_key.py @@ -16,12 +16,21 @@ from botocore.exceptions import ClientError from mock import MagicMock, patch, sentinel -from aws_encryption_sdk.exceptions import DecryptKeyError, EncryptKeyError, GenerateKeyError +from aws_encryption_sdk.exceptions import DecryptKeyError, EncryptKeyError, GenerateKeyError, MalformedArnError from aws_encryption_sdk.identifiers import Algorithm +from aws_encryption_sdk.internal.arn import arn_from_str from aws_encryption_sdk.key_providers.base import MasterKey -from aws_encryption_sdk.key_providers.kms import KMSMasterKey, KMSMasterKeyConfig +from aws_encryption_sdk.key_providers.kms import ( + KMSMasterKey, + KMSMasterKeyConfig, + MRKAwareKMSMasterKey, + MRKAwareKMSMasterKeyConfig, + _check_mrk_arns_equal, + _key_resource_match, +) from aws_encryption_sdk.structures import DataKey, EncryptedDataKey, MasterKeyInfo +from .test_arn import INVALID_KMS_ARNS, INVALID_KMS_IDENTIFIERS, VALID_KMS_IDENTIFIERS from .test_values import VALUES pytestmark = [pytest.mark.unit, pytest.mark.local] @@ -41,7 +50,7 @@ def apply_fixture(self): self.mock_client.decrypt.return_value = {"Plaintext": VALUES["data_key"], "KeyId": VALUES["arn_str"]} self.mock_algorithm = MagicMock() self.mock_algorithm.__class__ = Algorithm - self.mock_algorithm.data_key_len = sentinel.data_key_len + self.mock_algorithm.data_key_len = 32 self.mock_algorithm.kdf_input_len = sentinel.kdf_input_len self.mock_data_key = MagicMock() self.mock_data_key.data_key = VALUES["data_key"] @@ -53,182 +62,811 @@ def apply_fixture(self): self.mock_data_key_len_check = self.mock_data_key_len_check_patcher.start() self.mock_grant_tokens = (sentinel.grant_token_1, sentinel.grant_token_2) - self.mock_kms_mkc_1 = KMSMasterKeyConfig(key_id=VALUES["arn"], client=self.mock_client) - self.mock_kms_mkc_2 = KMSMasterKeyConfig( - key_id=VALUES["arn"], client=self.mock_client, grant_tokens=self.mock_grant_tokens - ) - self.mock_kms_mkc_3 = KMSMasterKeyConfig(key_id="ex_key_info", client=self.mock_client) + + self.mrk_region1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-abcd123" + self.mrk_region2 = "arn:aws:kms:us-west-2:123456789012:key/mrk-abcd123" yield # Run tearDown self.mock_data_key_len_check_patcher.stop() - def test_parent(self): + def test_kms_parent(self): assert issubclass(KMSMasterKey, MasterKey) - def test_config_bare(self): - test = KMSMasterKeyConfig(key_id=VALUES["arn"], client=self.mock_client) + def test_mrkaware_parent(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.5 + # //= type=test + # //# MUST implement the Master Key Interface (../master-key- + # //# interface.md#interface) + assert issubclass(MRKAwareKMSMasterKey, KMSMasterKey) + + @pytest.mark.parametrize("config_class", (KMSMasterKeyConfig, MRKAwareKMSMasterKeyConfig)) + def test_config_bare(self, config_class): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //= type=test + # //# On initialization, the caller MUST provide: + test = config_class(key_id=VALUES["arn"], client=self.mock_client) assert test.client is self.mock_client assert test.grant_tokens == () - def test_config_grant_tokens(self): - test = KMSMasterKeyConfig(key_id=VALUES["arn"], client=self.mock_client, grant_tokens=self.mock_grant_tokens) + @pytest.mark.parametrize("config_class", (KMSMasterKeyConfig, MRKAwareKMSMasterKeyConfig)) + def test_config_keyid_required(self, config_class): + """Fail to instantiate config if missing keyid""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //= type=test + # //# The AWS KMS key identifier MUST NOT be null or empty. + with pytest.raises(TypeError): + config_class(client=self.mock_client) + + @pytest.mark.parametrize("config_class", (KMSMasterKeyConfig, MRKAwareKMSMasterKeyConfig)) + def test_config_grant_tokens(self, config_class): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //= type=test + # //# The master key MUST be able to be + # //# configured with an optional list of Grant Tokens. + test = config_class(key_id=VALUES["arn"], client=self.mock_client, grant_tokens=self.mock_grant_tokens) assert test.grant_tokens is self.mock_grant_tokens - def test_init(self): + def test_config_default_client(self): + """KMSMasterKeys do not require passing a client.""" + test = KMSMasterKeyConfig(key_id=VALUES["arn"]) + arn = arn_from_str(VALUES["arn_str"]) + assert test.client._client_config.region_name == arn.region + + def test_config_no_client_mrkaware(self): + """MRKAwareKMSMasterKeys require a client.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //= type=test + # //# The AWS KMS SDK client MUST not be null. + with pytest.raises(TypeError): + MRKAwareKMSMasterKeyConfig(key_id=VALUES["arn"]) + + @pytest.mark.parametrize( + "key_id", + VALID_KMS_IDENTIFIERS + + INVALID_KMS_IDENTIFIERS, # To maintain backwards compatibility can be initialized with bad identifiers + ) + def test_init_kms_master_key(self, key_id): self.mock_client.meta.config.user_agent_extra = sentinel.user_agent_extra - test = KMSMasterKey(config=self.mock_kms_mkc_1) - assert test._key_id == VALUES["arn"].decode("utf-8") - - def test_generate_data_key(self): - test = KMSMasterKey(config=self.mock_kms_mkc_3) + config = KMSMasterKeyConfig(key_id=key_id, client=self.mock_client) + test = KMSMasterKey(config=config) + assert test._key_id == key_id + + @pytest.mark.parametrize( + "key_id", + VALID_KMS_IDENTIFIERS, + ) + def test_init_mrk_kms_master_key(self, key_id): + self.mock_client.meta.config.user_agent_extra = sentinel.user_agent_extra + config = MRKAwareKMSMasterKeyConfig(key_id=key_id, client=self.mock_client) + test = MRKAwareKMSMasterKey(config=config) + assert test._key_id == key_id + + @pytest.mark.parametrize( + "key_id", + INVALID_KMS_IDENTIFIERS, + ) + def test_init_mrk_kms_master_key_invalid_id(self, key_id): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.6 + # //= type=test + # //# The AWS KMS + # //# key identifier MUST be a valid identifier (aws-kms-key-arn.md#a- + # //# valid-aws-kms-identifier). + self.mock_client.meta.config.user_agent_extra = sentinel.user_agent_extra + config = MRKAwareKMSMasterKeyConfig(key_id=key_id, client=self.mock_client) + with pytest.raises(MalformedArnError) as excinfo: + MRKAwareKMSMasterKey(config=config) + excinfo.match("Resource {key} could not be parsed as an ARN".format(key=key_id)) + + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_generate_data_key(self, config_class, key_class, key_id): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //= type=test + # //# The inputs MUST be the same as the Master Key Generate Data Key + # //# (../master-key-interface.md#generate-data-key) interface. + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) generated_key = test._generate_data_key(self.mock_algorithm) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //= type=test + # //# This master key MUST use the configured AWS KMS client to make an AWS KMS + # //# GenerateDatakey (https://docs.aws.amazon.com/kms/latest/APIReference/ + # //# API_GenerateDataKey.html) request constructed as follows: self.mock_client.generate_data_key.assert_called_once_with( - KeyId="ex_key_info", NumberOfBytes=sentinel.kdf_input_len + KeyId=key_id.decode("ascii"), NumberOfBytes=sentinel.kdf_input_len ) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //= type=test + # //# The output MUST be the same as the Master Key Generate Data Key + # //# (../master-key-interface.md#generate-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //= type=test + # //# The response's cipher text blob MUST be used as the returned as the + # //# ciphertext for the encrypted data key in the output. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //= type=test + # //# The response's "Plaintext" MUST be the plaintext in the output. assert generated_key == DataKey( key_provider=MasterKeyInfo(provider_id=test.provider_id, key_info=VALUES["arn"]), data_key=VALUES["data_key"], encrypted_data_key=VALUES["encrypted_data_key"], ) - def test_generate_data_key_with_encryption_context(self): - test = KMSMasterKey(config=self.mock_kms_mkc_1) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_generate_data_key_with_encryption_context(self, config_class, key_class, key_id): + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) test._generate_data_key(self.mock_algorithm, VALUES["encryption_context"]) self.mock_client.generate_data_key.assert_called_once_with( - KeyId=VALUES["arn_str"], + KeyId=key_id.decode("ascii"), NumberOfBytes=sentinel.kdf_input_len, EncryptionContext=VALUES["encryption_context"], ) - def test_generate_data_key_with_grant_tokens(self): - test = KMSMasterKey(config=self.mock_kms_mkc_2) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_generate_data_key_with_grant_tokens(self, config_class, key_class, key_id): + config = config_class(key_id=key_id, client=self.mock_client, grant_tokens=self.mock_grant_tokens) + test = key_class(config=config) test._generate_data_key(self.mock_algorithm) self.mock_client.generate_data_key.assert_called_once_with( - KeyId=VALUES["arn_str"], NumberOfBytes=sentinel.kdf_input_len, GrantTokens=self.mock_grant_tokens + KeyId=key_id.decode("ascii"), NumberOfBytes=sentinel.kdf_input_len, GrantTokens=self.mock_grant_tokens ) - def test_generate_data_key_unsuccessful_clienterror(self): + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_generate_data_key_unsuccessful_clienterror(self, config_class, key_class, key_id): self.mock_client.generate_data_key.side_effect = ClientError({"Error": {}}, "This is an error!") - test = KMSMasterKey(config=self.mock_kms_mkc_3) + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) with pytest.raises(GenerateKeyError) as excinfo: test._generate_data_key(self.mock_algorithm) excinfo.match("Master Key .* unable to generate data key") - def test_generate_data_key_unsuccessful_keyerror(self): + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_generate_data_key_unsuccessful_keyerror(self, config_class, key_class, key_id): self.mock_client.generate_data_key.side_effect = KeyError - test = KMSMasterKey(config=self.mock_kms_mkc_3) + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) with pytest.raises(GenerateKeyError) as excinfo: test._generate_data_key(self.mock_algorithm) excinfo.match("Master Key .* unable to generate data key") - def test_encrypt_data_key(self): - test = KMSMasterKey(config=self.mock_kms_mkc_3) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_generate_data_key_unsuccessful_response_invalid_key_id(self, config_class, key_class, key_id): + """Check that we fail if KMS returns a response with an invalid keyid.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.10 + # //= type=test + # //# The response's "KeyId" MUST be valid. + invalid_key_id = "Not:an/arn" + self.mock_client.generate_data_key.return_value = { + "Plaintext": VALUES["data_key"], + "CiphertextBlob": VALUES["encrypted_data_key"], + "KeyId": invalid_key_id, + } + + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + + with pytest.raises(GenerateKeyError) as excinfo: + test._generate_data_key(algorithm=self.mock_algorithm) + excinfo.match("Retrieved an unexpected KeyID in response from KMS") + + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_encrypt_data_key(self, config_class, key_class, key_id): + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_client.encrypt.return_value["KeyId"] = key_id + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //= type=test + # //# The inputs MUST be the same as the Master Key Encrypt Data Key + # //# (../master-key-interface.md#encrypt-data-key) interface. encrypted_key = test._encrypt_data_key(self.mock_data_key, self.mock_algorithm) - self.mock_client.encrypt.assert_called_once_with(KeyId="ex_key_info", Plaintext=VALUES["data_key"]) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //= type=test + # //# The master key MUST use the configured AWS KMS client to make an AWS KMS Encrypt + # //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + # //# API_Encrypt.html) request constructed as follows: + self.mock_client.encrypt.assert_called_once_with(KeyId=key_id.decode("ascii"), Plaintext=VALUES["data_key"]) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //= type=test + # //# The response's cipher text blob MUST be used as the "ciphertext" for the + # //# encrypted data key. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //= type=test + # //# The output MUST be the same as the Master Key Encrypt Data Key + # //# (../master-key-interface.md#encrypt-data-key) interface. assert encrypted_key == EncryptedDataKey( - key_provider=MasterKeyInfo(provider_id=test.provider_id, key_info=VALUES["arn"]), + key_provider=MasterKeyInfo(provider_id=test.provider_id, key_info=key_id.decode("ascii")), encrypted_data_key=VALUES["encrypted_data_key"], ) - def test_encrypt_data_key_with_encryption_context(self): - test = KMSMasterKey(config=self.mock_kms_mkc_1) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_encrypt_data_key_with_encryption_context(self, config_class, key_class, key_id): + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_client.encrypt.return_value["KeyId"] = key_id test._encrypt_data_key(self.mock_data_key, self.mock_algorithm, VALUES["encryption_context"]) self.mock_client.encrypt.assert_called_once_with( - KeyId=VALUES["arn_str"], Plaintext=VALUES["data_key"], EncryptionContext=VALUES["encryption_context"] + KeyId=key_id.decode("ascii"), Plaintext=VALUES["data_key"], EncryptionContext=VALUES["encryption_context"] ) - def test_encrypt_data_key_with_grant_tokens(self): - test = KMSMasterKey(config=self.mock_kms_mkc_2) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_encrypt_data_key_with_grant_tokens(self, config_class, key_class, key_id): + config = config_class(key_id=key_id, client=self.mock_client, grant_tokens=self.mock_grant_tokens) + test = key_class(config=config) + self.mock_client.encrypt.return_value["KeyId"] = key_id test._encrypt_data_key(self.mock_data_key, self.mock_algorithm) self.mock_client.encrypt.assert_called_once_with( - KeyId=VALUES["arn_str"], Plaintext=VALUES["data_key"], GrantTokens=self.mock_grant_tokens + KeyId=key_id.decode("ascii"), Plaintext=VALUES["data_key"], GrantTokens=self.mock_grant_tokens ) - def test_encrypt_data_key_unsuccessful_clienterror(self): + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_encrypt_data_key_unsuccessful_clienterror(self, config_class, key_class, key_id): self.mock_client.encrypt.side_effect = ClientError({"Error": {}}, "This is an error!") - test = KMSMasterKey(config=self.mock_kms_mkc_3) + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) with pytest.raises(EncryptKeyError) as excinfo: test._encrypt_data_key(self.mock_data_key, self.mock_algorithm) excinfo.match("Master Key .* unable to encrypt data key") - def test_encrypt_data_key_unsuccessful_keyerror(self): + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_encrypt_data_key_unsuccessful_keyerror(self, config_class, key_class, key_id): self.mock_client.encrypt.side_effect = KeyError - test = KMSMasterKey(config=self.mock_kms_mkc_3) + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) with pytest.raises(EncryptKeyError) as excinfo: test._encrypt_data_key(self.mock_data_key, self.mock_algorithm) excinfo.match("Master Key .* unable to encrypt data key") - def test_decrypt_data_key(self): - test = KMSMasterKey(config=self.mock_kms_mkc_1) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_encrypt_data_key_unsuccessful_response_invalid_key_id(self, config_class, key_class, key_id): + """Check that we fail if KMS returns a response with an invalid keyid.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.11 + # //= type=test + # //# The AWS KMS Encrypt response MUST contain a valid "KeyId". + invalid_key_id = "Not:an/arn" + self.mock_client.encrypt.return_value = { + "Plaintext": VALUES["data_key"], + "CiphertextBlob": VALUES["encrypted_data_key"], + "KeyId": invalid_key_id, + } + + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + + with pytest.raises(EncryptKeyError) as excinfo: + test._encrypt_data_key(self.mock_data_key, self.mock_algorithm) + excinfo.match("Retrieved an unexpected KeyID in response from KMS") + + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key(self, config_class, key_class, key_id): + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + self.mock_client.decrypt.return_value["KeyId"] = key_id.decode("ascii") + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# The inputs MUST be the same as the Master Key Decrypt Data Key + # //# (../master-key-interface.md#decrypt-data-key) interface. decrypted_key = test._decrypt_data_key( - encrypted_data_key=self.mock_encrypted_data_key, algorithm=sentinel.algorithm + encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm ) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# To decrypt the encrypted data key this master key MUST use the + # //# configured AWS KMS client to make an AWS KMS Decrypt + # //# (https://docs.aws.amazon.com/kms/latest/APIReference/ + # //# API_Decrypt.html) request constructed as follows: self.mock_client.decrypt.assert_called_once_with( - CiphertextBlob=VALUES["encrypted_data_key"], KeyId=VALUES["arn_str"] + CiphertextBlob=VALUES["encrypted_data_key"], KeyId=key_id.decode("ascii") ) assert decrypted_key == DataKey( key_provider=test.key_provider, data_key=VALUES["data_key"], encrypted_data_key=VALUES["encrypted_data_key"] ) - def test_decrypt_data_key_with_encryption_context(self): - test = KMSMasterKey(config=self.mock_kms_mkc_1) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", INVALID_KMS_ARNS) + def test_decrypt_data_key_invalid_arn_edk(self, config_class, key_class, key_id): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# Additionally each provider info MUST be a valid AWS KMS ARN + # //# (aws-kms-key-arn.md#a-valid-aws-kms-arn) with a resource type of + # //# "key". + config = config_class(key_id=VALUES["arn"], client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + self.mock_client.decrypt.return_value["KeyId"] = key_id + with pytest.raises(MalformedArnError) as excinfo: + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) + excinfo.match("Resource {key_id} could not be parsed as an ARN".format(key_id=key_id)) + + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + def test_decrypt_data_key_alias_arn_edk(self, config_class, key_class): + key_id = "arn:aws:kms:us-east-1:248168362296:alias/myAlias" + config = config_class(key_id=VALUES["arn"], client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + self.mock_client.decrypt.return_value["KeyId"] = key_id + with pytest.raises(DecryptKeyError) as excinfo: + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) + excinfo.match("AWS KMS Provider EDK contains unexpected key_id: {key_id}".format(key_id=key_id)) + + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key_with_encryption_context(self, config_class, key_class, key_id): + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + self.mock_client.decrypt.return_value["KeyId"] = key_id.decode("ascii") test._decrypt_data_key( encrypted_data_key=self.mock_encrypted_data_key, - algorithm=sentinel.algorithm, + algorithm=self.mock_algorithm, encryption_context=VALUES["encryption_context"], ) self.mock_client.decrypt.assert_called_once_with( CiphertextBlob=VALUES["encrypted_data_key"], EncryptionContext=VALUES["encryption_context"], - KeyId=VALUES["arn_str"], + KeyId=key_id.decode("ascii"), ) - def test_decrypt_data_key_with_grant_tokens(self): - test = KMSMasterKey(config=self.mock_kms_mkc_2) - test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=sentinel.algorithm) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key_with_grant_tokens(self, config_class, key_class, key_id): + config = config_class(key_id=key_id, client=self.mock_client, grant_tokens=self.mock_grant_tokens) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + self.mock_client.decrypt.return_value["KeyId"] = key_id.decode("ascii") + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) self.mock_client.decrypt.assert_called_once_with( - CiphertextBlob=VALUES["encrypted_data_key"], GrantTokens=self.mock_grant_tokens, KeyId=VALUES["arn_str"] + CiphertextBlob=VALUES["encrypted_data_key"], + GrantTokens=self.mock_grant_tokens, + KeyId=key_id.decode("ascii"), ) - def test_decrypt_data_key_unsuccessful_clienterror(self): + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key_unsuccessful_clienterror(self, config_class, key_class, key_id): self.mock_client.decrypt.side_effect = ClientError({"Error": {}}, "This is an error!") - test = KMSMasterKey(config=self.mock_kms_mkc_1) + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + self.mock_client.decrypt.return_value["KeyId"] = key_id.decode("ascii") with pytest.raises(DecryptKeyError) as excinfo: test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=sentinel.algorithm) excinfo.match("Master Key .* unable to decrypt data key") - def test_decrypt_data_key_unsuccessful_keyerror(self): + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key_unsuccessful_keyerror(self, config_class, key_class, key_id): self.mock_client.decrypt.side_effect = KeyError - test = KMSMasterKey(config=self.mock_kms_mkc_1) + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + self.mock_client.decrypt.return_value["KeyId"] = key_id.decode("ascii") with pytest.raises(DecryptKeyError) as excinfo: test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=sentinel.algorithm) excinfo.match("Master Key .* unable to decrypt data key") - def test_decrypt_data_key_unsuccessful_key_id_does_not_match_edk(self): - test = KMSMasterKey(config=self.mock_kms_mkc_3) + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key_unsuccessful_response_missing_key_id(self, config_class, key_class, key_id): + """Check that we fail if KMS returns a response without a keyid.""" + self.mock_client.decrypt.return_value = {"Plaintext": VALUES["data_key"]} + + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id with pytest.raises(DecryptKeyError) as excinfo: test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=sentinel.algorithm) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# If all the input encrypted data keys have been processed then this + # //# function MUST yield an error that includes all the collected errors. + # Note the latter half of "includes all collected errors" is not satisfied + excinfo.match("Master Key .* unable to decrypt data key") + + self.mock_client.decrypt.assert_called_once_with( + CiphertextBlob=VALUES["encrypted_data_key"], KeyId=key_id.decode("ascii") + ) + + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key_unsuccessful_incorrect_plaintext_length(self, config_class, key_class, key_id): + """Check that we fail if KMS returns a plaintext of an unexpected length.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# The response's "Plaintext"'s length MUST equal the length + # //# required by the requested algorithm suite otherwise the function MUST + # //# collect an error. + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_algorithm.data_key_len = 128 + self.mock_encrypted_data_key.key_provider.key_info = key_id + self.mock_client.decrypt.return_value["KeyId"] = key_id.decode("ascii") + + with pytest.raises(DecryptKeyError) as excinfo: + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) + excinfo.match("Plaintext length .* does not match algorithm's expected length") + + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key_unsuccessful_mismatched_key_id(self, config_class, key_class, key_id): + """For all keys, if KMS returns a completely different key id we should fail to decrypt.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# If the call succeeds then the response's "KeyId" MUST be equal to the + # //# configured AWS KMS key identifier otherwise the function MUST collect + # //# an error. + mismatched_key_id = key_id.decode("ascii") + "-test" + self.mock_client.decrypt.return_value = {"Plaintext": VALUES["data_key"], "KeyId": mismatched_key_id} + + config = config_class(key_id=key_id, client=self.mock_client) + test = key_class(config=config) + self.mock_encrypted_data_key.key_provider.key_info = key_id + + with pytest.raises(DecryptKeyError) as excinfo: + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) + excinfo.match("AWS KMS returned unexpected key_id") + + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + @pytest.mark.parametrize("key_id", [VALUES["mrk_arn_region1"], VALUES["arn"]]) + def test_decrypt_data_key_unsuccessful_key_id_does_not_match_edk(self, config_class, key_class, key_id): + """For all keys, if the configured key id is a complete mismatch from the EDK key, we should fail to decrypt.""" + mismatched_key_id = key_id.decode("ascii") + "-test" + config = config_class(key_id=mismatched_key_id, client=self.mock_client) + test = key_class(config=config) + with pytest.raises(DecryptKeyError) as excinfo: + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) excinfo.match("does not match this provider's key_id") self.mock_client.assert_not_called() - def test_decrypt_data_key_unsuccessful_response_missing_key_id(self): - self.mock_client.decrypt.return_value = {"Plaintext": VALUES["data_key"]} + @pytest.mark.parametrize( + "config_class, key_class", + [(KMSMasterKeyConfig, KMSMasterKey), (MRKAwareKMSMasterKeyConfig, MRKAwareKMSMasterKey)], + ) + def test_decrypt_data_key_unsuccessful_srks_different_region(self, config_class, key_class): + """For SRKs, if the configured key id is identical to the EDK key except for region, no provider should treat + them as equivalent (since they are SRKs). This is a slightly more specific case than the general "mismatched + key id" in a previous test. + + Note that the chances of having two identical SRK key ids from different regions is tiny, but we should handle + the case anyway.""" + key_id1 = VALUES["arn"] + arn = arn_from_str(VALUES["arn_str"]) + arn.region = "ap-southeast-1" + key_id2 = arn.to_string() + + # Config uses the first SRK + config = config_class(key_id=key_id1, client=self.mock_client) + test = key_class(config=config) + + # EDK contains the second SRK + self.mock_encrypted_data_key.key_provider.key_info = key_id2 - test = KMSMasterKey(config=self.mock_kms_mkc_1) with pytest.raises(DecryptKeyError) as excinfo: - test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=sentinel.algorithm) - excinfo.match("Master Key .* unable to decrypt data key") + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) + excinfo.match("Cannot decrypt EDK wrapped by .*, because it does not match this provider") + + def test_decrypt_data_key_successful_mrk_provider_different_regions(self): + """For MRK-aware key providers, we should successfully decrypt using a related MRK.""" + # Config and KMS use the MRK in region 1 + config = MRKAwareKMSMasterKeyConfig(key_id=VALUES["mrk_arn_region1"], client=self.mock_client) + test = MRKAwareKMSMasterKey(config=config) + self.mock_client.decrypt.return_value = { + "Plaintext": VALUES["data_key"], + "KeyId": VALUES["mrk_arn_region1_str"], + } + + # EDK contains the related MRK in region 2 + self.mock_encrypted_data_key.key_provider.key_info = VALUES["mrk_arn_region2"] + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) self.mock_client.decrypt.assert_called_once_with( - CiphertextBlob=VALUES["encrypted_data_key"], KeyId=VALUES["arn_str"] + CiphertextBlob=VALUES["encrypted_data_key"], KeyId=VALUES["mrk_arn_region1_str"] ) - def test_decrypt_data_key_unsuccessful_mismatched_key_id(self): - mismatched_key_id = VALUES["arn_str"] + "-test" - self.mock_client.decrypt.return_value = {"Plaintext": VALUES["data_key"], "KeyId": mismatched_key_id} + def test_decrypt_data_key_unsuccessful_non_mrk_provider_different_region(self): + """For non MRK-aware key providers, related MRKs are not treated as equivalent and decryption should fail.""" + # Config uses the MRK in region 1 + config = KMSMasterKeyConfig(key_id=VALUES["mrk_arn_region1"], client=self.mock_client) + test = KMSMasterKey(config=config) + + # EDK contains the related MRK in region 2 + self.mock_encrypted_data_key.key_provider.key_info = VALUES["mrk_arn_region2"] + + with pytest.raises(DecryptKeyError) as excinfo: + test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=self.mock_algorithm) + excinfo.match("Cannot decrypt EDK wrapped by .*, because it does not match this provider") + + def test_decrypt_data_key_failure_kms_returns_wrong_mrk(self): + """For an MRK-aware provider, if KMS returns the MRK from the EDK rather than the MRK we called it with, + we should fail.""" + # Config uses MRK for region 1 + config = MRKAwareKMSMasterKeyConfig(key_id=self.mrk_region1, client=self.mock_client) + test = MRKAwareKMSMasterKey(config=config) + + # KMS returns the MRK for region 2 + self.mock_client.decrypt.return_value = {"Plaintext": VALUES["data_key"], "KeyId": self.mrk_region2} + self.mock_encrypted_data_key.key_provider.key_info = self.mrk_region2 - test = KMSMasterKey(config=self.mock_kms_mkc_1) with pytest.raises(DecryptKeyError) as excinfo: test._decrypt_data_key(encrypted_data_key=self.mock_encrypted_data_key, algorithm=sentinel.algorithm) excinfo.match("AWS KMS returned unexpected key_id") + + def test_owns_data_key_owned_same_region(self): + """The MRK Aware Master Key owns a data key when the arn exactly matches its configured ARN.""" + config = MRKAwareKMSMasterKeyConfig(key_id=self.mrk_region1, client=self.mock_client) + test = MRKAwareKMSMasterKey(config=config) + mock_data_key = MagicMock() + mock_data_key.key_provider = MagicMock() + mock_data_key.key_provider.provider_id = "aws-kms" + mock_data_key.key_provider.key_info = self.mrk_region1 + assert test.owns_data_key(data_key=mock_data_key) + + def test_owns_data_key_owned_different_region(self): + """The MRK Aware Master Key owns a data key when the arn refers to a related MRK of its configured ARN.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# To match the encrypted data key's + # //# provider ID MUST exactly match the value "aws-kms" and the the + # //# function AWS KMS MRK Match for Decrypt (aws-kms-mrk-match-for- + # //# decrypt.md#implementation) called with the configured AWS KMS key + # //# identifier and the encrypted data key's provider info MUST return + # //# "true". + config = MRKAwareKMSMasterKeyConfig(key_id=self.mrk_region1, client=self.mock_client) + test = MRKAwareKMSMasterKey(config=config) + mock_data_key = MagicMock() + mock_data_key.key_provider = MagicMock() + mock_data_key.key_provider.provider_id = "aws-kms" + mock_data_key.key_provider.key_info = self.mrk_region2 + assert test.owns_data_key(data_key=mock_data_key) + + def test_owns_data_key_not_owned_wrong_provider(self): + """The MRK Aware Master Key does not own a data key when the provider doesn't match.""" + config = MRKAwareKMSMasterKeyConfig(key_id=self.mrk_region1, client=self.mock_client) + test = MRKAwareKMSMasterKey(config=config) + mock_data_key = MagicMock() + mock_data_key.key_provider = MagicMock() + mock_data_key.key_provider.provider_id = "another_provider" + mock_data_key.key_provider.key_info = self.mrk_region1 + assert not test.owns_data_key(data_key=mock_data_key) + + def test_owns_data_key_not_owned_wrong_key_id(self): + """The MRK Aware Master Key does not own a data key when the key arn is not a related MRK of its + configured ARN.""" + config = MRKAwareKMSMasterKeyConfig(key_id=self.mrk_region1, client=self.mock_client) + test = MRKAwareKMSMasterKey(config=config) + mock_data_key = MagicMock() + mock_data_key.key_provider = MagicMock() + mock_data_key.key_provider.provider_id = "aws-kms" + mock_data_key.key_provider.key_info = VALUES["arn"] + assert not test.owns_data_key(data_key=mock_data_key) + + def test_match_srks_strictly_equal(self): + key1 = "arn:aws:kms:us-east-1:123456789012:key/abcd123" + assert _check_mrk_arns_equal(key1, key1) + + def test_match_mrks_strictly_equal(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + # //= type=test + # //# If both identifiers are identical, this function MUST return "true". + key1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-abcd123" + assert _check_mrk_arns_equal(key1, key1) + + def test_match_mrk_to_srk(self): + key1 = "arn:aws:kms:us-east-1:123456789012:key/abcd123" + mrk1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-abcd123" + assert not _check_mrk_arns_equal(key1, mrk1) + assert not _check_mrk_arns_equal(mrk1, key1) + + def test_match_mrks_srks(self): + """Single-Region keys cannot be equivalent MRKs, even if all fields (except region) match.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + # //= type=test + # //# Otherwise if either input is not identified as a multi-Region key + # //# (aws-kms-key-arn.md#identifying-an-aws-kms-multi-region-key), then + # //# this function MUST return "false". + key1 = "arn:aws:kms:us-east-1:123456789012:key/abcd123" + key2 = "arn:aws:kms:us-west-2:123456789012:key/abcd123" + assert not _check_mrk_arns_equal(key1, key2) + + def test_match_mrks(self): + """Multi-Region keys are equivalent if all fields except region match.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + # //= type=test + # //# Otherwise if both inputs are + # //# identified as a multi-Region keys (aws-kms-key-arn.md#identifying-an- + # //# aws-kms-multi-region-key), this function MUST return the result of + # //# comparing the "partition", "service", "accountId", "resourceType", + # //# and "resource" parts of both ARN inputs. + + # //= compliance/framework/aws-kms/aws-kms-mrk-match-for-decrypt.txt#2.5 + # //= type=test + # //# The caller MUST provide: + key1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-abcd123" + key2 = "arn:aws:kms:us-west-2:123456789012:key/mrk-abcd123" + assert _check_mrk_arns_equal(key1, key2) + + def test_master_keys_for_encryption_not_overridden(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.7 + # //= type=test + # //# MUST be unchanged from the Master Key interface. + assert MRKAwareKMSMasterKey.master_keys_for_encryption == KMSMasterKey.master_keys_for_encryption + + def test_master_key_not_overridden(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.8 + # //= type=test + # //# MUST be unchanged from the Master Key interface. + assert MRKAwareKMSMasterKey.master_key == KMSMasterKey.master_key + + def test_match_mrks_wrong_partition(self): + """Multi-Region keys are not equivalent if the partition does not match.""" + key1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-abcd123" + key2 = "arn:aws-us-gov:kms:us-west-2:123456789012:key/mrk-abcd123" + assert not _check_mrk_arns_equal(key1, key2) + assert not _check_mrk_arns_equal(key2, key1) # pylint: disable=arguments-out-of-order + + def test_match_mrks_wrong_account(self): + """Multi-Region keys are not equivalent if the account does not match.""" + key1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-abcd123" + key2 = "arn:aws:kms:us-west-2:333333333333:key/mrk-abcd123" + assert not _check_mrk_arns_equal(key1, key2) + assert not _check_mrk_arns_equal(key2, key1) # pylint: disable=arguments-out-of-order + + def test_match_mrks_wrong_resource_id(self): + """Multi-Region keys are not equivalent if the resource id does not match.""" + key1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-123" + key2 = "arn:aws:kms:us-west-2:123456789012:key/mrk-abc" + assert not _check_mrk_arns_equal(key1, key2) + assert not _check_mrk_arns_equal(key2, key1) # pylint: disable=arguments-out-of-order + + def test_match_mrks_bare_key_id_and_arn(self): + """For matching on decrypt, we cannot compare ARNs to bare key ids.""" + key1 = "mrk-123" + key2 = "arn:aws:kms:us-west-2:123456789012:key/mrk-123" + with pytest.raises(MalformedArnError) as excinfo: + _check_mrk_arns_equal(key1, key2) + excinfo.match("Resource .+ could not be parsed as an ARN") + + def test_related_mrks_bare_key_ids(self): + """When checking uniqueness, bare key ids are equivalent if they are the same.""" + key1 = "mrk-123" + key2 = "mrk-123" + assert _check_mrk_arns_equal(key1, key2) + assert _check_mrk_arns_equal(key2, key1) # pylint: disable=arguments-out-of-order + + def test_key_resource_match_same_arn_resource_id(self): + """When checking resource ids, arns match if they have the same resource id""" + key1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-123" + key2 = "arn:not-aws:kms:us-west-2:000000000000:key/mrk-123" + assert _key_resource_match(key1, key2) + assert _key_resource_match(key2, key1) # pylint: disable=arguments-out-of-order + + def test_key_resource_match_arn_and_bare_id(self): + """When checking resource ids, an arn matches a bare id if the bare id equals its resource id""" + key1 = "mrk-123" + key2 = "arn:aws:kms:us-west-2:123456789012:key/mrk-123" + assert _key_resource_match(key1, key2) + assert _key_resource_match(key2, key1) # pylint: disable=arguments-out-of-order + + def test_key_resource_match_bare_ids(self): + """When checking resource ids, bare ids match if they are the same""" + key1 = "mrk-123" + assert _key_resource_match(key1, key1) + + def test_key_resource_match_different_resource_ids(self): + """When checking resource ids, arns do not match if they have different resource ids""" + key1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-123" + key2 = "arn:aws:kms:us-west-2:123456789012:key/mrk-abc" + assert not _key_resource_match(key1, key2) + assert not _key_resource_match(key2, key1) # pylint: disable=arguments-out-of-order + + def test_key_resource_match_alias_resource_type(self): + """When checking resource ids, arns do not match if one is an alias resource type""" + key1 = "arn:aws:kms:us-east-1:123456789012:key/mrk-123" + key2 = "arn:aws:kms:us-east-1:123456789012:alias/mrk-123" + assert not _key_resource_match(key1, key2) + assert not _key_resource_match(key2, key1) # pylint: disable=arguments-out-of-order + + def test_key_resource_match_bare_id_with_alias_resource_type(self): + """When checking resource ids, arns do not match if one is an alias resource type and the other is a bare id""" + key1 = "mrk-123" + key2 = "arn:aws:kms:us-east-1:123456789012:alias/mrk-123" + assert not _key_resource_match(key1, key2) + assert not _key_resource_match(key2, key1) # pylint: disable=arguments-out-of-order + + def test_key_resource_match_different_bare_ids(self): + """When checking resource ids, bare ids do not match if they are different""" + key1 = "mrk-123" + key2 = "mrk-abc" + assert not _key_resource_match(key1, key2) + assert not _key_resource_match(key2, key1) # pylint: disable=arguments-out-of-order diff --git a/test/unit/test_providers_kms_master_key_provider.py b/test/unit/test_providers_kms_master_key_provider.py index a6418052b..6e142bb58 100644 --- a/test/unit/test_providers_kms_master_key_provider.py +++ b/test/unit/test_providers_kms_master_key_provider.py @@ -22,6 +22,7 @@ MasterKeyProviderError, UnknownRegionError, ) +from aws_encryption_sdk.internal.arn import arn_from_str from aws_encryption_sdk.internal.str_ops import to_str from aws_encryption_sdk.key_providers.base import MasterKeyProvider from aws_encryption_sdk.key_providers.kms import ( @@ -29,6 +30,9 @@ DiscoveryAwsKmsMasterKeyProvider, DiscoveryFilter, KMSMasterKey, + MRKAwareDiscoveryAwsKmsMasterKeyProvider, + MRKAwareKMSMasterKey, + MRKAwareStrictAwsKmsMasterKeyProvider, StrictAwsKmsMasterKeyProvider, ) @@ -59,7 +63,9 @@ class KMSMasterKeyProviderTestBase(object): @pytest.fixture(autouse=True) def apply_fixtures(self): self.botocore_no_region_session = botocore.session.Session(session_vars={"region": (None, None, None, None)}) - self.mock_botocore_session_patcher = patch("aws_encryption_sdk.key_providers.kms.botocore.session.Session") + self.mock_botocore_session_patcher = patch( + "aws_encryption_sdk.key_providers.kms.botocore.session.Session", __class__=botocore.session.Session + ) self.mock_botocore_session = self.mock_botocore_session_patcher.start() self.mock_boto3_session_patcher = patch("aws_encryption_sdk.key_providers.kms.boto3.session.Session") self.mock_boto3_session = self.mock_boto3_session_patcher.start() @@ -68,6 +74,7 @@ def apply_fixtures(self): self.mock_boto3_client_instance = MagicMock() self.mock_boto3_client_instance.__class__ = botocore.client.BaseClient self.mock_boto3_session_instance.client.return_value = self.mock_boto3_client_instance + yield # Run tearDown self.mock_botocore_session_patcher.stop() @@ -84,6 +91,47 @@ def validate_config(self): class TestBaseKMSMasterKeyProvider(KMSMasterKeyProviderTestBase): def test_parent(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=test + # //# The input MUST be the same as the Master Key Provider Get Master Key + # //# (../master-key-provider-interface.md#get-master-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=test + # //# The output MUST be the same as the Master Key Provider Get Master Key + # //# (../master-key-provider-interface.md#get-master-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + # //= type=test + # //# The input MUST be the same as the Master Key Provider Get Master Keys + # //# For Encryption (../master-key-provider-interface.md#get-master-keys- + # //# for-encryption) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + # //= type=test + # //# The output MUST be the same as the Master Key Provider Get Master + # //# Keys For Encryption (../master-key-provider-interface.md#get-master- + # //# keys-for-encryption) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //= type=test + # //# The input MUST be the same as the Master Key Provider Decrypt Data + # //# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.9 + # //= type=test + # //# The output MUST be the same as the Master Key Provider Decrypt Data + # //# Key (../master-key-provider-interface.md#decrypt-data-key) interface. + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.5 + # //= type=test + # //# MUST implement the Master Key Provider Interface (../master-key- + # //# provider-interface.md#interface) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key.txt#2.9 + # //= type=test + # //# The output MUST be the same as the Master Key Decrypt Data Key + # //# (../master-key-interface.md#decrypt-data-key) interface. assert issubclass(BaseKMSMasterKeyProvider, MasterKeyProvider) @patch("aws_encryption_sdk.key_providers.kms.BaseKMSMasterKeyProvider.add_regional_clients_from_list") @@ -92,6 +140,7 @@ def test_init_with_region_names(self, mock_add_clients): test = UnitTestBaseKMSMasterKeyProvider(region_names=region_names) mock_add_clients.assert_called_once_with(region_names) assert test.default_region is sentinel.region_name_1 + assert test.config.grant_tokens == () @patch("aws_encryption_sdk.key_providers.kms.BaseKMSMasterKeyProvider.add_regional_client") def test_init_with_default_region_found(self, mock_add_regional_client): @@ -105,6 +154,12 @@ def test_init_with_default_region_found(self, mock_add_regional_client): assert test.default_region is sentinel.default_region mock_add_regional_client.assert_called_with(sentinel.default_region) + def test_init_with_grant_tokens(self): + grant_tokens = (sentinel.grant_token2, sentinel.grant_token2) + test = UnitTestBaseKMSMasterKeyProvider(grant_tokens=grant_tokens) + test._process_config() + assert test.config.grant_tokens is grant_tokens + @patch("aws_encryption_sdk.key_providers.kms.BaseKMSMasterKeyProvider.add_regional_client") def test_init_with_default_region_not_found(self, mock_add_regional_client): test = UnitTestBaseKMSMasterKeyProvider(botocore_session=self.botocore_no_region_session) @@ -171,6 +226,7 @@ def test_new_master_key(self, mock_client): key = test._new_master_key(key_info) check_key = KMSMasterKey(key_id=key_info, client=self.mock_boto3_client_instance) assert key != check_key + assert key.config.grant_tokens == () @patch("aws_encryption_sdk.key_providers.kms.BaseKMSMasterKeyProvider._client") def test_new_master_key_with_discovery_filter_invalid_arn(self, mock_client): @@ -210,6 +266,12 @@ def test_new_master_key_with_discovery_filter_partition_not_allowed(self, mock_c @patch("aws_encryption_sdk.key_providers.kms.BaseKMSMasterKeyProvider._client") def test_new_master_key_with_discovery_filter_success(self, mock_client): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=test + # //# In discovery mode if a discovery filter is configured the requested AWS + # //# KMS key ARN's "partition" MUST match the discovery filter's + # //# "partition" and the AWS KMS key ARN's "account" MUST exist in the + # //# discovery filter's account id set. mock_client.return_value = self.mock_boto3_client_instance key_info = b"arn:aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" test = UnitTestBaseKMSMasterKeyProvider() @@ -228,6 +290,17 @@ def test_new_master_key_no_vend(self, mock_client): key = test._new_master_key(key_info) assert key.key_id == key_info + @patch("aws_encryption_sdk.key_providers.kms.BaseKMSMasterKeyProvider._client") + def test_new_master_key_with_grant_tokens(self, mock_client): + grant_tokens = (sentinel.grant_token2, sentinel.grant_token2) + mock_client.return_value = self.mock_boto3_client_instance + key_info = b"arn:aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + test = UnitTestBaseKMSMasterKeyProvider(key_ids=[key_info], grant_tokens=grant_tokens) + + key = test._new_master_key(key_info) + assert key.key_id == key_info + assert key.config.grant_tokens is grant_tokens + class TestDiscoveryKMSMasterKeyProvider(KMSMasterKeyProviderTestBase): def test_parent(self): @@ -235,6 +308,10 @@ def test_parent(self): def test_init_bare(self): test = DiscoveryAwsKmsMasterKeyProvider() + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + # //= type=test + # //# If the configured mode is discovery the function MUST return an empty + # //# list. assert test.vend_masterkey_on_decrypt def test_init_failure_discovery_filter_missing_account_ids(self): @@ -265,12 +342,20 @@ def test_init_failure_discovery_filter_empty_partition(self): excinfo.match("you must include both account ids and partition") def test_init_failure_with_key_ids(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //= type=test + # //# The key id list MUST be empty in discovery mode. with pytest.raises(ConfigMismatchError) as excinfo: DiscoveryAwsKmsMasterKeyProvider( discovery_filter=DiscoveryFilter(account_ids=["123"], partition="aws"), key_ids=["1234"] ) excinfo.match("To explicitly identify which keys should be used, use a StrictAwsKmsMasterKeyProvider.") + def test_init_failure_with_discovery_region(self): + with pytest.raises(ConfigMismatchError) as excinfo: + DiscoveryAwsKmsMasterKeyProvider(discovery_region="us-west-2") + excinfo.match("To enable MRK-aware discovery mode, use a MRKAwareDiscoveryAwsKmsMasterKeyProvider.") + def test_init_success(self): discovery_filter = DiscoveryFilter(account_ids=["1234"], partition="aws") test = DiscoveryAwsKmsMasterKeyProvider(discovery_filter=discovery_filter) @@ -289,6 +374,9 @@ def test_init_bare_fails(self): excinfo.match("To enable strict mode you must provide key ids") def test_init_empty_key_ids_fails(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //= type=test + # //# The key id list MUST NOT be empty or null in strict mode. with pytest.raises(ConfigMismatchError) as excinfo: StrictAwsKmsMasterKeyProvider(key_ids=[]) excinfo.match("To enable strict mode you must provide key ids") @@ -300,6 +388,9 @@ def test_init_null_key_id_fails(self): excinfo.match("Key ids must be valid AWS KMS ARNs") def test_init_empty_string_key_id_fails(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //= type=test + # //# The key id list MUST NOT contain any null or empty string values. key_ids = ("arn:aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", "") with pytest.raises(ConfigMismatchError) as excinfo: StrictAwsKmsMasterKeyProvider(key_ids=key_ids) @@ -307,6 +398,9 @@ def test_init_empty_string_key_id_fails(self): @patch("aws_encryption_sdk.key_providers.kms.StrictAwsKmsMasterKeyProvider.add_master_keys_from_list") def test_init_with_discovery_fails(self, mock_add_keys): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //= type=test + # //# A discovery filter MUST NOT be configured in strict mode. key_ids = ( "arn:aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", "arn:aws:kms:us-east-1:333333333333:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", @@ -317,6 +411,20 @@ def test_init_with_discovery_fails(self, mock_add_keys): excinfo.match("To enable discovery mode, use a DiscoveryAwsKmsMasterKeyProvider") mock_add_keys.assert_not_called() + @patch("aws_encryption_sdk.key_providers.kms.StrictAwsKmsMasterKeyProvider.add_master_keys_from_list") + def test_init_with_discovery_region_fails(self, mock_add_keys): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //= type=test + # //# A default MRK Region MUST NOT be configured in strict mode. + key_ids = ( + "arn:aws:kms:us-east-1:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "arn:aws:kms:us-east-1:333333333333:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + ) + with pytest.raises(ConfigMismatchError) as excinfo: + StrictAwsKmsMasterKeyProvider(key_ids=key_ids, discovery_region="us-east-1") + excinfo.match("To enable MRK-aware discovery mode, use a MRKAwareDiscoveryAwsKmsMasterKeyProvider") + mock_add_keys.assert_not_called() + @patch("aws_encryption_sdk.key_providers.kms.StrictAwsKmsMasterKeyProvider.add_master_keys_from_list") def test_init_with_key_ids(self, mock_add_keys): key_ids = ( @@ -324,5 +432,278 @@ def test_init_with_key_ids(self, mock_add_keys): "arn:aws:kms:us-east-1:333333333333:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", ) test = StrictAwsKmsMasterKeyProvider(key_ids=key_ids) + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.8 + # //= type=test + # //# If the configured mode is strict this function MUST return a + # //# list of master keys obtained by calling Get Master Key (aws-kms-mrk- + # //# aware-master-key-provider.md#get-master-key) for each AWS KMS key + # //# identifier in the configured key ids assert not test.vend_masterkey_on_decrypt mock_add_keys.assert_called_once_with(key_ids) + + @patch("aws_encryption_sdk.key_providers.kms.BaseKMSMasterKeyProvider._client") + def test_add_master_keys_class(self, mock_client): + """Check that the MRK-aware provider creates MRKAwareKMSMasterKeys""" + mock_client.return_value = self.mock_boto3_client_instance + key_id = "arn:aws:kms:eu-west-2:222222222222:key/mrk-aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + provider = StrictAwsKmsMasterKeyProvider(key_ids=[key_id]) + master_key = provider._new_master_key(key_id) + assert master_key.__class__ == KMSMasterKey + + +class TestMRKAwareStrictKMSMasterKeyProvider(KMSMasterKeyProviderTestBase): + def test_parent(self): + assert issubclass(MRKAwareStrictAwsKmsMasterKeyProvider, StrictAwsKmsMasterKeyProvider) + + def test_init_with_key_ids(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //= type=test + # //# All AWS KMS + # //# key identifiers are be passed to Assert AWS KMS MRK are unique (aws- + # //# kms-mrk-are-unique.md#Implementation) and the function MUST return + # //# success. + + # //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + # //= type=test + # //# The caller MUST provide: + + # //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + # //= type=test + # //# If there are zero duplicate resource ids between the multi-region + # //# keys, this function MUST exit successfully + key_ids = ( + "arn:aws:kms:eu-west-2:222222222222:key/mrk-aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "arn:aws:kms:us-east-1:222222222222:key/mrk-bbbbbbbb-1111-2222-3333-bbbbbbbbbbbb", + ) + provider = MRKAwareStrictAwsKmsMasterKeyProvider(key_ids=key_ids) + assert len(provider.config.key_ids) == 2 + assert key_ids[0] in provider.config.key_ids + assert key_ids[1] in provider.config.key_ids + + def test_init_with_duplicate_non_mrk_key_ids(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + # //= type=test + # //# If the list does not contain any multi-Region keys (aws-kms-key- + # //# arn.md#identifying-an-aws-kms-multi-region-key) this function MUST + # //# exit successfully. + key_ids = ( + "arn:aws:kms:eu-west-2:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "arn:aws:kms:eu-west-2:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "alias/myAlias", + "alias/myAlias", + ) + with patch.object(self.mock_botocore_session, "get_config_variable", return_value="us-west-2"): + provider = MRKAwareStrictAwsKmsMasterKeyProvider( + botocore_session=self.mock_botocore_session, key_ids=key_ids + ) + assert len(provider.config.key_ids) == 4 + assert key_ids[0] in provider.config.key_ids + assert key_ids[1] in provider.config.key_ids + assert key_ids[2] in provider.config.key_ids + assert key_ids[3] in provider.config.key_ids + + def test_init_requires_unique_mrks(self): + # //= compliance/framework/aws-kms/aws-kms-mrk-are-unique.txt#2.5 + # //= type=test + # //# If any duplicate multi-region resource ids exist, this function MUST + # //# yield an error that includes all identifiers with duplicate resource + # //# ids not only the first duplicate found. + key_ids = ( + "arn:aws:kms:eu-west-2:222222222222:key/mrk-aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "arn:aws:kms:us-east-1:222222222222:key/mrk-aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "mrk-aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "arn:aws:kms:eu-west-2:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + ) + expected_error_string = "Configured key ids must be unique. Found related MRKs: .*, .*, .*" + with patch.object(self.mock_botocore_session, "get_config_variable", return_value="us-west-2"): + with pytest.raises(ConfigMismatchError) as excinfo: + MRKAwareStrictAwsKmsMasterKeyProvider(botocore_session=self.mock_botocore_session, key_ids=key_ids) + excinfo.match(expected_error_string) + + def test_add_master_keys_class(self): + """Check that the MRK-aware provider creates MRKAwareKMSMasterKeys""" + grant_tokens = (sentinel.grant_token2, sentinel.grant_token2) + key_id = "arn:aws:kms:eu-west-2:222222222222:key/mrk-aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + provider = MRKAwareStrictAwsKmsMasterKeyProvider(key_ids=[key_id], grant_tokens=grant_tokens) + + master_key = provider._new_master_key(key_id) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=test + # //# In strict mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- + # //# master-key.md) MUST be returned configured with + assert master_key.__class__ == MRKAwareKMSMasterKey + assert "eu-west-2" in master_key._key_id + self.mock_boto3_session.assert_called_with(botocore_session=ANY) + self.mock_boto3_session_instance.client.assert_called_with( + "kms", + region_name="eu-west-2", + config=provider._user_agent_adding_config, + ) + assert master_key.config.grant_tokens is grant_tokens + + @pytest.mark.parametrize( + "non_arn", + ( + "alias/myAlias", + "mrk-1234", + "1234", + ), + ) + def test_add_master_keys_invalid_arn(self, non_arn): + """ + Check that the Strict MRK-aware provider uses the default region when creating a new key with an invalid arn. + """ + with patch.object( + self.mock_botocore_session, "get_config_variable", return_value="us-west-2" + ) as mock_get_config: + provider = MRKAwareStrictAwsKmsMasterKeyProvider( + botocore_session=self.mock_botocore_session, key_ids=[non_arn] + ) + master_key = provider._new_master_key(non_arn) + + mock_get_config.assert_called_with("region") + assert master_key.__class__ == MRKAwareKMSMasterKey + assert master_key._key_id == non_arn + self.mock_boto3_session.assert_called_with(botocore_session=ANY) + self.mock_boto3_session_instance.client.assert_called_with( + "kms", + region_name="us-west-2", + config=provider._user_agent_adding_config, + ) + + +class TestMRKAwareDiscoveryKMSMasterKeyProvider(KMSMasterKeyProviderTestBase): + def test_parent(self): + assert issubclass(MRKAwareDiscoveryAwsKmsMasterKeyProvider, DiscoveryAwsKmsMasterKeyProvider) + + def test_init_explicit_discovery_region(self): + provider = MRKAwareDiscoveryAwsKmsMasterKeyProvider(discovery_region="us-east-1") + assert provider.config.discovery_region == "us-east-1" + + def test_init_implicit_discovery_region(self): + """Check that an MRK-aware provider without an explicit discovery_region uses the default SDK region.""" + with patch.object( + self.mock_botocore_session, "get_config_variable", return_value=sentinel.default_region + ) as mock_get_config: + test = MRKAwareDiscoveryAwsKmsMasterKeyProvider(botocore_session=self.mock_botocore_session) + mock_get_config.assert_called_once_with("region") + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //= type=test + # //# In discovery mode + # //# if a default MRK Region is not configured the AWS SDK Default Region + # //# MUST be used. + assert test.default_region is sentinel.default_region + assert test.config.discovery_region is sentinel.default_region + + def test_init_sdk_default_not_found(self): + """Check that an MRK-aware provider without an explicit discovery_region fails to initialize if it cannot + find a default region for the AWS SDK.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.6 + # //= type=test + # //# If an AWS SDK Default Region can not be obtained + # //# initialization MUST fail. + with pytest.raises(ConfigMismatchError) as excinfo: + MRKAwareDiscoveryAwsKmsMasterKeyProvider(botocore_session=self.botocore_no_region_session) + excinfo.match("Failed to determine default discovery region") + + def test_add_master_keys_mrk_with_discovery_region(self): + """Check that an MRK-aware provider with an explicit discovery_region uses its configured region when creating + new keys if the requested keys are MRKs.""" + grant_tokens = (sentinel.grant_token2, sentinel.grant_token2) + original_arn = arn_from_str("arn:aws:kms:eu-west-2:222222222222:key/mrk-aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb") + configured_region = "us-east-1" + provider = MRKAwareDiscoveryAwsKmsMasterKeyProvider( + discovery_region=configured_region, grant_tokens=grant_tokens + ) + master_key = provider._new_master_key(original_arn.to_string()) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=test + # //# In discovery mode a AWS KMS MRK Aware Master Key (aws-kms-mrk-aware- + # //# master-key.md) MUST be returned configured with + assert master_key.__class__ == MRKAwareKMSMasterKey + self.mock_boto3_session.assert_called_with(botocore_session=ANY) + self.mock_boto3_session_instance.client.assert_called_with( + "kms", + region_name=configured_region, + config=provider._user_agent_adding_config, + ) + assert configured_region in master_key._key_id + assert original_arn.region not in master_key._key_id + assert master_key.config.grant_tokens is grant_tokens + + def test_add_master_keys_mrk_sdk_default(self): + """Check that an MRK-aware provider without an explicit discovery_region uses its default region when creating + new keys if the requested keys are MRKs.""" + grant_tokens = (sentinel.grant_token2, sentinel.grant_token2) + original_arn = arn_from_str("arn:aws:kms:eu-west-2:222222222222:key/mrk-aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb") + with patch.object( + self.mock_botocore_session, "get_config_variable", return_value="us-west-2" + ) as mock_get_config: + provider = MRKAwareDiscoveryAwsKmsMasterKeyProvider( + botocore_session=self.mock_botocore_session, grant_tokens=grant_tokens + ) + mock_get_config.assert_called_once_with("region") + + master_key = provider._new_master_key(original_arn.to_string()) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=test + # //# Otherwise if the mode is discovery then + # //# the AWS Region MUST be the discovery MRK region. + + assert master_key.__class__ == MRKAwareKMSMasterKey + self.mock_boto3_session.assert_called_with(botocore_session=ANY) + self.mock_boto3_session_instance.client.assert_called_with( + "kms", + region_name=provider.default_region, + config=provider._user_agent_adding_config, + ) + assert provider.default_region in master_key._key_id + assert original_arn.region not in master_key._key_id + assert master_key.config.grant_tokens is grant_tokens + + def test_add_master_keys_srk(self): + """Check that the MRK-aware provider uses the original key region when creating new keys if the requested + keys are SRKs.""" + original_arn = "arn:aws:kms:eu-west-2:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb" + provider = MRKAwareDiscoveryAwsKmsMasterKeyProvider(discovery_region="us-east-1") + master_key = provider._new_master_key(original_arn) + + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=test + # //# Otherwise if the requested AWS KMS key + # //# identifier is identified as a multi-Region key (aws-kms-key- + # //# arn.md#identifying-an-aws-kms-multi-region-key), then AWS Region MUST + # //# be the region from the AWS KMS key ARN stored in the provider info + # //# from the encrypted data key. + assert master_key.__class__ == MRKAwareKMSMasterKey + self.mock_boto3_session.assert_called_with(botocore_session=ANY) + self.mock_boto3_session_instance.client.assert_called_with( + "kms", + region_name="eu-west-2", + config=provider._user_agent_adding_config, + ) + assert master_key._key_id == original_arn + + @pytest.mark.parametrize( + "non_arn", + ( + "alias/myAlias", + "mrk-1234", + "1234", + ":aws:kms:eu-west-2:222222222222:key/aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb", + "arn:aws:kms:eu-west-2:222222222222:key/", + ), + ) + def test_add_master_keys_invalid_arn(self, non_arn): + """Check that the provider throws an error when creating a new key with an invalid arn.""" + # //= compliance/framework/aws-kms/aws-kms-mrk-aware-master-key-provider.txt#2.7 + # //= type=test + # //# In discovery mode, the requested + # //# AWS KMS key identifier MUST be a well formed AWS KMS ARN. + + provider = MRKAwareDiscoveryAwsKmsMasterKeyProvider(discovery_region="us-east-1") + with pytest.raises(MalformedArnError): + provider._new_master_key(non_arn) diff --git a/test/unit/test_values.py b/test/unit/test_values.py index 4283e8a84..cb48c8c74 100644 --- a/test/unit/test_values.py +++ b/test/unit/test_values.py @@ -80,6 +80,10 @@ def array_byte(source): "encoded_curve_point": "AmZvwV/dN6o9p/usAnJdRcdnE12UbaDHuEFPeyVkw5FC1ULGlSznzDdD3FP8SW1UMg==", "arn": b"arn:aws:kms:us-east-1:248168362296:key/ce78d3b3-f800-4785-a3b9-63e30bb4b183", "arn_str": "arn:aws:kms:us-east-1:248168362296:key/ce78d3b3-f800-4785-a3b9-63e30bb4b183", + "mrk_arn_region1": b"arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7", + "mrk_arn_region1_str": "arn:aws:kms:us-west-2:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7", + "mrk_arn_region2": b"arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7", + "mrk_arn_region2_str": "arn:aws:kms:us-east-1:658956600833:key/mrk-80bd8ecdcd4342aebd84b7dc9da498a7", "data_key": six.b( "\x00\xfa\x8c\xdd\x08Au\xc6\x92_4\xc5\xfb\x90\xaf\x8f\xa1D\xaf\xcc\xd25\xa8\x0b\x0b\x16\x92\x91W\x01\xb7\x84" ), diff --git a/test_vector_handlers/requirements.txt b/test_vector_handlers/requirements.txt index a913110a0..0b43ea50f 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.2.0 +aws-encryption-sdk>=2.3.0 pytest>=3.3.1 six diff --git a/test_vector_handlers/src/awses_test_vectors/internal/aws_kms.py b/test_vector_handlers/src/awses_test_vectors/internal/aws_kms.py index c63e167bc..14c109e7d 100644 --- a/test_vector_handlers/src/awses_test_vectors/internal/aws_kms.py +++ b/test_vector_handlers/src/awses_test_vectors/internal/aws_kms.py @@ -15,12 +15,17 @@ from aws_encryption_sdk.identifiers import AlgorithmSuite except ImportError: from aws_encryption_sdk.identifiers import Algorithm as AlgorithmSuite -from aws_encryption_sdk.key_providers.kms import DiscoveryAwsKmsMasterKeyProvider, StrictAwsKmsMasterKeyProvider +from aws_encryption_sdk.key_providers.kms import ( + DiscoveryAwsKmsMasterKeyProvider, + MRKAwareDiscoveryAwsKmsMasterKeyProvider, + StrictAwsKmsMasterKeyProvider, +) from awses_test_vectors.internal.defaults import ENCODING # This lets us easily use a single boto3 client per region for all KMS master keys. KMS_MASTER_KEY_PROVIDER = DiscoveryAwsKmsMasterKeyProvider() +KMS_MRK_AWARE_MASTER_KEY_PROVIDER = MRKAwareDiscoveryAwsKmsMasterKeyProvider(discovery_region="us-west-2") def arn_from_key_id(key_id): 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 8e4cc9588..c94fd1452 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 @@ -24,7 +24,6 @@ import pytest import six from aws_encryption_sdk.identifiers import CommitmentPolicy -from aws_encryption_sdk.key_providers.base import MasterKeyProvider from awses_test_vectors.internal.defaults import ENCODING from awses_test_vectors.internal.util import ( @@ -91,9 +90,12 @@ def matcher_spec(self): def match(self, name, decrypt_fn): """Assert that the given decrypt closure behaves as expected.""" - plaintext, _header = decrypt_fn() - if plaintext != self.plaintext: - raise ValueError("Decrypted plaintext does not match expected value for scenario '{}'".format(name)) + try: + plaintext, _header = decrypt_fn() + if plaintext != self.plaintext: + raise ValueError("Decrypted plaintext does not match expected value for scenario '{}'".format(name)) + except BaseException: + raise RuntimeError("Decryption did not succeed as expected for scenario '{}'".format(name)) @attr.s @@ -189,7 +191,7 @@ class MessageDecryptionTestScenario(object): :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: + :param Callable master_key_provider_fn: :param str description: Description of test scenario (optional) """ @@ -198,7 +200,7 @@ class MessageDecryptionTestScenario(object): ciphertext_uri = attr.ib(validator=attr.validators.instance_of(six.string_types)) ciphertext = attr.ib(validator=attr.validators.instance_of(six.binary_type)) master_key_specs = attr.ib(validator=iterable_validator(list, MasterKeySpec)) - master_key_provider = attr.ib(validator=attr.validators.instance_of(MasterKeyProvider)) + master_key_provider_fn = attr.ib(validator=attr.validators.is_callable()) result = attr.ib(validator=attr.validators.instance_of(MessageDecryptionTestResult)) decryption_method = attr.ib( default=None, validator=attr.validators.optional(attr.validators.instance_of(DecryptionMethod)) @@ -213,7 +215,7 @@ def __init__( ciphertext, # type: bytes result, # type: MessageDecryptionTestResult master_key_specs, # type: Iterable[MasterKeySpec] - master_key_provider, # type: MasterKeyProvider + master_key_provider_fn, # type: Callable decryption_method=None, # type: Optional[DecryptionMethod] description=None, # type: Optional[str] ): # noqa=D107 @@ -226,7 +228,7 @@ def __init__( self.ciphertext = ciphertext self.result = result self.master_key_specs = master_key_specs - self.master_key_provider = master_key_provider + self.master_key_provider_fn = master_key_provider_fn self.decryption_method = decryption_method self.description = description attr.validate(self) @@ -251,7 +253,10 @@ 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) + + def master_key_provider_fn(): + return 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"] @@ -261,7 +266,7 @@ def from_scenario( ciphertext_uri=scenario["ciphertext"], ciphertext=ciphertext_reader(scenario["ciphertext"]), master_key_specs=master_key_specs, - master_key_provider=master_key_provider, + master_key_provider_fn=master_key_provider_fn, result=result, decryption_method=decryption_method, description=scenario.get("description"), @@ -288,12 +293,12 @@ def scenario_spec(self): def _one_shot_decrypt(self): client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - return client.decrypt(source=self.ciphertext, key_provider=self.master_key_provider) + return client.decrypt(source=self.ciphertext, key_provider=self.master_key_provider_fn()) def _streaming_decrypt(self): result = bytearray() client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) - with client.stream(source=self.ciphertext, mode="d", key_provider=self.master_key_provider) as decryptor: + with client.stream(source=self.ciphertext, mode="d", key_provider=self.master_key_provider_fn()) as decryptor: for chunk in decryptor: result.extend(chunk) return result, decryptor.header @@ -302,7 +307,7 @@ def _streaming_decrypt_unsigned(self): result = bytearray() client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT) with client.stream( - source=self.ciphertext, mode="decrypt-unsigned", key_provider=self.master_key_provider + source=self.ciphertext, mode="decrypt-unsigned", key_provider=self.master_key_provider_fn() ) as decryptor: for chunk in decryptor: result.extend(chunk) 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 ebe76824c..e407a1b65 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 @@ -22,8 +22,9 @@ import attr import six -from aws_encryption_sdk.key_providers.base import MasterKeyProvider +from aws_encryption_sdk.caches.local import LocalCryptoMaterialsCache from aws_encryption_sdk.materials_managers.base import CryptoMaterialsManager +from aws_encryption_sdk.materials_managers.caching import CachingCryptoMaterialsManager from aws_encryption_sdk.materials_managers.default import DefaultCryptoMaterialsManager from awses_test_vectors.internal.defaults import ENCODING @@ -91,7 +92,9 @@ def run_scenario_with_tampering(self, ciphertext_writer, generation_scenario, pl return: a list of (ciphertext, result) pairs """ - materials_manager = DefaultCryptoMaterialsManager(generation_scenario.encryption_scenario.master_key_provider) + materials_manager = DefaultCryptoMaterialsManager( + generation_scenario.encryption_scenario.master_key_provider_fn() + ) ciphertext_to_decrypt = generation_scenario.encryption_scenario.run(materials_manager) if generation_scenario.result: expected_result = generation_scenario.result @@ -125,16 +128,28 @@ def run_scenario_with_tampering(self, ciphertext_writer, generation_scenario, _p return: a list of (ciphertext, result) pairs. """ + master_key_provider = generation_scenario.encryption_scenario.master_key_provider_fn() + + # Use a caching CMM to avoid generating a new data key every time. + cache = LocalCryptoMaterialsCache(10) + caching_cmm = CachingCryptoMaterialsManager( + master_key_provider=master_key_provider, + cache=cache, + max_age=60.0, + max_messages_encrypted=100, + ) return [ - self.run_scenario_with_new_provider_info(ciphertext_writer, generation_scenario, new_provider_info) + self.run_scenario_with_new_provider_info( + ciphertext_writer, generation_scenario, caching_cmm, new_provider_info + ) for new_provider_info in self.new_provider_infos ] - def run_scenario_with_new_provider_info(self, ciphertext_writer, generation_scenario, new_provider_info): + def run_scenario_with_new_provider_info( + self, ciphertext_writer, generation_scenario, materials_manager, new_provider_info + ): """Run with tampering for a specific new provider info value""" - tampering_materials_manager = ProviderInfoChangingCryptoMaterialsManager( - generation_scenario.encryption_scenario.master_key_provider, new_provider_info - ) + tampering_materials_manager = ProviderInfoChangingCryptoMaterialsManager(materials_manager, new_provider_info) ciphertext_to_decrypt = generation_scenario.encryption_scenario.run(tampering_materials_manager) expected_result = MessageDecryptionTestResult.expect_error( "Incorrect encrypted data key provider info: " + new_provider_info @@ -152,30 +167,27 @@ class ProviderInfoChangingCryptoMaterialsManager(CryptoMaterialsManager): production! """ - wrapped_default_cmm = attr.ib(validator=attr.validators.instance_of(CryptoMaterialsManager)) + wrapped_cmm = attr.ib(validator=attr.validators.instance_of(CryptoMaterialsManager)) new_provider_info = attr.ib(validator=attr.validators.instance_of(six.string_types)) - def __init__(self, master_key_provider, new_provider_info): - """ - 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 __init__(self, materials_manager, new_provider_info): + """Create a new CMM that wraps a the given CMM.""" + self.wrapped_cmm = materials_manager self.new_provider_info = new_provider_info def get_encryption_materials(self, request): """ - Request materials from the wrapped default CMM, and then change the provider info + Request materials from the wrapped CMM, and then change the provider info on each EDK. """ - result = self.wrapped_default_cmm.get_encryption_materials(request) + result = self.wrapped_cmm.get_encryption_materials(request) for encrypted_data_key in result.encrypted_data_keys: - encrypted_data_key.provider_info = self.new_provider_info + encrypted_data_key.key_provider.key_info = self.new_provider_info return result def decrypt_materials(self, request): - """Thunks to the wrapped default CMM""" - return self.wrapped_default_cmm.decrypt_materials(request) + """Thunks to the wrapped CMM""" + return self.wrapped_cmm.decrypt_materials(request) BITS_PER_BYTE = 8 @@ -242,7 +254,7 @@ def run_scenario_with_tampering(self, ciphertext_writer, generation_scenario, _p return: a list of (ciphertext, result) pairs. """ tampering_materials_manager = HalfSigningCryptoMaterialsManager( - generation_scenario.encryption_scenario.master_key_provider + generation_scenario.encryption_scenario.master_key_provider_fn() ) ciphertext_to_decrypt = generation_scenario.encryption_scenario.run(tampering_materials_manager) expected_result = MessageDecryptionTestResult.expect_error( @@ -312,7 +324,7 @@ class MessageDecryptionTestScenarioGenerator(object): :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: + :param Callable decryption_master_key_provider_fn: :param result: """ @@ -320,7 +332,7 @@ class MessageDecryptionTestScenarioGenerator(object): 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)) + decryption_master_key_provider_fn = attr.ib(validator=attr.validators.is_callable()) result = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(MessageDecryptionTestResult))) @classmethod @@ -343,12 +355,13 @@ def from_scenario(cls, scenario, keys, plaintexts): decryption_master_key_specs = [ MasterKeySpec.from_scenario(spec) for spec in scenario["decryption-master-keys"] ] - decryption_master_key_provider = master_key_provider_from_master_key_specs( - keys, decryption_master_key_specs - ) + + def decryption_master_key_provider_fn(): + return master_key_provider_from_master_key_specs(keys, decryption_master_key_specs) + else: decryption_master_key_specs = encryption_scenario.master_key_specs - decryption_master_key_provider = encryption_scenario.master_key_provider + decryption_master_key_provider_fn = encryption_scenario.master_key_provider_fn result_spec = scenario.get("result") result = MessageDecryptionTestResult.from_result_spec(result_spec, None) if result_spec else None @@ -357,7 +370,7 @@ def from_scenario(cls, scenario, keys, plaintexts): tampering_method=tampering_method, decryption_method=decryption_method, decryption_master_key_specs=decryption_master_key_specs, - decryption_master_key_provider=decryption_master_key_provider, + decryption_master_key_provider_fn=decryption_master_key_provider_fn, result=result, ) @@ -384,7 +397,7 @@ def decryption_test_scenario_pair(self, ciphertext_writer, ciphertext_to_decrypt ciphertext_uri=ciphertext_uri, ciphertext=ciphertext_to_decrypt, master_key_specs=self.decryption_master_key_specs, - master_key_provider=self.decryption_master_key_provider, + master_key_provider_fn=self.decryption_master_key_provider_fn, decryption_method=self.decryption_method, result=expected_result, ), diff --git a/test_vector_handlers/src/awses_test_vectors/manifests/full_message/encrypt.py b/test_vector_handlers/src/awses_test_vectors/manifests/full_message/encrypt.py index e17d8690a..c77fed1ce 100644 --- a/test_vector_handlers/src/awses_test_vectors/manifests/full_message/encrypt.py +++ b/test_vector_handlers/src/awses_test_vectors/manifests/full_message/encrypt.py @@ -21,7 +21,6 @@ import attr import aws_encryption_sdk import six -from aws_encryption_sdk.key_providers.base import MasterKeyProvider from awses_test_vectors.internal.defaults import ENCODING from awses_test_vectors.internal.util import ( @@ -69,7 +68,7 @@ class MessageEncryptionTestScenario(object): :param dict encryption_context: Encryption context to use :param master_key_specs: Iterable of loaded master key specifications :type master_key_specs: iterable of :class:`MasterKeySpec` - :param MasterKeyProvider master_key_provider: + :param Callable master_key_provider_fn: """ plaintext_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) @@ -78,7 +77,7 @@ class MessageEncryptionTestScenario(object): frame_size = attr.ib(validator=attr.validators.instance_of(int)) encryption_context = attr.ib(validator=dictionary_validator(six.string_types, six.string_types)) master_key_specs = attr.ib(validator=iterable_validator(list, MasterKeySpec)) - master_key_provider = attr.ib(validator=attr.validators.instance_of(MasterKeyProvider)) + master_key_provider_fn = attr.ib(validator=attr.validators.is_callable()) @classmethod def from_scenario(cls, scenario, keys, plaintexts): @@ -93,7 +92,9 @@ def from_scenario(cls, scenario, keys, plaintexts): """ algorithm = algorithm_suite_from_string_id(scenario["algorithm"]) master_key_specs = [MasterKeySpec.from_scenario(spec) for spec in scenario["master-keys"]] - master_key_provider = master_key_provider_from_master_key_specs(keys, master_key_specs) + + def master_key_provider_fn(): + return master_key_provider_from_master_key_specs(keys, master_key_specs) return cls( plaintext_name=scenario["plaintext"], @@ -102,7 +103,7 @@ def from_scenario(cls, scenario, keys, plaintexts): frame_size=scenario["frame-size"], encryption_context=scenario["encryption-context"], master_key_specs=master_key_specs, - master_key_provider=master_key_provider, + master_key_provider_fn=master_key_provider_fn, ) def run(self, materials_manager=None): @@ -129,7 +130,7 @@ def run(self, materials_manager=None): if materials_manager: encrypt_kwargs["materials_manager"] = materials_manager else: - encrypt_kwargs["key_provider"] = self.master_key_provider + encrypt_kwargs["key_provider"] = self.master_key_provider_fn() ciphertext, _header = client.encrypt(**encrypt_kwargs) return ciphertext 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 783ae9da6..cba6b7e25 100644 --- a/test_vector_handlers/src/awses_test_vectors/manifests/keys.py +++ b/test_vector_handlers/src/awses_test_vectors/manifests/keys.py @@ -99,11 +99,14 @@ def manifest_spec(self): :return: Key specification JSON :rtype: dict """ + key_id = self.key_id + if self.encrypt or self.decrypt: + key_id = arn_from_key_id(self.key_id) return { "encrypt": self.encrypt, "decrypt": self.decrypt, "type": self.type_name, - "key-id": arn_from_key_id(self.key_id), + "key-id": key_id, } diff --git a/test_vector_handlers/src/awses_test_vectors/manifests/master_key.py b/test_vector_handlers/src/awses_test_vectors/manifests/master_key.py index 05975ccbf..a1a7ae4af 100644 --- a/test_vector_handlers/src/awses_test_vectors/manifests/master_key.py +++ b/test_vector_handlers/src/awses_test_vectors/manifests/master_key.py @@ -19,10 +19,14 @@ import six from aws_encryption_sdk.identifiers import EncryptionKeyType, WrappingAlgorithm from aws_encryption_sdk.key_providers.base import MasterKeyProvider # noqa pylint: disable=unused-import -from aws_encryption_sdk.key_providers.kms import KMSMasterKey # noqa pylint: disable=unused-import +from aws_encryption_sdk.key_providers.kms import ( # noqa pylint: disable=unused-import + DiscoveryFilter, + KMSMasterKey, + MRKAwareDiscoveryAwsKmsMasterKeyProvider, +) from aws_encryption_sdk.key_providers.raw import RawMasterKey -from awses_test_vectors.internal.aws_kms import KMS_MASTER_KEY_PROVIDER +from awses_test_vectors.internal.aws_kms import KMS_MASTER_KEY_PROVIDER, KMS_MRK_AWARE_MASTER_KEY_PROVIDER from awses_test_vectors.internal.util import membership_validator from awses_test_vectors.manifests.keys import KeysManifest, KeySpec # noqa pylint: disable=unused-import @@ -40,7 +44,7 @@ # We only actually need these imports when running the mypy checks pass -KNOWN_TYPES = ("aws-kms", "raw") +KNOWN_TYPES = ("aws-kms", "aws-kms-mrk-aware", "aws-kms-mrk-aware-discovery", "raw") KNOWN_ALGORITHMS = ("aes", "rsa") KNOWN_PADDING = ("pkcs1", "oaep-mgf1") KNOWN_PADDING_HASH = ("sha1", "sha256", "sha384", "sha512") @@ -70,7 +74,7 @@ @attr.s -class MasterKeySpec(object): +class MasterKeySpec(object): # pylint: disable=too-many-instance-attributes """AWS Encryption SDK master key specification utilities. Described in AWS Crypto Tools Test Vector Framework features #0003 and #0004. @@ -84,7 +88,9 @@ class MasterKeySpec(object): """ type_name = attr.ib(validator=membership_validator(KNOWN_TYPES)) - key_name = attr.ib(validator=attr.validators.instance_of(six.string_types)) + key_name = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(six.string_types))) + default_mrk_region = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(six.string_types))) + discovery_filter = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(DiscoveryFilter))) provider_id = attr.ib(validator=attr.validators.optional(attr.validators.instance_of(six.string_types))) encryption_algorithm = attr.ib(validator=attr.validators.optional(membership_validator(KNOWN_ALGORITHMS))) padding_algorithm = attr.ib(validator=attr.validators.optional(membership_validator(KNOWN_PADDING))) @@ -106,6 +112,10 @@ def __attrs_post_init__(self): if self.padding_algorithm == "oaep-mgf1" and self.padding_hash is None: raise ValueError('Padding hash must be specified if padding algorithm is "oaep-mgf1"') + if self.type_name == "aws-kms-mrk-aware-discovery": + if self.default_mrk_region is None: + raise ValueError("Default MRK region is required for MRK-aware discovery master keys") + @classmethod def from_scenario(cls, spec): # type: (MASTER_KEY_SPEC) -> MasterKeySpec @@ -117,13 +127,25 @@ def from_scenario(cls, spec): """ return cls( type_name=spec["type"], - key_name=spec["key"], + key_name=spec.get("key"), + default_mrk_region=spec.get("default-mrk-region"), + discovery_filter=cls._discovery_filter_from_spec(spec.get("aws-kms-discovery-filter")), provider_id=spec.get("provider-id"), encryption_algorithm=spec.get("encryption-algorithm"), padding_algorithm=spec.get("padding-algorithm"), padding_hash=spec.get("padding-hash"), ) + @classmethod + def _discovery_filter_from_spec(cls, spec): + if spec: + return DiscoveryFilter(partition=str(spec["partition"]), account_ids=spec["account-ids"]) + return None + + @classmethod + def _discovery_filter_spec(cls, discovery_filter): + return {"partition": discovery_filter.partition, "account-ids": discovery_filter.account_ids} + def _wrapping_algorithm(self, key_bits): # type: (int) -> WrappingAlgorithm """Determine the correct wrapping algorithm if this is a raw master key. @@ -170,8 +192,8 @@ def _wrapping_key(self, key_spec): key_type = _RAW_ENCRYPTION_KEY_TYPE[key_spec.type_name] return WrappingKey(wrapping_algorithm=algorithm, wrapping_key=material, wrapping_key_type=key_type) - def _raw_master_key_from_spec(self, key_spec): - # type: (KeySpec) -> RawMasterKey + def _raw_master_key_from_spec(self, keys): + # type: (KeysManifest) -> RawMasterKey """Build a raw master key using this specification. :param KeySpec key_spec: Key specification to use with this master key @@ -182,11 +204,12 @@ def _raw_master_key_from_spec(self, key_spec): if not self.type_name == "raw": raise TypeError("This is not a raw master key") + key_spec = keys.key(self.key_name) wrapping_key = self._wrapping_key(key_spec) return RawMasterKey(provider_id=self.provider_id, key_id=key_spec.key_id, wrapping_key=wrapping_key) - def _kms_master_key_from_spec(self, key_spec): - # type: (KeySpec) -> KMSMasterKey + def _kms_master_key_from_spec(self, keys): + # type: (KeysManifest) -> KMSMasterKey """Build an AWS KMS master key using this specification. :param KeySpec key_spec: Key specification to use with this master key @@ -197,9 +220,46 @@ def _kms_master_key_from_spec(self, key_spec): if not self.type_name == "aws-kms": raise TypeError("This is not an AWS KMS master key") + key_spec = keys.key(self.key_name) return KMS_MASTER_KEY_PROVIDER.master_key(key_id=key_spec.key_id) - _MASTER_KEY_LOADERS = {"aws-kms": _kms_master_key_from_spec, "raw": _raw_master_key_from_spec} + def _kms_mrk_aware_master_key_from_spec(self, keys): + # type: (KeysManifest) -> KMSMasterKey + """Build an AWS KMS master key using this specification. + + :param KeySpec key_spec: Key specification to use with this master key + :return: AWS KMS master key based on this specification + :rtype: KMSMasterKey + :raises TypeError: if this is not an AWS KMS master key specification + """ + if not self.type_name == "aws-kms-mrk-aware": + raise TypeError("This is not an AWS KMS MRK-aware master key") + + key_spec = keys.key(self.key_name) + return KMS_MRK_AWARE_MASTER_KEY_PROVIDER.master_key(key_id=key_spec.key_id) + + def _kms_mrk_aware_discovery_master_key_from_spec(self, _keys): + # type: (KeysManifest) -> KMSMasterKey + """Build an AWS KMS master key using this specification. + + :param KeySpec key_spec: Key specification to use with this master key + :return: AWS KMS master key based on this specification + :rtype: KMSMasterKey + :raises TypeError: if this is not an AWS KMS master key specification + """ + if not self.type_name == "aws-kms-mrk-aware-discovery": + raise TypeError("This is not an AWS KMS MRK-aware discovery master key") + + return MRKAwareDiscoveryAwsKmsMasterKeyProvider( + discovery_region=self.default_mrk_region, discovery_filter=self.discovery_filter + ) + + _MASTER_KEY_LOADERS = { + "aws-kms": _kms_master_key_from_spec, + "aws-kms-mrk-aware": _kms_mrk_aware_master_key_from_spec, + "aws-kms-mrk-aware-discovery": _kms_mrk_aware_discovery_master_key_from_spec, + "raw": _raw_master_key_from_spec, + } def master_key(self, keys): # type: (KeysManifest) -> MasterKeyProvider @@ -207,9 +267,8 @@ def master_key(self, keys): :param KeysManifest keys: Loaded key materials """ - key_spec = keys.key(self.key_name) key_loader = self._MASTER_KEY_LOADERS[self.type_name] - return key_loader(self, key_spec) + return key_loader(self, keys) @property def scenario_spec(self): @@ -219,7 +278,13 @@ def scenario_spec(self): :return: Master key specification JSON :rtype: dict """ - spec = {"type": self.type_name, "key": self.key_name} + spec = {"type": self.type_name} + if self.type_name == "aws-kms-mrk-aware-discovery": + spec["default-mrk-region"] = self.default_mrk_region + if self.discovery_filter: + spec["aws-kms-discovery-filter"] = MasterKeySpec._discovery_filter_spec(self.discovery_filter) + else: + spec["key"] = self.key_name if self.type_name != "aws-kms": spec.update( 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 753b83496..e120375d0 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 753b83496aabfe34b8a179d60a62f05975d396bf +Subproject commit e120375d092082d12e2146a8e20980772122fbe3 diff --git a/test_vector_handlers/test/aws-encryption-sdk-test-vectors b/test_vector_handlers/test/aws-encryption-sdk-test-vectors new file mode 160000 index 000000000..5cb6870e3 --- /dev/null +++ b/test_vector_handlers/test/aws-encryption-sdk-test-vectors @@ -0,0 +1 @@ +Subproject commit 5cb6870e3d9e0d7117220c0f0033451818f25e85 diff --git a/tox.ini b/tox.ini index 43030695e..b0221eee2 100644 --- a/tox.ini +++ b/tox.ini @@ -48,6 +48,10 @@ passenv = AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID \ # Identifies a second AWS KMS key id to use in integration tests AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_KEY_ID_2 \ + # Identifies AWS KMS MRK key id to use in integration tests + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_1 \ + # Identifies a related AWS KMS MRK key id to use in integration tests + AWS_ENCRYPTION_SDK_PYTHON_INTEGRATION_TEST_AWS_KMS_MRK_KEY_ID_2 \ # Pass through AWS credentials AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN \ # AWS Role access in CodeBuild is via the contaner URI @@ -177,6 +181,7 @@ deps = commands = pylint \ --rcfile=src/pylintrc \ + --max-module-lines=1500 \ src/aws_encryption_sdk/ \ setup.py