Skip to content

Commit

Permalink
feat(certificatemanager): Allow opting out of transparency logging (#…
Browse files Browse the repository at this point in the history
…21686)

Certificates created with AWS Certificate Manager are recorded in a certificate transparency log. ACM however allows you to opt of out of transparency logging. This feature enables certificates created in ACM through CDK to opt out of transparency logging.

### All Submissions:

* [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### Adding new Unconventional Dependencies:

* [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies)

### New Features

* [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
AKoetsier authored Aug 23, 2022
1 parent e039b51 commit 85b6db0
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 2 deletions.
11 changes: 11 additions & 0 deletions packages/@aws-cdk/aws-certificatemanager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ new acm.PrivateCertificate(this, 'PrivateCertificate', {
});
```

## Requesting certificates without transparency logging

Transparency logging can be opted out of for AWS Certificate Manager certificates. See [opting out of certifiacte transparency logging](https://docs.aws.amazon.com/acm/latest/userguide/acm-bestpractices.html#best-practices-transparency) for limits.

```ts
new acm.Certificate(this, 'Certificate', {
domainName: 'test.example.com',
transparencyLoggingEnabled: false,
});
```

## Importing

If you want to import an existing certificate, you can do so from its ARN:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ let report = function (event, context, responseStatus, physicalResourceId, respo
* @param {map} tags Tags to add to the requested certificate
* @returns {string} Validated certificate ARN
*/
const requestCertificate = async function (requestId, domainName, subjectAlternativeNames, hostedZoneId, region, route53Endpoint, tags) {
const requestCertificate = async function (requestId, domainName, subjectAlternativeNames, certificateTransparencyLoggingPreference, hostedZoneId, region, route53Endpoint, tags) {
const crypto = require('crypto');
const acm = new aws.ACM({ region });
const route53 = route53Endpoint ? new aws.Route53({ endpoint: route53Endpoint }) : new aws.Route53();
Expand All @@ -92,6 +92,9 @@ const requestCertificate = async function (requestId, domainName, subjectAlterna
const reqCertResponse = await acm.requestCertificate({
DomainName: domainName,
SubjectAlternativeNames: subjectAlternativeNames,
Options: {
CertificateTransparencyLoggingPreference: certificateTransparencyLoggingPreference
},
IdempotencyToken: crypto.createHash('sha256').update(requestId).digest('hex').slice(0, 32),
ValidationMethod: 'DNS'
}).promise();
Expand Down Expand Up @@ -288,6 +291,7 @@ exports.certificateRequestHandler = async function (event, context) {
event.RequestId,
event.ResourceProperties.DomainName,
event.ResourceProperties.SubjectAlternativeNames,
event.ResourceProperties.CertificateTransparencyLoggingPreference,
event.ResourceProperties.HostedZoneId,
event.ResourceProperties.Region,
event.ResourceProperties.Route53Endpoint,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,10 @@ describe('DNS Validated Certificate Handler', () => {
.expectResolve(() => {
sinon.assert.calledWith(requestCertificateFake, sinon.match({
DomainName: testDomainName,
ValidationMethod: 'DNS'
ValidationMethod: 'DNS',
Options: {
CertificateTransparencyLoggingPreference: undefined
}
}));
sinon.assert.calledWith(changeResourceRecordSetsFake, sinon.match({
ChangeBatch: {
Expand Down Expand Up @@ -731,6 +734,72 @@ describe('DNS Validated Certificate Handler', () => {
});
});

test('Create operation with `CertificateTransparencyLoggingPreference` requests a certificate with that preference set', () => {
const requestCertificateFake = sinon.fake.resolves({
CertificateArn: testCertificateArn,
});

const describeCertificateFake = sinon.stub();
describeCertificateFake.onFirstCall().resolves({
Certificate: {
CertificateArn: testCertificateArn
}
});
describeCertificateFake.resolves({
Certificate: {
CertificateArn: testCertificateArn,
DomainValidationOptions: [{
ValidationStatus: 'SUCCESS',
ResourceRecord: {
Name: testRRName,
Type: 'CNAME',
Value: testRRValue
}
}]
}
});

const addTagsToCertificateFake = sinon.fake.resolves({});

const changeResourceRecordSetsFake = sinon.fake.resolves({
ChangeInfo: {
Id: 'bogus'
}
});

AWS.mock('ACM', 'requestCertificate', requestCertificateFake);
AWS.mock('ACM', 'describeCertificate', describeCertificateFake);
AWS.mock('Route53', 'changeResourceRecordSets', changeResourceRecordSetsFake);
AWS.mock('ACM', 'addTagsToCertificate', addTagsToCertificateFake);

const request = nock(ResponseURL).put('/', body => {
return body.Status === 'SUCCESS';
}).reply(200);

return LambdaTester(handler.certificateRequestHandler)
.event({
RequestType: 'Create',
RequestId: testRequestId,
ResourceProperties: {
DomainName: testDomainName,
HostedZoneId: testHostedZoneId,
Region: 'us-east-1',
CertificateTransparencyLoggingPreference: 'DISABLED',
Tags: testTags
}
})
.expectResolve(() => {
sinon.assert.calledWith(requestCertificateFake, sinon.match({
DomainName: testDomainName,
ValidationMethod: 'DNS',
Options: {
CertificateTransparencyLoggingPreference: 'DISABLED'
}
}));
expect(request.isDone()).toBe(true);
});
});

test('Delete operation deletes the certificate', () => {
const describeCertificateFake = sinon.fake.resolves({
Certificate: {
Expand Down
21 changes: 21 additions & 0 deletions packages/@aws-cdk/aws-certificatemanager/lib/certificate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ export interface CertificateProps {
* @default CertificateValidation.fromEmail()
*/
readonly validation?: CertificateValidation;

/**
* Enable or disable transparency logging for this certificate
*
* Once a certificate has been logged, it cannot be removed from the log.
* Opting out at that point will have no effect. If you opt out of logging
* when you request a certificate and then choose later to opt back in,
* your certificate will not be logged until it is renewed.
* If you want the certificate to be logged immediately, we recommend that you issue a new one.
*
* @see https://docs.aws.amazon.com/acm/latest/userguide/acm-bestpractices.html#best-practices-transparency
*
* @default true
*/
readonly transparencyLoggingEnabled?: boolean;
}

/**
Expand Down Expand Up @@ -214,11 +229,17 @@ export class Certificate extends CertificateBase implements ICertificate {

const allDomainNames = [props.domainName].concat(props.subjectAlternativeNames || []);

let certificateTransparencyLoggingPreference: string | undefined;
if (props.transparencyLoggingEnabled !== undefined) {
certificateTransparencyLoggingPreference = props.transparencyLoggingEnabled ? 'ENABLED' : 'DISABLED';
}

const cert = new CfnCertificate(this, 'Resource', {
domainName: props.domainName,
subjectAlternativeNames: props.subjectAlternativeNames,
domainValidationOptions: renderDomainValidation(validation, allDomainNames),
validationMethod: validation.method,
certificateTransparencyLoggingPreference,
});

this.certificateArn = cert.ref;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ export class DnsValidatedCertificate extends CertificateBase implements ICertifi
this.hostedZoneId = props.hostedZone.hostedZoneId.replace(/^\/hostedzone\//, '');
this.tags = new cdk.TagManager(cdk.TagType.MAP, 'AWS::CertificateManager::Certificate');

let certificateTransparencyLoggingPreference: string | undefined;
if (props.transparencyLoggingEnabled !== undefined) {
certificateTransparencyLoggingPreference = props.transparencyLoggingEnabled ? 'ENABLED' : 'DISABLED';
}

const requestorFunction = new lambda.Function(this, 'CertificateRequestorFunction', {
code: lambda.Code.fromAsset(path.resolve(__dirname, '..', 'lambda-packages', 'dns_validated_certificate_handler', 'lib')),
handler: 'index.certificateRequestHandler',
Expand All @@ -121,6 +126,7 @@ export class DnsValidatedCertificate extends CertificateBase implements ICertifi
properties: {
DomainName: props.domainName,
SubjectAlternativeNames: cdk.Lazy.list({ produce: () => props.subjectAlternativeNames }, { omitEmpty: true }),
CertificateTransparencyLoggingPreference: certificateTransparencyLoggingPreference,
HostedZoneId: this.hostedZoneId,
Region: props.region,
Route53Endpoint: props.route53Endpoint,
Expand Down
42 changes: 42 additions & 0 deletions packages/@aws-cdk/aws-certificatemanager/test/certificate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,45 @@ test('CertificateValidation.fromDnsMultiZone', () => {
ValidationMethod: 'DNS',
});
});

describe('Transparency logging settings', () => {
test('leaves transparency logging untouched by default', () => {
const stack = new Stack();

new Certificate(stack, 'Certificate', {
domainName: 'test.example.com',
});

const certificateNodes = Template.fromStack(stack).findResources('AWS::CertificateManager::Certificate');
expect(certificateNodes.Certificate4E7ABB08).toBeDefined();
expect(certificateNodes.Certificate4E7ABB08.CertificateTransparencyLoggingPreference).toBeUndefined();
});

test('can enable transparency logging', () => {
const stack = new Stack();

new Certificate(stack, 'Certificate', {
domainName: 'test.example.com',
transparencyLoggingEnabled: true,
});

Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', {
DomainName: 'test.example.com',
CertificateTransparencyLoggingPreference: 'ENABLED',
});
});

test('can disable transparency logging', () => {
const stack = new Stack();

new Certificate(stack, 'Certificate', {
domainName: 'test.example.com',
transparencyLoggingEnabled: false,
});

Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', {
DomainName: 'test.example.com',
CertificateTransparencyLoggingPreference: 'DISABLED',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,31 @@ test('works with imported role', () => {
Role: 'arn:aws:iam::account-id:role/role-name',
});
});

test('test transparency logging settings is passed to the custom resource', () => {
const stack = new Stack();

const exampleDotComZone = new PublicHostedZone(stack, 'ExampleDotCom', {
zoneName: 'example.com',
});

new DnsValidatedCertificate(stack, 'Cert', {
domainName: 'example.com',
hostedZone: exampleDotComZone,
transparencyLoggingEnabled: false,
});

Template.fromStack(stack).hasResourceProperties('AWS::CloudFormation::CustomResource', {
ServiceToken: {
'Fn::GetAtt': [
'CertCertificateRequestorFunction98FDF273',
'Arn',
],
},
DomainName: 'example.com',
HostedZoneId: {
Ref: 'ExampleDotCom4D1B83AA',
},
CertificateTransparencyLoggingPreference: 'DISABLED',
});
});

0 comments on commit 85b6db0

Please sign in to comment.