diff --git a/packages/@aws-cdk/aws-kms/README.md b/packages/@aws-cdk/aws-kms/README.md index 201accf122675..03670e3dc3a62 100644 --- a/packages/@aws-cdk/aws-kms/README.md +++ b/packages/@aws-cdk/aws-kms/README.md @@ -217,3 +217,15 @@ const key = new kms.Key(this, 'MyKey', { runs the risk of the key becoming unmanageable if that user or role is deleted. It is highly recommended that the key policy grants access to the account root, rather than specific principals. See https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html for more information. + +### HMAC specific key policies + +HMAC keys have a different key policy than other KMS keys. They have a policy for generating and for verifying a MAC. +The respective policies can be attached to a principal via the `grantGenerateMac` and `grantVerifyMac` methods. + +```ts +const key = new kms.Key(this, 'MyKey'); +const user = new iam.User(this, 'MyUser'); +key.grantGenerateMac(user); // Adds 'kms:GenerateMac' to the principal's policy +key.grantVerifyMac(user); // Adds 'kms:VerifyMac' to the principal's policy +``` diff --git a/packages/@aws-cdk/aws-kms/lib/alias.ts b/packages/@aws-cdk/aws-kms/lib/alias.ts index af538f6f17780..4e810c4458fbe 100644 --- a/packages/@aws-cdk/aws-kms/lib/alias.ts +++ b/packages/@aws-cdk/aws-kms/lib/alias.ts @@ -93,6 +93,14 @@ abstract class AliasBase extends Resource implements IAlias { public grantEncryptDecrypt(grantee: iam.IGrantable): iam.Grant { return this.aliasTargetKey.grantEncryptDecrypt(grantee); } + + grantGenerateMac(grantee: iam.IGrantable): iam.Grant { + return this.aliasTargetKey.grantGenerateMac(grantee); + } + + grantVerifyMac(grantee: iam.IGrantable): iam.Grant { + return this.aliasTargetKey.grantVerifyMac(grantee); + } } /** @@ -160,6 +168,8 @@ export class Alias extends AliasBase { public grantDecrypt(grantee: iam.IGrantable): iam.Grant { return iam.Grant.drop(grantee, ''); } public grantEncrypt(grantee: iam.IGrantable): iam.Grant { return iam.Grant.drop(grantee, ''); } public grantEncryptDecrypt(grantee: iam.IGrantable): iam.Grant { return iam.Grant.drop(grantee, ''); } + public grantGenerateMac(grantee: iam.IGrantable): iam.Grant { return iam.Grant.drop(grantee, ''); } + public grantVerifyMac(grantee: iam.IGrantable): iam.Grant { return iam.Grant.drop(grantee, ''); } } return new Import(scope, id); diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index a9626f2b69b4c..b5c6c0c8d57e1 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -1,6 +1,19 @@ import * as iam from '@aws-cdk/aws-iam'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; -import { FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, ResourceProps, Stack, Duration, Token, ContextProvider, Arn, ArnFormat } from '@aws-cdk/core'; +import { + Arn, + ArnFormat, + ContextProvider, + Duration, + FeatureFlags, + IResource, + Lazy, + RemovalPolicy, + Resource, + ResourceProps, + Stack, + Token, +} from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { Alias } from './alias'; @@ -60,6 +73,16 @@ export interface IKey extends IResource { * Grant encryption and decryption permissions using this key to the given principal */ grantEncryptDecrypt(grantee: iam.IGrantable): iam.Grant; + + /** + * Grant permissions to generating MACs to the given principal + */ + grantGenerateMac(grantee: iam.IGrantable): iam.Grant + + /** + * Grant permissions to verifying MACs to the given principal + */ + grantVerifyMac(grantee: iam.IGrantable): iam.Grant } abstract class KeyBase extends Resource implements IKey { @@ -193,6 +216,20 @@ abstract class KeyBase extends Resource implements IKey { return this.grant(grantee, ...[...perms.DECRYPT_ACTIONS, ...perms.ENCRYPT_ACTIONS]); } + /** + * Grant permissions to generating MACs to the given principal + */ + public grantGenerateMac(grantee: iam.IGrantable): iam.Grant { + return this.grant(grantee, ...perms.GENERATE_HMAC_ACTIONS); + } + + /** + * Grant permissions to verifying MACs to the given principal + */ + public grantVerifyMac(grantee: iam.IGrantable): iam.Grant { + return this.grant(grantee, ...perms.VERIFY_HMAC_ACTIONS); + } + /** * Checks whether the grantee belongs to a stack that will be deployed * after the stack containing this key. @@ -300,6 +337,41 @@ export enum KeySpec { * Valid usage: SIGN_VERIFY */ ECC_SECG_P256K1 = 'ECC_SECG_P256K1', + + /** + * Hash-Based Message Authentication Code as defined in RFC 2104 using the message digest function SHA224. + * + * Valid usage: GENERATE_VERIFY_MAC + */ + HMAC_224 = 'HMAC_224', + + /** + * Hash-Based Message Authentication Code as defined in RFC 2104 using the message digest function SHA256. + * + * Valid usage: GENERATE_VERIFY_MAC + */ + HMAC_256 = 'HMAC_256', + + /** + * Hash-Based Message Authentication Code as defined in RFC 2104 using the message digest function SHA384. + * + * Valid usage: GENERATE_VERIFY_MAC + */ + HMAC_384 = 'HMAC_384', + + /** + * Hash-Based Message Authentication Code as defined in RFC 2104 using the message digest function SHA512. + * + * Valid usage: GENERATE_VERIFY_MAC + */ + HMAC_512 = 'HMAC_512', + + /** + * Elliptic curve key spec available only in China Regions. + * + * Valid usage: ENCRYPT_DECRYPT and SIGN_VERIFY + */ + SM2 = 'SM2', } /** @@ -315,6 +387,11 @@ export enum KeyUsage { * Signing and verification */ SIGN_VERIFY = 'SIGN_VERIFY', + + /** + * Generating and verifying MACs + */ + GENERATE_VERIFY_MAC = 'GENERATE_VERIFY_MAC', } /** @@ -595,9 +672,28 @@ export class Key extends KeyBase { KeySpec.ECC_NIST_P384, KeySpec.ECC_NIST_P521, KeySpec.ECC_SECG_P256K1, + KeySpec.HMAC_224, + KeySpec.HMAC_256, + KeySpec.HMAC_384, + KeySpec.HMAC_512, ], [KeyUsage.SIGN_VERIFY]: [ KeySpec.SYMMETRIC_DEFAULT, + KeySpec.HMAC_224, + KeySpec.HMAC_256, + KeySpec.HMAC_384, + KeySpec.HMAC_512, + ], + [KeyUsage.GENERATE_VERIFY_MAC]: [ + KeySpec.RSA_2048, + KeySpec.RSA_3072, + KeySpec.RSA_4096, + KeySpec.ECC_NIST_P256, + KeySpec.ECC_NIST_P384, + KeySpec.ECC_NIST_P521, + KeySpec.ECC_SECG_P256K1, + KeySpec.SYMMETRIC_DEFAULT, + KeySpec.SM2, ], }; const keySpec = props.keySpec ?? KeySpec.SYMMETRIC_DEFAULT; @@ -606,6 +702,10 @@ export class Key extends KeyBase { throw new Error(`key spec '${keySpec}' is not valid with usage '${keyUsage}'`); } + if (keySpec.startsWith('HMAC') && props.enableKeyRotation) { + throw new Error('key rotation cannot be enabled on HMAC keys'); + } + if (keySpec !== KeySpec.SYMMETRIC_DEFAULT && props.enableKeyRotation) { throw new Error('key rotation cannot be enabled on asymmetric keys'); } diff --git a/packages/@aws-cdk/aws-kms/lib/private/perms.ts b/packages/@aws-cdk/aws-kms/lib/private/perms.ts index fe510359d7e89..eb869b9762f7e 100644 --- a/packages/@aws-cdk/aws-kms/lib/private/perms.ts +++ b/packages/@aws-cdk/aws-kms/lib/private/perms.ts @@ -26,3 +26,11 @@ export const ENCRYPT_ACTIONS = [ export const DECRYPT_ACTIONS = [ 'kms:Decrypt', ]; + +export const GENERATE_HMAC_ACTIONS = [ + 'kms:GenerateMac', +]; + +export const VERIFY_HMAC_ACTIONS = [ + 'kms:VerifyMac', +]; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/package.json b/packages/@aws-cdk/aws-kms/package.json index 239f406d0dc44..8722eea4ec607 100644 --- a/packages/@aws-cdk/aws-kms/package.json +++ b/packages/@aws-cdk/aws-kms/package.json @@ -83,6 +83,7 @@ "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", + "@aws-cdk/integ-tests": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-kms/test/alias.test.ts b/packages/@aws-cdk/aws-kms/test/alias.test.ts index cd5d9723ed4db..da866603e4360 100644 --- a/packages/@aws-cdk/aws-kms/test/alias.test.ts +++ b/packages/@aws-cdk/aws-kms/test/alias.test.ts @@ -1,5 +1,6 @@ import { Template } from '@aws-cdk/assertions'; import { ArnPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; +import * as iam from '@aws-cdk/aws-iam'; import { App, CfnOutput, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Alias } from '../lib/alias'; @@ -215,3 +216,50 @@ test('fails if alias policy is invalid', () => { expect(() => app.synth()).toThrow(/A PolicyStatement must specify at least one \'action\' or \'notAction\'/); }); + +test('grants generate mac to the alias target key', () => { + const app = new App(); + const stack = new Stack(app, 'my-stack'); + const key = new Key(stack, 'Key'); + const alias = new Alias(stack, 'Alias', { targetKey: key, aliasName: 'alias/foo' }); + const user = new iam.User(stack, 'User'); + + alias.grantGenerateMac(user); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:GenerateMac', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + +test('grants generate mac to the alias target key', () => { + const app = new App(); + const stack = new Stack(app, 'my-stack'); + const key = new Key(stack, 'Key'); + const alias = new Alias(stack, 'Alias', { targetKey: key, aliasName: 'alias/foo' }); + const user = new iam.User(stack, 'User'); + + alias.grantVerifyMac(user); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:VerifyMac', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }, + ], + Version: '2012-10-17', + }, + }); +}); + diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/HmacIntegTestDefaultTestDeployAssert10C19CC4.assets.json b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/HmacIntegTestDefaultTestDeployAssert10C19CC4.assets.json new file mode 100644 index 0000000000000..c435bd0c59cd4 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/HmacIntegTestDefaultTestDeployAssert10C19CC4.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "HmacIntegTestDefaultTestDeployAssert10C19CC4.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/HmacIntegTestDefaultTestDeployAssert10C19CC4.template.json b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/HmacIntegTestDefaultTestDeployAssert10C19CC4.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/HmacIntegTestDefaultTestDeployAssert10C19CC4.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/aws-cdk-kms-hmac.assets.json b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/aws-cdk-kms-hmac.assets.json new file mode 100644 index 0000000000000..3323f12e22807 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/aws-cdk-kms-hmac.assets.json @@ -0,0 +1,19 @@ +{ + "version": "29.0.0", + "files": { + "9842a0bf1dbc91e927911dc4706da97c827206f07263b9d3ba005b2e5cebfc61": { + "source": { + "path": "aws-cdk-kms-hmac.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "9842a0bf1dbc91e927911dc4706da97c827206f07263b9d3ba005b2e5cebfc61.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/aws-cdk-kms-hmac.template.json b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/aws-cdk-kms-hmac.template.json new file mode 100644 index 0000000000000..ab0e364fee31b --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/aws-cdk-kms-hmac.template.json @@ -0,0 +1,136 @@ +{ + "Resources": { + "Role1ABCC5F0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "RoleDefaultPolicy5FFB7DAB": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:GenerateMac", + "kms:VerifyMac" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyHmacKey32477643", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "RoleDefaultPolicy5FFB7DAB", + "Roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + }, + "MyHmacKey32477643": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "KeySpec": "HMAC_512", + "KeyUsage": "GENERATE_VERIFY_MAC" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/cdk.out b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/cdk.out new file mode 100644 index 0000000000000..d8b441d447f8a --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"29.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/integ.json b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/integ.json new file mode 100644 index 0000000000000..e113cade888b8 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "29.0.0", + "testCases": { + "HmacIntegTest/DefaultTest": { + "stacks": [ + "aws-cdk-kms-hmac" + ], + "assertionStack": "HmacIntegTest/DefaultTest/DeployAssert", + "assertionStackName": "HmacIntegTestDefaultTestDeployAssert10C19CC4" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/manifest.json b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/manifest.json new file mode 100644 index 0000000000000..20656f89fd573 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/manifest.json @@ -0,0 +1,123 @@ +{ + "version": "29.0.0", + "artifacts": { + "aws-cdk-kms-hmac.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-kms-hmac.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-kms-hmac": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-kms-hmac.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/9842a0bf1dbc91e927911dc4706da97c827206f07263b9d3ba005b2e5cebfc61.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-kms-hmac.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-kms-hmac.assets" + ], + "metadata": { + "/aws-cdk-kms-hmac/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Role1ABCC5F0" + } + ], + "/aws-cdk-kms-hmac/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "RoleDefaultPolicy5FFB7DAB" + } + ], + "/aws-cdk-kms-hmac/MyHmacKey/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyHmacKey32477643" + } + ], + "/aws-cdk-kms-hmac/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-kms-hmac/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-kms-hmac" + }, + "HmacIntegTestDefaultTestDeployAssert10C19CC4.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "HmacIntegTestDefaultTestDeployAssert10C19CC4.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "HmacIntegTestDefaultTestDeployAssert10C19CC4": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "HmacIntegTestDefaultTestDeployAssert10C19CC4.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "HmacIntegTestDefaultTestDeployAssert10C19CC4.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "HmacIntegTestDefaultTestDeployAssert10C19CC4.assets" + ], + "metadata": { + "/HmacIntegTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/HmacIntegTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "HmacIntegTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/tree.json b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/tree.json new file mode 100644 index 0000000000000..a037a6bccf386 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.js.snapshot/tree.json @@ -0,0 +1,259 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-kms-hmac": { + "id": "aws-cdk-kms-hmac", + "path": "aws-cdk-kms-hmac", + "children": { + "Role": { + "id": "Role", + "path": "aws-cdk-kms-hmac/Role", + "children": { + "ImportRole": { + "id": "ImportRole", + "path": "aws-cdk-kms-hmac/Role/ImportRole", + "constructInfo": { + "fqn": "@aws-cdk/core.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-kms-hmac/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-kms-hmac/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-kms-hmac/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "kms:GenerateMac", + "kms:VerifyMac" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyHmacKey32477643", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "RoleDefaultPolicy5FFB7DAB", + "roles": [ + { + "Ref": "Role1ABCC5F0" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "MyHmacKey": { + "id": "MyHmacKey", + "path": "aws-cdk-kms-hmac/MyHmacKey", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-kms-hmac/MyHmacKey/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Key", + "aws:cdk:cloudformation:props": { + "keyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "keySpec": "HMAC_512", + "keyUsage": "GENERATE_VERIFY_MAC" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-kms.CfnKey", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-kms.Key", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-kms-hmac/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-kms-hmac/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "HmacIntegTest": { + "id": "HmacIntegTest", + "path": "HmacIntegTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "HmacIntegTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "HmacIntegTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.216" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "HmacIntegTest/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "HmacIntegTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "HmacIntegTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.216" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key-hmac.ts b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.ts new file mode 100644 index 0000000000000..6becea2144a38 --- /dev/null +++ b/packages/@aws-cdk/aws-kms/test/integ.key-hmac.ts @@ -0,0 +1,31 @@ +import { AccountRootPrincipal, Role } from '@aws-cdk/aws-iam'; +import { App, RemovalPolicy, Stack } from '@aws-cdk/core'; +import { IntegTest } from '@aws-cdk/integ-tests'; +import { Key, KeySpec, KeyUsage } from '../lib'; + +const app = new App(); + +const stack = new Stack(app, 'aws-cdk-kms-hmac'); + +const role = new Role(stack, 'Role', { + assumedBy: new AccountRootPrincipal(), +}); + +const key = new Key(stack, 'MyHmacKey', { + removalPolicy: RemovalPolicy.DESTROY, + keyUsage: KeyUsage.GENERATE_VERIFY_MAC, + keySpec: KeySpec.HMAC_512, +}); + +key.grantGenerateMac(role); +key.grantVerifyMac(role); + +new IntegTest(app, 'HmacIntegTest', { + testCases: [ + stack, + ], +}); + +app.synth(); + + diff --git a/packages/@aws-cdk/aws-kms/test/key.test.ts b/packages/@aws-cdk/aws-kms/test/key.test.ts index 33e3361e1bf9a..94bb56f6c0c14 100644 --- a/packages/@aws-cdk/aws-kms/test/key.test.ts +++ b/packages/@aws-cdk/aws-kms/test/key.test.ts @@ -3,6 +3,7 @@ import * as iam from '@aws-cdk/aws-iam'; import { describeDeprecated } from '@aws-cdk/cdk-build-tools'; import * as cdk from '@aws-cdk/core'; import * as kms from '../lib'; +import { KeySpec, KeyUsage } from '../lib'; const ADMIN_ACTIONS: string[] = [ 'kms:Create*', @@ -966,16 +967,17 @@ describe('key specs and key usages', () => { }); }); - test('invalid combinations of key specs and key usages', () => { + test.each(generateInvalidKeySpecKeyUsageCombinations())('invalid combinations of key specs and key usages (%s)', ({ keySpec, keyUsage }) => { const stack = new cdk.Stack(); - expect(() => new kms.Key(stack, 'Key1', { keySpec: kms.KeySpec.ECC_NIST_P256 })) - .toThrow('key spec \'ECC_NIST_P256\' is not valid with usage \'ENCRYPT_DECRYPT\''); - expect(() => new kms.Key(stack, 'Key2', { keySpec: kms.KeySpec.ECC_SECG_P256K1, keyUsage: kms.KeyUsage.ENCRYPT_DECRYPT })) - .toThrow('key spec \'ECC_SECG_P256K1\' is not valid with usage \'ENCRYPT_DECRYPT\''); - expect(() => new kms.Key(stack, 'Key3', { keySpec: kms.KeySpec.SYMMETRIC_DEFAULT, keyUsage: kms.KeyUsage.SIGN_VERIFY })) - .toThrow('key spec \'SYMMETRIC_DEFAULT\' is not valid with usage \'SIGN_VERIFY\''); - expect(() => new kms.Key(stack, 'Key4', { keyUsage: kms.KeyUsage.SIGN_VERIFY })) + expect(() => new kms.Key(stack, 'Key1', { keySpec, keyUsage })) + .toThrow(`key spec \'${keySpec}\' is not valid with usage \'${keyUsage.toString()}\'`); + }); + + test('invalid combinations of default key spec and key usage SIGN_VERIFY', () => { + const stack = new cdk.Stack(); + + expect(() => new kms.Key(stack, 'Key1', { keyUsage: KeyUsage.SIGN_VERIFY })) .toThrow('key spec \'SYMMETRIC_DEFAULT\' is not valid with usage \'SIGN_VERIFY\''); }); @@ -1017,3 +1019,242 @@ describe('Key.fromKeyArn()', () => { }); }); }); + +describe('HMAC', () => { + let stack: cdk.Stack; + + beforeEach(() => { + stack = new cdk.Stack(); + }); + + test.each([ + [KeySpec.HMAC_224, 'HMAC_224'], + [KeySpec.HMAC_256, 'HMAC_256'], + [KeySpec.HMAC_384, 'HMAC_384'], + [KeySpec.HMAC_512, 'HMAC_512'], + ])('%s is not valid for default usage', (keySpec: KeySpec) => { + expect(() => new kms.Key(stack, 'Key1', { keySpec })) + .toThrow(`key spec \'${keySpec}\' is not valid with usage \'ENCRYPT_DECRYPT\'`); + }); + + test.each([ + [KeySpec.HMAC_224, 'HMAC_224'], + [KeySpec.HMAC_256, 'HMAC_256'], + [KeySpec.HMAC_384, 'HMAC_384'], + [KeySpec.HMAC_512, 'HMAC_512'], + ])('%s can not be used with key rotation', (keySpec: KeySpec) => { + expect(() => new kms.Key(stack, 'Key', { + keySpec, + keyUsage: KeyUsage.GENERATE_VERIFY_MAC, + enableKeyRotation: true, + })).toThrow('key rotation cannot be enabled on HMAC keys'); + }); + + test.each([ + [KeySpec.HMAC_224, 'HMAC_224'], + [KeySpec.HMAC_256, 'HMAC_256'], + [KeySpec.HMAC_384, 'HMAC_384'], + [KeySpec.HMAC_512, 'HMAC_512'], + ])('%s can be used for KMS key creation', (keySpec: KeySpec, expected: string) => { + new kms.Key(stack, 'Key', { + keySpec, + keyUsage: KeyUsage.GENERATE_VERIFY_MAC, + }); + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { + KeySpec: expected, + KeyUsage: 'GENERATE_VERIFY_MAC', + }); + }); + + test('grant generate mac policy', () => { + const key = new kms.Key(stack, 'Key', { + keySpec: KeySpec.HMAC_256, + keyUsage: KeyUsage.GENERATE_VERIFY_MAC, + }); + const user = new iam.User(stack, 'User'); + + key.grantGenerateMac(user); + + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] } }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:GenerateMac', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('grant verify mac policy', () => { + const key = new kms.Key(stack, 'Key', { + keySpec: KeySpec.HMAC_256, + keyUsage: KeyUsage.GENERATE_VERIFY_MAC, + }); + const user = new iam.User(stack, 'User'); + + key.grantVerifyMac(user); + + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + { + Action: 'kms:*', + Effect: 'Allow', + Principal: { AWS: { 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] } }, + Resource: '*', + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:VerifyMac', + Effect: 'Allow', + Resource: { 'Fn::GetAtt': ['Key961B73FD', 'Arn'] }, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('grant generate mac policy for imported key', () => { + const keyArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'; + const key = kms.Key.fromKeyArn( + stack, + 'Key', + keyArn, + ); + const user = new iam.User(stack, 'User'); + + key.grantGenerateMac(user); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:GenerateMac', + Effect: 'Allow', + Resource: keyArn, + }, + ], + Version: '2012-10-17', + }, + }); + }); + + test('grant verify mac policy for imported key', () => { + const keyArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'; + const key = kms.Key.fromKeyArn( + stack, + 'Key', + keyArn, + ); + const user = new iam.User(stack, 'User'); + + key.grantVerifyMac(user); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: 'kms:VerifyMac', + Effect: 'Allow', + Resource: keyArn, + }, + ], + Version: '2012-10-17', + }, + }); + }); +}); + +describe('SM2', () => { + let stack: cdk.Stack; + + beforeEach(() => { + stack = new cdk.Stack(); + }); + + test('can be used for KMS key creation', () => { + new kms.Key(stack, 'Key1', { + keySpec: KeySpec.SM2, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { + KeySpec: 'SM2', + }); + }); +}); + + +function generateInvalidKeySpecKeyUsageCombinations() { + // Copied from Key class + const denyLists = { + [KeyUsage.ENCRYPT_DECRYPT]: [ + KeySpec.ECC_NIST_P256, + KeySpec.ECC_NIST_P384, + KeySpec.ECC_NIST_P521, + KeySpec.ECC_SECG_P256K1, + KeySpec.HMAC_224, + KeySpec.HMAC_256, + KeySpec.HMAC_384, + KeySpec.HMAC_512, + ], + [KeyUsage.SIGN_VERIFY]: [ + KeySpec.SYMMETRIC_DEFAULT, + KeySpec.HMAC_224, + KeySpec.HMAC_256, + KeySpec.HMAC_384, + KeySpec.HMAC_512, + ], + [KeyUsage.GENERATE_VERIFY_MAC]: [ + KeySpec.RSA_2048, + KeySpec.RSA_3072, + KeySpec.RSA_4096, + KeySpec.ECC_NIST_P256, + KeySpec.ECC_NIST_P384, + KeySpec.ECC_NIST_P521, + KeySpec.ECC_SECG_P256K1, + KeySpec.SYMMETRIC_DEFAULT, + KeySpec.SM2, + ], + }; + const testCases: { keySpec: KeySpec, keyUsage: KeyUsage, toString: () => string }[] = []; + for (const keySpec in KeySpec) { + for (const keyUsage in KeyUsage) { + if (denyLists[keyUsage as KeyUsage].includes(keySpec as KeySpec)) { + testCases.push({ + keySpec: keySpec as KeySpec, + keyUsage: keyUsage as KeyUsage, + toString: () => `${keySpec} can not be used for ${keyUsage}`, + }); + } + } + } + // Sorting for debugging purposes to see if test cases match deny list + testCases.sort((a, b) => a.keyUsage.localeCompare(b.keyUsage)); + return testCases; +}