{"version":3,"sources":["src/common.browser/CertChecks.ts"],"names":[],"mappings":";AAGA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAuB7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAUxC,qBAAa,cAAc;IAGvB,OAAc,cAAc,EAAE,MAAM,CAAK;IAGzC,OAAc,wBAAwB,EAAE,OAAO,CAAS;IAGxD,OAAO,CAAC,MAAM,CAAC,YAAY,CAAiC;IAG5D,OAAO,CAAC,MAAM,CAAC,aAAa,CAAQ;IAEpC,OAAO,CAAC,aAAa,CAAY;gBAErB,SAAS,CAAC,EAAE,SAAS;WAYnB,oBAAoB,IAAI,IAAI;IAKnC,QAAQ,CAAC,eAAe,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK;IAatD,OAAO,CAAC,MAAM,CAAC,aAAa;mBAoBP,SAAS;IAyD9B,OAAO,CAAC,MAAM,CAAC,SAAS;mBAkBH,oBAAoB;mBAkEpB,kBAAkB;mBAkClB,WAAW;IAUhC,OAAO,CAAC,MAAM,CAAC,eAAe;IAK9B,OAAO,CAAC,MAAM,CAAC,qBAAqB;IAKpC,OAAO,CAAC,MAAM,CAAC,mBAAmB;IAMlC,OAAO,CAAC,MAAM,CAAC,eAAe;IAkC9B,OAAO,CAAC,MAAM,CAAC,OAAO,CAErB;IAED,OAAO,CAAC,gBAAgB;CAuC3B","file":"CertChecks.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\r\n// Licensed under the MIT license.\r\n\r\nimport * as http from \"http\";\r\nimport * as tls from \"tls\";\r\nimport * as parse from \"url-parse\";\r\nimport * as ocsp from \"../../external/ocsp/ocsp\";\r\nimport {\r\n    Events,\r\n    OCSPCacheEntryExpiredEvent,\r\n    OCSPCacheEntryNeedsRefreshEvent,\r\n    OCSPCacheFetchErrorEvent,\r\n    OCSPCacheHitEvent,\r\n    OCSPCacheMissEvent,\r\n    OCSPCacheUpdatehCompleteEvent,\r\n    OCSPCacheUpdateNeededEvent,\r\n    OCSPDiskCacheHitEvent,\r\n    OCSPDiskCacheStoreEvent,\r\n    OCSPEvent,\r\n    OCSPMemoryCacheHitEvent,\r\n    OCSPMemoryCacheStoreEvent,\r\n    OCSPResponseRetrievedEvent,\r\n    OCSPStapleReceivedEvent,\r\n    OCSPVerificationFailedEvent,\r\n} from \"../common/Exports\";\r\nimport { IStringDictionary } from \"../common/IDictionary\";\r\nimport { ProxyInfo } from \"./ProxyInfo\";\r\n\r\nimport Agent from \"agent-base\";\r\n\r\n// @ts-ignore\r\nimport Cache from \"async-disk-cache\";\r\nimport HttpsProxyAgent from \"https-proxy-agent\";\r\nimport * as net from \"net\";\r\nimport { OCSPCacheUpdateErrorEvent } from \"../common/OCSPEvents\";\r\n\r\nexport class CertCheckAgent {\r\n\r\n    // Test hook to enable forcing expiration / refresh to happen.\r\n    public static testTimeOffset: number = 0;\r\n\r\n    // Test hook to disable stapling for cache testing.\r\n    public static forceDisableOCSPStapling: boolean = false;\r\n\r\n    // An in memory cache for recived responses.\r\n    private static privMemCache: IStringDictionary<Buffer> = {};\r\n\r\n    // The on disk cache.\r\n    private static privDiskCache: Cache;\r\n\r\n    private privProxyInfo: ProxyInfo;\r\n\r\n    constructor(proxyInfo?: ProxyInfo) {\r\n        if (!!proxyInfo) {\r\n            this.privProxyInfo = proxyInfo;\r\n        }\r\n\r\n        // Initialize this here to allow tests to set the env variable before the cache is constructed.\r\n        if (!CertCheckAgent.privDiskCache) {\r\n            CertCheckAgent.privDiskCache = new Cache(\"microsoft-cognitiveservices-speech-sdk-cache\", { supportBuffer: true, location: (typeof process !== \"undefined\" && !!process.env.SPEECH_OCSP_CACHE_ROOT) ? process.env.SPEECH_OCSP_CACHE_ROOT : undefined });\r\n        }\r\n    }\r\n\r\n    // Test hook to force the disk cache to be recreated.\r\n    public static forceReinitDiskCache(): void {\r\n        CertCheckAgent.privDiskCache = undefined;\r\n        CertCheckAgent.privMemCache = {};\r\n    }\r\n\r\n    public GetAgent(disableStapling?: boolean): http.Agent {\r\n        const agent: any = new Agent.Agent(this.CreateConnection);\r\n\r\n        if (this.privProxyInfo !== undefined &&\r\n            this.privProxyInfo.HostName !== undefined &&\r\n            this.privProxyInfo.Port > 0) {\r\n            const proxyName: string = \"privProxyInfo\";\r\n            agent[proxyName] = this.privProxyInfo;\r\n        }\r\n\r\n        return agent;\r\n    }\r\n\r\n    private static GetProxyAgent(proxyInfo: ProxyInfo): HttpsProxyAgent {\r\n        const httpProxyOptions: HttpsProxyAgent.HttpsProxyAgentOptions = {\r\n            host: proxyInfo.HostName,\r\n            port: proxyInfo.Port,\r\n        };\r\n\r\n        if (!!proxyInfo.UserName) {\r\n            httpProxyOptions.headers = {\r\n                \"Proxy-Authentication\": \"Basic \" + new Buffer(proxyInfo.UserName + \":\" + (proxyInfo.Password === undefined) ? \"\" : proxyInfo.Password).toString(\"base64\"),\r\n            };\r\n        } else {\r\n            httpProxyOptions.headers = {};\r\n        }\r\n\r\n        httpProxyOptions.headers.requestOCSP = \"true\";\r\n\r\n        const httpProxyAgent: HttpsProxyAgent = new HttpsProxyAgent(httpProxyOptions);\r\n        return httpProxyAgent;\r\n    }\r\n\r\n    private static async OCSPCheck(socketPromise: Promise<net.Socket>, proxyInfo: ProxyInfo): Promise<net.Socket> {\r\n        let ocspRequest: ocsp.Request;\r\n        let stapling: Buffer;\r\n        let resolved: boolean = false;\r\n\r\n        const socket: net.Socket = await socketPromise;\r\n        socket.cork();\r\n\r\n        const tlsSocket: tls.TLSSocket = socket as tls.TLSSocket;\r\n\r\n        return new Promise<net.Socket>((resolve: (value: net.Socket) => void, reject: (error: string | Error) => void) => {\r\n            socket.on(\"OCSPResponse\", (data: Buffer): void => {\r\n                if (!!data) {\r\n                    this.onEvent(new OCSPStapleReceivedEvent());\r\n                    stapling = data;\r\n                }\r\n            });\r\n\r\n            socket.on(\"error\", (error: Error) => {\r\n                if (!resolved) {\r\n                    resolved = true;\r\n                    socket.destroy();\r\n                    reject(error);\r\n                }\r\n            });\r\n\r\n            tlsSocket.on(\"secure\", async () => {\r\n                const peer: tls.DetailedPeerCertificate = tlsSocket.getPeerCertificate(true);\r\n                try {\r\n                    const issuer: tls.DetailedPeerCertificate = await this.GetIssuer(peer);\r\n\r\n                    // We always need a request to verify the response.\r\n                    ocspRequest = ocsp.request.generate(peer.raw, issuer.raw);\r\n\r\n                    // Do we have a result for this certificate in our memory cache?\r\n                    const sig: string = ocspRequest.id.toString(\"hex\");\r\n\r\n                    // Stapled response trumps cached response.\r\n                    if (!stapling) {\r\n                        const cacheEntry: Buffer = await CertCheckAgent.GetResponseFromCache(sig, ocspRequest, proxyInfo);\r\n                        stapling = cacheEntry;\r\n                    }\r\n\r\n                    await this.VerifyOCSPResponse(stapling, ocspRequest, proxyInfo);\r\n\r\n                    socket.uncork();\r\n                    resolved = true;\r\n                    resolve(socket);\r\n                } catch (e) {\r\n                    socket.destroy();\r\n                    resolved = true;\r\n                    reject(e);\r\n                }\r\n            });\r\n        });\r\n    }\r\n\r\n    private static GetIssuer(peer: tls.DetailedPeerCertificate): Promise<tls.DetailedPeerCertificate> {\r\n        if (peer.issuerCertificate) {\r\n            return Promise.resolve(peer.issuerCertificate);\r\n        }\r\n\r\n        return new Promise<tls.DetailedPeerCertificate>((resolve: (value: tls.DetailedPeerCertificate) => void, reject: (reason: string) => void) => {\r\n            const ocspAgent: ocsp.Agent = new ocsp.Agent({});\r\n            ocspAgent.fetchIssuer(peer, null, (error: string, value: tls.DetailedPeerCertificate): void => {\r\n                if (!!error) {\r\n                    reject(error);\r\n                    return;\r\n                }\r\n\r\n                resolve(value);\r\n            });\r\n        });\r\n    }\r\n\r\n    private static async GetResponseFromCache(signature: string, ocspRequest: ocsp.Request, proxyInfo: ProxyInfo): Promise<Buffer> {\r\n        let cachedResponse: Buffer = CertCheckAgent.privMemCache[signature];\r\n\r\n        if (!!cachedResponse) {\r\n            this.onEvent(new OCSPMemoryCacheHitEvent(signature));\r\n        }\r\n\r\n        // Do we have a result for this certificate on disk in %TMP%?\r\n        if (!cachedResponse) {\r\n            try {\r\n                const diskCacheResponse: any = await CertCheckAgent.privDiskCache.get(signature);\r\n                if (!!diskCacheResponse.isCached) {\r\n                    CertCheckAgent.onEvent(new OCSPDiskCacheHitEvent(signature));\r\n                    CertCheckAgent.StoreMemoryCacheEntry(signature, diskCacheResponse.value);\r\n                    cachedResponse = diskCacheResponse.value;\r\n                }\r\n            } catch (error) {\r\n                cachedResponse = null;\r\n            }\r\n        }\r\n\r\n        if (!cachedResponse) {\r\n            return cachedResponse;\r\n        }\r\n\r\n        try {\r\n            const cachedOcspResponse: ocsp.Response = ocsp.utils.parseResponse(cachedResponse);\r\n            const tbsData = cachedOcspResponse.value.tbsResponseData;\r\n            if (tbsData.responses.length < 1) {\r\n                this.onEvent(new OCSPCacheFetchErrorEvent(signature, \"Not enough data in cached response\"));\r\n                return;\r\n            }\r\n\r\n            const cachedStartTime: number = tbsData.responses[0].thisUpdate;\r\n            const cachedNextTime: number = tbsData.responses[0].nextUpdate;\r\n\r\n            if (cachedNextTime < (Date.now() + this.testTimeOffset - 60000)) {\r\n                // Cached entry has expired.\r\n                this.onEvent(new OCSPCacheEntryExpiredEvent(signature, cachedNextTime));\r\n                cachedResponse = null;\r\n            } else {\r\n                // If we're within one day of the next update, or 50% of the way through the validity period,\r\n                // background an update to the cache.\r\n\r\n                const minUpdate: number = Math.min(24 * 60 * 60 * 1000, (cachedNextTime - cachedStartTime) / 2);\r\n\r\n                if ((cachedNextTime - (Date.now() + this.testTimeOffset)) < minUpdate) {\r\n                    this.onEvent(new OCSPCacheEntryNeedsRefreshEvent(signature, cachedStartTime, cachedNextTime));\r\n                    this.UpdateCache(ocspRequest, proxyInfo).catch((error: string) => {\r\n                        // Well, not much we can do here.\r\n                        this.onEvent(new OCSPCacheUpdateErrorEvent(signature, error.toString()));\r\n                    });\r\n                } else {\r\n                    this.onEvent(new OCSPCacheHitEvent(signature, cachedStartTime, cachedNextTime));\r\n                }\r\n            }\r\n        } catch (error) {\r\n            this.onEvent(new OCSPCacheFetchErrorEvent(signature, error));\r\n            cachedResponse = null;\r\n        }\r\n        if (!cachedResponse) {\r\n            this.onEvent(new OCSPCacheMissEvent(signature));\r\n        }\r\n        return cachedResponse;\r\n    }\r\n\r\n    private static async VerifyOCSPResponse(cacheValue: Buffer, ocspRequest: ocsp.Request, proxyInfo: ProxyInfo): Promise<void> {\r\n        let ocspResponse: Buffer = cacheValue;\r\n        const sig: string = ocspRequest.certID.toString(\"hex\");\r\n\r\n        // Do we have a valid response?\r\n        if (!ocspResponse) {\r\n            ocspResponse = await CertCheckAgent.GetOCSPResponse(ocspRequest, proxyInfo);\r\n        }\r\n\r\n        return new Promise<void>((resolve: () => void, reject: (error: string | Error) => void) => {\r\n            ocsp.verify({ request: ocspRequest, response: ocspResponse }, (error: string, result: any): void => {\r\n                if (!!error) {\r\n                    CertCheckAgent.onEvent(new OCSPVerificationFailedEvent(ocspRequest.id.toString(\"hex\"), error));\r\n\r\n                    // Bad Cached Value? One more try without the cache.\r\n                    if (!!cacheValue) {\r\n                        this.VerifyOCSPResponse(null, ocspRequest, proxyInfo).then(() => {\r\n                            resolve();\r\n                        }, (error: Error) => {\r\n                            reject(error);\r\n                        });\r\n                    } else {\r\n                        reject(error);\r\n                    }\r\n                } else {\r\n                    if (!cacheValue) {\r\n                        CertCheckAgent.StoreCacheEntry(ocspRequest.id.toString(\"hex\"), ocspResponse);\r\n                    }\r\n                    resolve();\r\n                }\r\n            });\r\n        });\r\n    }\r\n\r\n    private static async UpdateCache(req: ocsp.Request, proxyInfo: ProxyInfo): Promise<void> {\r\n        const signature: string = req.id.toString(\"hex\");\r\n        this.onEvent(new OCSPCacheUpdateNeededEvent(signature));\r\n\r\n        const rawResponse: Buffer = await this.GetOCSPResponse(req, proxyInfo);\r\n        this.StoreCacheEntry(signature, rawResponse);\r\n        this.onEvent(new OCSPCacheUpdatehCompleteEvent(req.id.toString(\"hex\")));\r\n\r\n    }\r\n\r\n    private static StoreCacheEntry(sig: string, rawResponse: Buffer): void {\r\n        this.StoreMemoryCacheEntry(sig, rawResponse);\r\n        this.StoreDiskCacheEntry(sig, rawResponse);\r\n    }\r\n\r\n    private static StoreMemoryCacheEntry(sig: string, rawResponse: Buffer): void {\r\n        this.privMemCache[sig] = rawResponse;\r\n        this.onEvent(new OCSPMemoryCacheStoreEvent(sig));\r\n    }\r\n\r\n    private static StoreDiskCacheEntry(sig: string, rawResponse: Buffer): void {\r\n        this.privDiskCache.set(sig, rawResponse).then(() => {\r\n            this.onEvent(new OCSPDiskCacheStoreEvent(sig));\r\n        });\r\n    }\r\n\r\n    private static GetOCSPResponse(req: ocsp.Request, proxyInfo: ProxyInfo): Promise<Buffer> {\r\n\r\n        const ocspMethod: string = \"1.3.6.1.5.5.7.48.1\";\r\n        let options: http.RequestOptions = {};\r\n\r\n        if (!!proxyInfo) {\r\n            const agent: HttpsProxyAgent = CertCheckAgent.GetProxyAgent(proxyInfo);\r\n            options.agent = agent;\r\n        }\r\n\r\n        return new Promise<Buffer>((resolve: (value: Buffer) => void, reject: (error: string | Error) => void) => {\r\n            ocsp.utils.getAuthorityInfo(req.cert, ocspMethod, (error: string, uri: string): void => {\r\n                if (error) {\r\n                    reject(error);\r\n                    return;\r\n                }\r\n\r\n                const parsedUri: {[k: string]: any} = parse.default(uri);\r\n                parsedUri.path = parsedUri.pathname;\r\n                options = { ...options, ...parsedUri };\r\n\r\n                ocsp.utils.getResponse(options, req.data, (error: string, raw: Buffer): void => {\r\n                    if (error) {\r\n                        reject(error);\r\n                        return;\r\n                    }\r\n\r\n                    this.onEvent(new OCSPResponseRetrievedEvent(req.certID.toString(\"hex\")));\r\n                    resolve(raw);\r\n                });\r\n            });\r\n        });\r\n    }\r\n\r\n    private static onEvent = (event: OCSPEvent): void => {\r\n        Events.instance.onEvent(event);\r\n    }\r\n\r\n    private CreateConnection(request: Agent.ClientRequest, options: Agent.RequestOptions): Promise<net.Socket> {\r\n        const enableOCSP: boolean = (typeof process !== \"undefined\" && process.env.NODE_TLS_REJECT_UNAUTHORIZED !== \"0\" && process.env.SPEECH_CONDUCT_OCSP_CHECK !== \"0\") && options.secureEndpoint;\r\n        let socketPromise: Promise<net.Socket>;\r\n\r\n        options = {\r\n            ...options,\r\n            ...{\r\n                requestOCSP: !CertCheckAgent.forceDisableOCSPStapling,\r\n                servername: options.host\r\n            }\r\n        };\r\n\r\n        if (!!this.privProxyInfo) {\r\n            const httpProxyAgent: HttpsProxyAgent = CertCheckAgent.GetProxyAgent(this.privProxyInfo);\r\n            const baseAgent: Agent.Agent = httpProxyAgent as unknown as Agent.Agent;\r\n\r\n            socketPromise = new Promise<net.Socket>((resolve: (value: net.Socket) => void, reject: (error: string | Error) => void) => {\r\n                baseAgent.callback(request, options, (error: Error, socket: net.Socket) => {\r\n                    if (!!error) {\r\n                        reject(error);\r\n                    } else {\r\n                        resolve(socket);\r\n                    }\r\n                });\r\n            });\r\n        } else {\r\n            if (!!options.secureEndpoint) {\r\n                socketPromise = Promise.resolve(tls.connect(options));\r\n            } else {\r\n                socketPromise = Promise.resolve(net.connect(options));\r\n            }\r\n        }\r\n\r\n        if (!!enableOCSP) {\r\n            return CertCheckAgent.OCSPCheck(socketPromise, this.privProxyInfo);\r\n        } else {\r\n            return socketPromise;\r\n        }\r\n    }\r\n}\r\n"]}