import { createAskSdkError , getRequestType } from 'ask-sdk-core' ; import { RequestEnvelope } from 'ask-sdk-model' ; import crypto = require ( 'crypto' ) ; import { IncomingHttpHeaders } from 'http' ; import * as client from 'https' ; import { pki } from 'node-forge' ; import * as url from 'url' ; import { gte } from 'semver' ; 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>
 */ export const REQUIRED_NODE_VERSION = "12.3.0" ; const VALID_SIGNING_CERT_CHAIN_PROTOCOL = 'https:' ; const VALID_SIGNING_CERT_CHAIN_URL_HOST_NAME = 's3.amazonaws.com' ; const VALID_SIGNING_CERT_CHAIN_URL_PATH_PREFIX = '/echo.api/' ; const SIGNATURE_CERT_CHAIN_URL_HEADER = 'SignatureCertChainUrl' ; const SIGNATURE_HEADER = 'Signature' ; const SIGNATURE_FORMAT = 'base64' ; const CERT_CHAIN_URL_PORT = 443 ; const CERT_CHAIN_DOMAIN = 'echo-api.amazon.com' ; const CHARACTER_ENCODING = 'utf8' ; const MAXIMUM_NORMAL_REQUEST_TOLERANCE_IN_MILLIS = 150000 ; const MAXIMUM_SKILL_EVENT_TOLERANCE_IN_MILLIS = 3600000 ; const ALEXA_SKILL_EVENT_LIST = new Set ( [ 'AlexaSkillEvent.SkillEnabled' , 'AlexaSkillEvent.SkillDisabled' , 'AlexaSkillEvent.SkillPermissionChanged' , 'AlexaSkillEvent.SkillPermissionAccepted' , 'AlexaSkillEvent.SkillAccountLinked' ] ) ; /**
 * Verifiers are run against incoming requests to verify authenticity and integrity of the request before processing
 * it.
 */ export interface Verifier { param { string } requestEnvelope param { IncomingHttpHeaders } headers verify ( requestEnvelope , headers ? ) : Promise < void | string > ; } /**
 * Implementation of Verifier which provides a utility method to verify the signature of a skill request.
 */ export class SkillRequestSignatureVerifier implements Verifier { protected certCache : Map < string , string > ; constructor ( ) { this . certCache = new Map < string , string > ( ) ; } param { string } requestEnvelope param { IncomingHttpHeaders } headers public async verify ( requestEnvelope , headers ) { let signatureCertChainUrl ; let signature ; for ( const key of Object . keys ( headers ) ) { const keyInLowerCase = key . toLocaleLowerCase ( ) ; if ( keyInLowerCase === SIGNATURE_CERT_CHAIN_URL_HEADER . toLowerCase ( ) ) { signatureCertChainUrl = headers [ key ] as string ; } else if ( keyInLowerCase === SIGNATURE_HEADER . toLowerCase ( ) ) { signature = headers [ key ] as string ; } } if ( ! signatureCertChainUrl ) { throw createAskSdkError ( this . constructor . name , 'Missing Certificate for the skill request' ) ; } if ( ! signature ) { throw createAskSdkError ( this . constructor . name , 'Missing Signature for the skill request' ) ; } try { const pemCert = await this . _validateUrlAndRetrieveCertChain ( signatureCertChainUrl ) ; this . _validateRequestBody ( pemCert , signature , requestEnvelope ) ; } catch ( err ) { throw createAskSdkError ( this . constructor . name , err . message ) ; } } private param { string } signatureCertChainUrl return { Promise < string > } private async _validateUrlAndRetrieveCertChain ( signatureCertChainUrl ) { this . _validateCertificateUrl ( signatureCertChainUrl ) ; const pemCert = await this . _loadCertChain ( signatureCertChainUrl ) ; return pemCert ; } private param { string } signatureCertChainUrl private _validateCertificateUrl ( signatureCertChainUrl ) { const urlObj = url . parse ( signatureCertChainUrl ) ; const protocol = urlObj . protocol ; if ( protocol . toLowerCase ( ) !== VALID_SIGNING_CERT_CHAIN_PROTOCOL ) { throw createAskSdkError ( this . constructor . name , `SignatureCertChainUrl contains an unsupported protocol ${ protocol }.` + ` Expecting ${ VALID_SIGNING_CERT_CHAIN_PROTOCOL }` ) ; } const hostname = urlObj . hostname ; if ( hostname !== VALID_SIGNING_CERT_CHAIN_URL_HOST_NAME ) { throw createAskSdkError ( this . constructor . name , `SignatureCertChainUrl has invalid host name: ${ hostname }.` + ` Expecting ${ VALID_SIGNING_CERT_CHAIN_URL_HOST_NAME }` ) ; } const path = urlObj . pathname ; if ( ! path . startsWith ( VALID_SIGNING_CERT_CHAIN_URL_PATH_PREFIX ) ) { throw createAskSdkError ( this . constructor . name , `SignatureCertChainUrl has invalid path: ${ path }.` + ` Expecting the path to start with ${ VALID_SIGNING_CERT_CHAIN_URL_PATH_PREFIX }` ) ; } const port = Number ( urlObj . port ) ; if ( port && port !== CERT_CHAIN_URL_PORT ) { throw createAskSdkError ( this . constructor . name , `SignatureCertChainUrl has invalid port: ${ port }.` + ` Expecting ${ CERT_CHAIN_URL_PORT }` ) ; } } private param { string } signatureCertChainUrl return { Promise < string > } private async _loadCertChain ( signatureCertChainUrl ) { if ( this . certCache . has ( signatureCertChainUrl ) ) { return this . certCache . get ( signatureCertChainUrl ) ; } const pemCert = await this . _getCertChainByUrl ( signatureCertChainUrl ) ; this . _validateCertChain ( pemCert ) ; this . certCache . set ( signatureCertChainUrl , pemCert ) ; return pemCert ; } private param { string } signatureCertChainUrl return { Promise < string > } private _getCertChainByUrl ( signatureCertChainUrl ) { return new Promise < string > ( ( resolve , reject ) => { const clientRequest = client . get ( signatureCertChainUrl , ( resp ) => { let data = '' ; let statusCode ; if ( ! resp || resp . statusCode !== 200 ) { statusCode = resp ? resp . statusCode : 0 ; reject ( new Error ( `Unable to load x509 certificate from URL: ${ signatureCertChainUrl }. Response status code: ${ statusCode }` ) ) ; } resp . setEncoding ( CHARACTER_ENCODING ) ; resp . on ( 'data' , ( chunk ) => { data += chunk ; } ) ; resp . on ( 'end' , ( ) => { resolve ( data ) ; } ) ; } ) ; clientRequest . on ( 'error' , ( err ) => { reject ( new Error ( err . message ) ) ; } ) ; clientRequest . end ( ) ; } ) ; } private param { string } pemCert private _validateCertChain ( pemCert ) { const cert = pki . certificateFromPem ( pemCert ) ; const now = new Date ( ) . getTime ( ) ; const notAfter = new Date ( cert . validity . notAfter ) . getTime ( ) ; const notBefore = new Date ( cert . validity . notBefore ) . getTime ( ) ; if ( ! ( now <= notAfter && now >= notBefore ) ) { throw createAskSdkError ( this . constructor . name , 'Signing Certificate expired or not started' ) ; } const subjectAltNameExtension = cert . getExtension ( 'subjectAltName' ) ; const keyName = 'altNames' ; const domainExist = ( domain ) => domain . value === CERT_CHAIN_DOMAIN ; if ( ! subjectAltNameExtension [ keyName ] . some ( domainExist ) ) { throw createAskSdkError ( this . constructor . name , `${ CERT_CHAIN_DOMAIN } domain missing in Signature Certificate Chain.` ) ; } if ( ! gte ( process . version , REQUIRED_NODE_VERSION ) ) { throw createAskSdkError ( this . constructor . name , `ask-sdk-express-adapter package require node version ${ REQUIRED_NODE_VERSION } or later, your current node version is ${ process . version }. Please update your node version.` ) ; } const caStore = generateCAStore ( require ( 'tls' ) . rootCertificates ) ; const certChain = generateCertificatesArray ( pemCert ) ; try { pki . verifyCertificateChain ( caStore , certChain ) ; } catch ( e ) { throw createAskSdkError ( this . constructor . name , e . message ) ; } } param pemCert param signature param requestEnvelope private _validateRequestBody ( pemCert , signature , requestEnvelope ) { const verifier = crypto . createVerify ( 'RSA-SHA1' ) ; verifier . update ( requestEnvelope , CHARACTER_ENCODING ) ; if ( ! verifier . verify ( pemCert , signature , SIGNATURE_FORMAT ) ) { throw new Error ( 'request body and signature does not match' ) ; } } } /**
 * Implementation of Verifier which provides a utility method to handle
 * the request timestamp verification of the input request.
 */ export class TimestampVerifier implements Verifier { protected toleranceInMillis : number ; constructor ( tolerance = MAXIMUM_NORMAL_REQUEST_TOLERANCE_IN_MILLIS ) { if ( tolerance > MAXIMUM_NORMAL_REQUEST_TOLERANCE_IN_MILLIS ) { console . warn ( `ask-sdk-express-adapter TimestampVerifier: Provided tolerance value ${ tolerance } exceeds the maximum allowed value ${ MAXIMUM_NORMAL_REQUEST_TOLERANCE_IN_MILLIS }, Maximum value will be used instead.` ) ; tolerance = MAXIMUM_NORMAL_REQUEST_TOLERANCE_IN_MILLIS ; } if ( tolerance < 0 ) { throw createAskSdkError ( this . constructor . name , `Negative tolerance values not supported` ) ; } this . toleranceInMillis = tolerance ; } param { string } requestEnvelope return { Promise < void > } public async verify ( requestEnvelope ) { const requestEnvelopeJson = JSON . parse ( requestEnvelope ) ; if ( ! ( requestEnvelopeJson . request && requestEnvelopeJson . request . timestamp ) ) { throw createAskSdkError ( this . constructor . name , 'Timestamp is not present in request' ) ; } const requestTimeStamp = new Date ( requestEnvelopeJson . request . timestamp ) ; const localNow = new Date ( ) ; if ( requestTimeStamp . getTime ( ) + this . toleranceInMillis < localNow . getTime ( ) ) { if ( ALEXA_SKILL_EVENT_LIST . has ( getRequestType ( requestEnvelopeJson ) ) && ( requestTimeStamp . getTime ( ) + MAXIMUM_SKILL_EVENT_TOLERANCE_IN_MILLIS >= localNow . getTime ( ) ) ) { return ; } throw createAskSdkError ( this . constructor . name , 'Timestamp verification failed' ) ; } } }