Combine NODE_EXTRA_CA_CERTS with Safe Chain's certificate bundle

This commit is contained in:
Reinier Criel 2025-12-05 14:23:57 -08:00 committed by Sander Declerck
parent 09809d29bc
commit 02c30a2544
No known key found for this signature in database
2 changed files with 71 additions and 2 deletions

View file

@ -6,6 +6,7 @@ import certifi from "certifi";
import tls from "node:tls"; import tls from "node:tls";
import { X509Certificate } from "node:crypto"; import { X509Certificate } from "node:crypto";
import { getCaCertPath } from "./certUtils.js"; import { getCaCertPath } from "./certUtils.js";
import { ui } from "../environment/userInteraction.js";
/** /**
* Check if a PEM string contains only parsable cert blocks. * Check if a PEM string contains only parsable cert blocks.
@ -93,3 +94,68 @@ export function getCombinedCaBundlePath() {
cachedPath = target; cachedPath = target;
return cachedPath; return cachedPath;
} }
/**
* Read user certificate file.
* @param {string} certPath - Path to certificate file
* @returns {string | null} Certificate PEM content or null if invalid/unreadable
*/
function readUserCertificateFile(certPath) {
try {
// Validate path is a string and not attempting path traversal
if (typeof certPath !== "string" || certPath.includes("..") || certPath.startsWith("/")) {
return null;
}
if (!fs.existsSync(certPath)) {
return null;
}
const certPathAbsolute = path.resolve(certPath);
// Verify it's an absolute path (cross-platform)
if (!path.isAbsolute(certPathAbsolute)) {
return null;
}
const content = fs.readFileSync(certPathAbsolute, "utf8");
return content && isParsable(content) ? content : null;
} catch {
return null;
}
}
/**
* Combine user's existing NODE_EXTRA_CA_CERTS with Safe Chain's CA certificate.
* If user has NODE_EXTRA_CA_CERTS set, it's merged with Safe Chain CA.
*
* @param {string | undefined} userCertPath - User's existing NODE_EXTRA_CA_CERTS path (if any)
* @returns {string} Path to the final CA bundle
*/
export function getCombinedCaBundlePathWithUserCerts(userCertPath) {
const parts = [];
// 1) Add Safe Chain CA (for MITM'd registries)
const safeChainPath = getCaCertPath();
try {
const safeChainPem = fs.readFileSync(safeChainPath, "utf8");
if (isParsable(safeChainPem)) parts.push(safeChainPem.trim());
} catch {
// Ignore if Safe Chain CA is not available
}
// 2) Add user's certificates if provided
if (userCertPath) {
const userPem = readUserCertificateFile(userCertPath);
if (userPem) {
parts.push(userPem.trim());
ui.writeWarning(`Safe-chain: Merging user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
} else {
ui.writeWarning(`Safe-chain: Could not read or parse user's NODE_EXTRA_CA_CERTS from ${userCertPath}`);
}
}
const finalCombined = parts.filter(Boolean).join("\n");
const target = path.join(os.tmpdir(), `safe-chain-ca-bundle-${Date.now()}.pem`);
fs.writeFileSync(target, finalCombined, { encoding: "utf8" });
return target;
}

View file

@ -2,7 +2,7 @@ import * as http from "http";
import { tunnelRequest } from "./tunnelRequestHandler.js"; import { tunnelRequest } from "./tunnelRequestHandler.js";
import { mitmConnect } from "./mitmRequestHandler.js"; import { mitmConnect } from "./mitmRequestHandler.js";
import { handleHttpProxyRequest } from "./plainHttpProxy.js"; import { handleHttpProxyRequest } from "./plainHttpProxy.js";
import { getCaCertPath } from "./certUtils.js"; import { getCombinedCaBundlePathWithUserCerts } from "./certBundle.js";
import { ui } from "../environment/userInteraction.js"; import { ui } from "../environment/userInteraction.js";
import chalk from "chalk"; import chalk from "chalk";
import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js"; import { createInterceptorForUrl } from "./interceptors/createInterceptorForEcoSystem.js";
@ -37,10 +37,13 @@ function getSafeChainProxyEnvironmentVariables() {
} }
const proxyUrl = `http://localhost:${state.port}`; const proxyUrl = `http://localhost:${state.port}`;
const userNodeExtraCaCerts = process.env.NODE_EXTRA_CA_CERTS;
const caCertPath = getCombinedCaBundlePathWithUserCerts(userNodeExtraCaCerts);
return { return {
HTTPS_PROXY: proxyUrl, HTTPS_PROXY: proxyUrl,
GLOBAL_AGENT_HTTP_PROXY: proxyUrl, GLOBAL_AGENT_HTTP_PROXY: proxyUrl,
NODE_EXTRA_CA_CERTS: getCaCertPath(), NODE_EXTRA_CA_CERTS: caCertPath,
}; };
} }