From 30254400e62b8de7263e6ec297879bceb5bcef4e Mon Sep 17 00:00:00 2001 From: "Hung-Han (Henry) Chen" Date: Fri, 29 Oct 2021 13:51:03 +0300 Subject: [PATCH] Add getMacRootCA/getWinRootCA and tests Signed-off-by: Hung-Han (Henry) Chen --- src/common/__tests__/system-ca.test.ts | 87 ++++++++++++++++++++++++++ src/common/system-ca.ts | 78 +++++++++++++++++++---- 2 files changed, 152 insertions(+), 13 deletions(-) create mode 100644 src/common/__tests__/system-ca.test.ts diff --git a/src/common/__tests__/system-ca.test.ts b/src/common/__tests__/system-ca.test.ts new file mode 100644 index 0000000000..8d0c3bff33 --- /dev/null +++ b/src/common/__tests__/system-ca.test.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +import https from "https"; +import os from "os"; +import { getMacRootCA, getWinRootCA, injectCAs } from "../system-ca"; +import { dependencies, devDependencies } from "../../../package.json"; + +describe("inject CA for Mac", () => { + // for reset https.globalAgent.options.ca after testing + let _ca: string | Buffer | (string | Buffer)[]; + + beforeEach(() => { + _ca = https.globalAgent.options.ca; + }); + + afterEach(() => { + https.globalAgent.options.ca = _ca; + }); + + const deps = { ...dependencies, ...devDependencies }; + + // skip the test if mac-ca is not installed + (deps["mac-ca"] && os.platform().includes("darwin") ? it: it.skip)("should inject the same ca as mac-ca", async () => { + const osxCAs = await getMacRootCA(); + + injectCAs(osxCAs); + const injected = https.globalAgent.options.ca; + + await import("mac-ca"); + const injectedByMacCA = https.globalAgent.options.ca; + + // @ts-ignore + expect(new Set(injected)).toEqual(new Set(injectedByMacCA)); + }); +}); + +describe("inject CA for Windows", () => { + // for reset https.globalAgent.options.ca after testing + let _ca: string | Buffer | (string | Buffer)[]; + + beforeEach(() => { + _ca = https.globalAgent.options.ca; + }); + + afterEach(() => { + https.globalAgent.options.ca = _ca; + }); + + const deps = { ...dependencies, ...devDependencies }; + + // skip the test if win-ca is not installed + (deps["win-ca"] && os.platform().includes("win32") ? it: it.skip)("should inject the same ca as winca.inject('+')", async () => { + const winCAs = await getWinRootCA(); + + injectCAs(winCAs); + const injected = https.globalAgent.options.ca; + + const winca = await import("win-ca"); + + winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats + const injectedByWinCA = https.globalAgent.options.ca; + + // @ts-ignore + expect(new Set(injected)).toEqual(new Set(injectedByWinCA)); + }); +}); + + + diff --git a/src/common/system-ca.ts b/src/common/system-ca.ts index 7ed0120569..3cc976867f 100644 --- a/src/common/system-ca.ts +++ b/src/common/system-ca.ts @@ -20,22 +20,74 @@ */ import { isMac, isWindows } from "./vars"; -import winca from "win-ca"; -import macca from "mac-ca"; -import logger from "../main/logger"; +// @ts-expect-error winca/api module doesn't have a type definition +import winca from "win-ca/api"; +import https from "https"; +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); + +/** + * Get root CA certificate from MacOSX system keychain + */ +export const getMacRootCA = async () => { + // inspired mac-ca https://github.com/jfromaniello/mac-ca + const args = "find-certificate -a -p"; + const splitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g; + const systemRootCertsPath = "/System/Library/Keychains/SystemRootCertificates.keychain"; + const bin = "/usr/bin/security"; + const trusted = (await execAsync(`${bin} ${args}`)).stdout.toString().split(splitPattern); + const rootCA = (await execAsync(`${bin} ${args} ${systemRootCertsPath}`)).stdout.toString().split(splitPattern); + + return [...new Set([...trusted, ...rootCA])]; +}; + +/** + * Get root CA certificate from Windows system certificate store + */ +export const getWinRootCA = (): Promise => { + return new Promise((resolve) => { + const CAs: string[] = []; + + winca({ + format: winca.der2.pem, + inject: false, + ondata: (ca: string) => { + CAs.push(ca); + }, + onend: () => { + resolve(CAs); + } + }); + }); +}; + +/** + * Add (or merge) CAs to https.globalAgent.options.ca + */ +export const injectCAs = async (CAs: Array) => { + for (const cert of CAs) { + if (Array.isArray(https.globalAgent.options.ca)) { + !https.globalAgent.options.ca.includes(cert) && https.globalAgent.options.ca.push(cert); + } else { + https.globalAgent.options.ca = [cert]; + } + } +}; if (isMac) { - for (const crt of macca.all()) { - const attributes = crt.issuer?.attributes?.map((a: any) => `${a.name}=${a.value}`); - - logger.debug(`Using host CA: ${attributes.join(",")}`); - } + getMacRootCA().then((osxRootCAs) => { + injectCAs(osxRootCAs); + }).catch((error) => { + console.error(`[MAC-CA]: Error injecting root CAs from MacOSX. ${error?.message}`); + }); } if (isWindows) { - try { - winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats - } catch (error) { - logger.error(`[CA]: failed to force load: ${error}`); - } + getWinRootCA().then((winRootCAs) => { + injectCAs(winRootCAs); + }).catch((error) => { + console.error(`[WIN-CA]: Error injecting root CAs from Windows. ${error?.message}`); + }); }