From 500ee4e36e7427b6ca63ab9f750b966155f1acd1 Mon Sep 17 00:00:00 2001 From: robertd Date: Wed, 27 Jan 2021 17:04:48 -0700 Subject: [PATCH] feat(cloudfront): add PublicKey and KeyGroup L2 constructs --- packages/@aws-cdk/aws-cloudfront/README.md | 37 +++++ packages/@aws-cdk/aws-cloudfront/lib/index.ts | 2 + .../@aws-cdk/aws-cloudfront/lib/key-group.ts | 82 +++++++++++ .../@aws-cdk/aws-cloudfront/lib/public-key.ts | 81 +++++++++++ packages/@aws-cdk/aws-cloudfront/package.json | 4 +- .../aws-cloudfront/test/key-group.test.ts | 137 ++++++++++++++++++ .../aws-cloudfront/test/public-key.test.ts | 52 +++++++ 7 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-cloudfront/lib/key-group.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/lib/public-key.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index f42d6a15f7abc..f5fe25a2ffa44 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -520,3 +520,40 @@ new CloudFrontWebDistribution(stack, 'ADistribution', { ], }); ``` + +## KeyGroup & PublicKey API + +Now you can create a key group to use with CloudFront signed URLs and signed cookies. You can add public keys to use with CloudFront features such as signed URLs, signed cookies, and field-level encryption. + +The following example command uses OpenSSL to generate an RSA key pair with a length of 2048 bits and save to the file named `private_key.pem`. + +```bash +openssl genrsa -out private_key.pem 2048 +``` + +The resulting file contains both the public and the private key. The following example command extracts the public key from the file named `private_key.pem` and stores it in `public_key.pem`. + +```bash +openssl rsa -pubout -in private_key.pem -out public_key.pem +``` + +Note: Don't forget to copy/paste the contents of `public_key.pem` file including `-----BEGIN PUBLIC KEY-----` and `-----END PUBLIC KEY-----` lines into `encodedKey` parameter when creating a `PublicKey`. + +Example: +```ts + new cloudfront.KeyGroup(stack, 'MyKeyGroup', { + items: [ + new cloudfront.PublicKey(stack, 'MyPublicKey', { + encodedKey: '...', // contents of public_key.pem file + // comment: 'Key is expiring on ...', + }), + ], + // comment: 'Key group containing public keys ...', + }); +``` + +See: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html + +See: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html#private-content-creating-cloudfront-key-pairs + +See: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/lib/index.ts b/packages/@aws-cdk/aws-cloudfront/lib/index.ts index 726a1d1d01948..7de2aa62b4412 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/index.ts @@ -1,9 +1,11 @@ export * from './cache-policy'; export * from './distribution'; export * from './geo-restriction'; +export * from './key-group'; export * from './origin'; export * from './origin-access-identity'; export * from './origin-request-policy'; +export * from './public-key'; export * from './web-distribution'; export * as experimental from './experimental'; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts b/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts new file mode 100644 index 0000000000000..3ae6b69c3f1ae --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/key-group.ts @@ -0,0 +1,82 @@ +import { IResource, Lazy, Names, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnKeyGroup } from './cloudfront.generated'; +import { IPublicKey } from './public-key'; + +/** + * Represents a Key Group + */ +export interface IKeyGroup extends IResource { + /** + * The ID of the key group. + * @attribute + */ + readonly keyGroupId: string; +} + +/** + * Properties for creating a Public Key + */ +export interface KeyGroupProps { + /** + * A name to identify the key group. + * @default - generated from the `id` + */ + readonly keyGroupName?: string; + + /** + * A comment to describe the key group. + * @default - no comment + */ + readonly comment?: string; + + /** + * A list of the identifiers of the public keys in the key group. + */ + readonly items: IPublicKey[]; +} + +/** + * A Key Group configuration + * + * @resource AWS::CloudFront::KeyGroup + */ +export class KeyGroup extends Resource implements IKeyGroup { + + /** Imports a Key Group from its id. */ + public static fromKeyGroupId(scope: Construct, id: string, keyGroupId: string): IKeyGroup { + return new class extends Resource implements IKeyGroup { + public readonly keyGroupId = keyGroupId; + }(scope, id); + } + + public readonly keyGroupId: string; + + constructor(scope: Construct, id: string, props: KeyGroupProps) { + super(scope, id, { + physicalName: props.keyGroupName ?? + Lazy.string({ produce: () => this.generateName() }), + }); + + const resource = new CfnKeyGroup(this, 'Resource', { + keyGroupConfig: { + name: this.physicalName, + comment: props.comment, + items: this.getKeyIdentifiers(props.items), + }, + }); + this.keyGroupId = resource.ref; + } + + private getKeyIdentifiers(items: IPublicKey[]): string[] { + return items.map(key => key.publicKeyId); + } + + private generateName(): string { + const name = Names.uniqueId(this); + if (name.length > 80) { + return name.substring(0, 40) + name.substring(name.length - 40); + } + return name; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts b/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts new file mode 100644 index 0000000000000..90ce1766d4bad --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/public-key.ts @@ -0,0 +1,81 @@ +import { IResource, Lazy, Names, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnPublicKey } from './cloudfront.generated'; + +/** + * Represents a Public Key + */ +export interface IPublicKey extends IResource { + /** + * The ID of the key group. + * @attribute + */ + readonly publicKeyId: string; +} + +/** + * Properties for creating a Public Key + */ +export interface PublicKeyProps { + /** + * A name to identify the public key. + * @default - generated from the `id` + */ + readonly publicKeyName?: string; + + /** + * A comment to describe the public key. + * @default - no comment + */ + readonly comment?: string; + + /** + * The public key that you can use with signed URLs and signed cookies, or with field-level encryption. + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html + * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/field-level-encryption.html + */ + readonly encodedKey: string; +} + +/** + * A Public Key Configuration + * + * @resource AWS::CloudFront::PublicKey + */ +export class PublicKey extends Resource implements IPublicKey { + + /** Imports a Public Key from its id. */ + public static fromPublicKeyId(scope: Construct, id: string, publicKeyId: string): IPublicKey { + return new class extends Resource implements IPublicKey { + public readonly publicKeyId = publicKeyId; + }(scope, id); + } + + public readonly publicKeyId: string; + + constructor(scope: Construct, id: string, props: PublicKeyProps) { + super(scope, id, { + physicalName: props.publicKeyName ?? + Lazy.string({ produce: () => this.generateName() }), + }); + + const resource = new CfnPublicKey(this, 'Resource', { + publicKeyConfig: { + name: this.physicalName, + callerReference: this.node.addr, + encodedKey: props.encodedKey, + comment: props.comment, + }, + }); + + this.publicKeyId = resource.ref; + } + + private generateName(): string { + const name = Names.uniqueId(this); + if (name.length > 80) { + return name.substring(0, 40) + name.substring(name.length - 40); + } + return name; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 651d22e95e668..c781a065bbc0a 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -153,7 +153,9 @@ "resource-attribute:@aws-cdk/aws-cloudfront.CachePolicy.cachePolicyLastModifiedTime", "construct-interface-extends-iconstruct:@aws-cdk/aws-cloudfront.IOriginRequestPolicy", "resource-interface-extends-resource:@aws-cdk/aws-cloudfront.IOriginRequestPolicy", - "resource-attribute:@aws-cdk/aws-cloudfront.OriginRequestPolicy.originRequestPolicyLastModifiedTime" + "resource-attribute:@aws-cdk/aws-cloudfront.OriginRequestPolicy.originRequestPolicyLastModifiedTime", + "resource-attribute:@aws-cdk/aws-cloudfront.KeyGroup.keyGroupLastModifiedTime", + "resource-attribute:@aws-cdk/aws-cloudfront.PublicKey.publicKeyCreatedTime" ] }, "awscdkio": { diff --git a/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts b/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts new file mode 100644 index 0000000000000..38cb9f70d7fd5 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts @@ -0,0 +1,137 @@ +import '@aws-cdk/assert/jest'; +import { App, Stack } from '@aws-cdk/core'; +import { KeyGroup, PublicKey } from '../lib'; + +describe('KeyGroup', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '123456789012', region: 'testregion' }, + }); + }); + + test('import existing key group by id', () => { + const keyGroupId = '344f6fe5-7ce5-4df0-a470-3f14177c549c'; + const keyGroup = KeyGroup.fromKeyGroupId(stack, 'MyKeyGroup', keyGroupId); + expect(keyGroup.keyGroupId).toEqual(keyGroupId); + }); + + test('minimal example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + items: [ + new PublicKey(stack, 'MyPublicKey', { + encodedKey: 'encoded-key', + }), + ], + }); + + expect(stack).toHaveResource('AWS::CloudFront::KeyGroup', { + KeyGroupConfig: { + Name: 'StackMyKeyGroupC9D82374', + Items: [ + { + Ref: 'MyPublicKey78071F3D', + }, + ], + }, + }); + + expect(stack).toHaveResource('AWS::CloudFront::PublicKey', { + PublicKeyConfig: { + Name: 'StackMyPublicKey36EDA6AB', + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: 'encoded-key', + }, + }); + }); + + test('maximum example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + keyGroupName: 'AcmeKeyGroup', + comment: 'Key group created on 1/1/1984', + items: [ + new PublicKey(stack, 'MyPublicKey', { + publicKeyName: 'pub-key', + encodedKey: 'encoded-key', + comment: 'Key expiring on 1/1/1984', + }), + ], + }); + + expect(stack).toHaveResource('AWS::CloudFront::KeyGroup', { + KeyGroupConfig: { + Name: 'AcmeKeyGroup', + Comment: 'Key group created on 1/1/1984', + Items: [ + { + Ref: 'MyPublicKey78071F3D', + }, + ], + }, + }); + + expect(stack).toHaveResource('AWS::CloudFront::PublicKey', { + PublicKeyConfig: { + Name: 'pub-key', + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: 'encoded-key', + Comment: 'Key expiring on 1/1/1984', + }, + }); + }); + + test('multiple keys example', () => { + new KeyGroup(stack, 'MyKeyGroup', { + keyGroupName: 'AcmeKeyGroup', + comment: 'Key group created on 1/1/1984', + items: [ + new PublicKey(stack, 'MyPublicKey1', { + publicKeyName: 'Bingo-Key', + encodedKey: 'encoded-key', + comment: 'Key expiring on 1/1/1984', + }), + new PublicKey(stack, 'MyPublicKey2', { + publicKeyName: 'Rolly-Key', + encodedKey: 'encoded-key', + comment: 'Key expiring on 1/1/1984', + }), + ], + }); + + expect(stack).toHaveResource('AWS::CloudFront::KeyGroup', { + KeyGroupConfig: { + Name: 'AcmeKeyGroup', + Comment: 'Key group created on 1/1/1984', + Items: [ + { + Ref: 'MyPublicKey153715628', + }, + { + Ref: 'MyPublicKey23469100D', + }, + ], + }, + }); + + expect(stack).toHaveResource('AWS::CloudFront::PublicKey', { + PublicKeyConfig: { + Name: 'Bingo-Key', + CallerReference: 'c81ef73d09656cdf6d0893f1bfb461fa3c13d1b3bb', + EncodedKey: 'encoded-key', + Comment: 'Key expiring on 1/1/1984', + }, + }); + + expect(stack).toHaveResource('AWS::CloudFront::PublicKey', { + PublicKeyConfig: { + Name: 'Rolly-Key', + CallerReference: 'c8730c508b0cf6227f78d85a808a7e2eb2561375ea', + EncodedKey: 'encoded-key', + Comment: 'Key expiring on 1/1/1984', + }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts b/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts new file mode 100644 index 0000000000000..3ad40c13d891f --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/public-key.test.ts @@ -0,0 +1,52 @@ +import '@aws-cdk/assert/jest'; +import { App, Stack } from '@aws-cdk/core'; +import { PublicKey } from '../lib'; + +describe('PublicKey', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '123456789012', region: 'testregion' }, + }); + }); + + test('import existing key group by id', () => { + const publicKeyId = 'K36X4X2EO997HM'; + const publicKey = PublicKey.fromPublicKeyId(stack, 'MyPublicKey', publicKeyId); + expect(publicKey.publicKeyId).toEqual(publicKeyId); + }); + + test('minimal example', () => { + new PublicKey(stack, 'MyPublicKey', { + encodedKey: 'encoded-key', + }); + + expect(stack).toHaveResource('AWS::CloudFront::PublicKey', { + PublicKeyConfig: { + Name: 'StackMyPublicKey36EDA6AB', + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: 'encoded-key', + }, + }); + }); + + test('maximum example', () => { + new PublicKey(stack, 'MyPublicKey', { + publicKeyName: 'pub-key', + encodedKey: 'encoded-key', + comment: 'Key expiring on 1/1/1984', + }); + + expect(stack).toHaveResource('AWS::CloudFront::PublicKey', { + PublicKeyConfig: { + Name: 'pub-key', + CallerReference: 'c872d91ae0d2943aad25d4b31f1304d0a62c658ace', + EncodedKey: 'encoded-key', + Comment: 'Key expiring on 1/1/1984', + }, + }); + }); +}); \ No newline at end of file