Skip to content

Commit

Permalink
Add certificate chain validation & bump version to 2.1.0 (#622)
Browse files Browse the repository at this point in the history
  • Loading branch information
ShenChen93 committed May 12, 2020
1 parent 7c03ab5 commit 32810f3
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 2 deletions.
6 changes: 6 additions & 0 deletions ask-sdk-express-adapter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change Log

# 2.1.0 (2020-05-07)

This release contains the following changes :

- Add certificate chain validation by using node-forge pacakge.

# 2.0.1 (2020-01-22)

This release contains the following changes :
Expand Down
55 changes: 55 additions & 0 deletions ask-sdk-express-adapter/lib/verifier/helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2018 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://www.apache.org/licenses/LICENSE-2.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.
*/

import { pki } from 'node-forge';

/**
* Function used to convert certificate chain string into Certificate Object Array
* @param {string} certChain certificate chain in pem format
* @return {pki.Certificate[]}
*/

const CERT_START_KEY = '-----BEGIN CERTIFICATE-----';
const CERT_END_KEY = '-----END CERTIFICATE-----';
export function generateCertificatesArray(certChain : string) : pki.Certificate[] {
const certs = [];
while (certChain.length > 0) {
const start = certChain.indexOf(CERT_START_KEY);
const end = certChain.indexOf(CERT_END_KEY) + CERT_END_KEY.length;
const certString = certChain.slice(start, end);
certs.push(pki.certificateFromPem(certString));
certChain = certChain.slice(end).trim();
}

return certs;
}

/**
* Function used to generate ca store based on input root CAs list
* @param {string[]} certs root CAs in pem format
*/
export function generateCAStore(certs : string[]) : pki.CAStore {
const caStore = pki.createCaStore([]);

for (const cert of certs) {
try {
caStore.addCertificate(cert);
} catch (e) {
// do nothing
// node-forge doesn't support ECDSA encryped pem
}

}

return caStore;
}
16 changes: 16 additions & 0 deletions ask-sdk-express-adapter/lib/verifier/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import * as client from 'https';
import { pki } from 'node-forge';
import * as url from 'url';

import { generateCAStore, generateCertificatesArray } from './helper';

/**
* Provide constant value
* For more info, check `link <https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-a-web-service.html#checking-the-signature-of-the-request>
Expand Down Expand Up @@ -272,6 +274,20 @@ export class SkillRequestSignatureVerifier implements Verifier {
`${CERT_CHAIN_DOMAIN} domain missing in Signature Certificate Chain.`,
);
}
// Use the pki.verifyCertificateChain function from Node-forge to
// validate that all certificates in the chain combine to create a chain of trust to a trusted root CA certificate
// TODO: Implement certificate revocation check which is misssed in pki.verifyCertificateChain function
const certChain : pki.Certificate[] = generateCertificatesArray(pemCert);
const caStore : pki.CAStore = generateCAStore(require('ssl-root-cas/latest').create());
try {
pki.verifyCertificateChain(caStore, certChain);
} catch (e) {
throw createAskSdkError(
this.constructor.name,
e.message,
);
}

}

/**
Expand Down
5 changes: 3 additions & 2 deletions ask-sdk-express-adapter/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ask-sdk-express-adapter",
"version": "2.0.1",
"version": "2.1.0",
"description": "Express adapter package for Alexa Skills Kit SDK",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -23,7 +23,8 @@
"license": "Apache-2.0",
"dependencies": {
"body-parser": "^1.18.2",
"node-forge": "^0.8.0"
"node-forge": "^0.8.0",
"ssl-root-cas": "^1.3.1"
},
"peerDependencies": {
"ask-sdk-core": "^2.7.0"
Expand Down
36 changes: 36 additions & 0 deletions ask-sdk-express-adapter/tst/verifier/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { pki } from 'node-forge';
import * as sinon from 'sinon';
import * as url from 'url';
import { SkillRequestSignatureVerifier, TimestampVerifier, Verifier } from '../../lib/verifier';
import * as helper from '../../lib/verifier/helper';
import { createInvalidCert, DataProvider } from '../mocks/DataProvider';

describe('TimestampVerifier', () => {
Expand Down Expand Up @@ -86,6 +87,8 @@ describe('SkillRequestSignatureVerifier', () => {
const testUrl : string = 'https://s3.amazonaws.com/echo.api/echo-api-cert-4.pem';
const certUrl = url.parse(testUrl);
const validPem : string = fs.readFileSync(__dirname + '/../mocks/echo-api-cert-7.pem').toString();
const leafPem : string = validPem.slice(validPem.indexOf('-----BEGIN CERTIFICATE-----'), validPem.indexOf('-----END CERTIFICATE-----') + 25);
const lastPem : string = validPem.slice(validPem.lastIndexOf('-----BEGIN CERTIFICATE-----'), validPem.lastIndexOf('-----END CERTIFICATE-----') + 25);
const validSignature = 'jsHzkhi2zPaFXV4gnHN4foePDtv4SqmreEDqKqJc8kUX7skhOlZ03uKYeqLOHAot98tVJc9pMdi'
+ '1TRMnkQ8sr/GoReO++yGi3iAYjO8/XXL1oscx1vMUzmOLmvCO/EfF3/iEpNOb3BIJEiNhT2ZIwp7EisQi3eYLDmDaklSmP'
+ 'WWGVQRtcSq1EoHarMW9GrUaApu2cJdAjnF1aF3yFoLiHheN4DSW0qQ14N+ndba4C+YQBn4Ds2SXCFUyEC+q/H4A7SFioAE'
Expand Down Expand Up @@ -158,6 +161,7 @@ describe('SkillRequestSignatureVerifier', () => {
requestHeader[urlKey] = testUrl;
nock('https://s3.amazonaws.com').get(certUrl.path).reply(200, validPem);
sinon.stub(verifier, <any> '_validateRequestBody');
sinon.stub(helper, 'generateCAStore').returns(pki.createCaStore([lastPem]));
try {
await verifier.verify(validRequestBody, requestHeader);
} catch (err) {
Expand Down Expand Up @@ -388,6 +392,7 @@ describe('SkillRequestSignatureVerifier', () => {

describe('function _validateCertChain', () => {
const functionKey : string = '_validateCertChain';
const rootCA = require('ssl-root-cas/latest');

it('should throw error when cert expired', () => {
sinon.useFakeTimers(new Date(2022, 2, 15));
Expand Down Expand Up @@ -437,6 +442,37 @@ describe('SkillRequestSignatureVerifier', () => {
}
throw new Error('should have thrown an error!');
});

it('should throw error when cert chain is not valid', () => {
sinon.useFakeTimers(new Date(2019, 9, 1));
try {

verifier[functionKey](leafPem + lastPem);
} catch (err) {

expect(err.name).equal('AskSdk.SkillRequestSignatureVerifier Error');
expect(err.message).equal('Certificate signature is invalid.');

return;
}
throw new Error('should have thrown an error!');
});

it('should throw error when certificate chain is not trused against CA store', () => {
sinon.useFakeTimers(new Date(2019, 9, 1));
sinon.stub(helper, 'generateCAStore').returns(pki.createCaStore([leafPem]));
try {

verifier[functionKey](validPem);
} catch (err) {

expect(err.name).equal('AskSdk.SkillRequestSignatureVerifier Error');
expect(err.message).equal('Certificate is not trusted.');

return;
}
throw new Error('should have thrown an error!');
});
});

describe('function _validateRequestBody', () => {
Expand Down

0 comments on commit 32810f3

Please sign in to comment.