Skip to content

Commit

Permalink
feat(cloudfront): add PublicKey and KeyGroup L2 constructs
Browse files Browse the repository at this point in the history
  • Loading branch information
robertd committed Jan 29, 2021
1 parent 6e2c0e9 commit 500ee4e
Show file tree
Hide file tree
Showing 7 changed files with 394 additions and 1 deletion.
37 changes: 37 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/lib/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
82 changes: 82 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/lib/key-group.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
81 changes: 81 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/lib/public-key.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-cloudfront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
137 changes: 137 additions & 0 deletions packages/@aws-cdk/aws-cloudfront/test/key-group.test.ts
Original file line number Diff line number Diff line change
@@ -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',
},
});
});
});
Loading

0 comments on commit 500ee4e

Please sign in to comment.