use system-ca library for ca certs

This commit is contained in:
lohit-bruno
2025-09-12 00:33:22 +05:30
parent 5b716cbe60
commit 27cbb194bf
12 changed files with 424 additions and 353 deletions

View File

@@ -93,7 +93,7 @@
"@rsbuild/plugin-react": "^1.0.7",
"@rsbuild/plugin-sass": "^1.1.0",
"@rsbuild/plugin-styled-components": "1.1.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"autoprefixer": "10.4.20",

View File

@@ -69,6 +69,7 @@
"lodash": "^4.17.21",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"system-ca": "^2.0.1",
"xmlbuilder": "^15.1.1",
"yargs": "^17.6.2"
}

View File

@@ -25,7 +25,8 @@ const { createFormData } = require('../utils/form-data');
const { getOAuth2Token } = require('./oauth2');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const { NtlmClient } = require('axios-ntlm');
const { addDigestInterceptor, getCACertificates } = require('@usebruno/requests');
const { addDigestInterceptor } = require('@usebruno/requests');
const { getCACertificates } = require('../utils/ca-cert');
const { encodeUrl } = require('@usebruno/common').utils;
const onConsoleLog = (type, args) => {
@@ -157,8 +158,8 @@ const runSingleRequest = async function (
} else {
const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE];
const caCertFilePath = caCertArray.find((el) => el);
let caCertificatesWithCertType = getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] });
let caCertificates = caCertificatesWithCertType.map(certData => certData.certificate);
let caCertificatesData = await getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] });
let caCertificates = caCertificatesData.caCertificates;
if (caCertificates?.length > 0) {
httpsAgentRequestFields['ca'] = caCertificates;
}

View File

@@ -0,0 +1,119 @@
const { systemCertsAsync } = require('system-ca');
const { rootCertificates } = require('node:tls');
const fs = require('node:fs');
let systemCertsCache;
async function getSystemCerts(systemCAOpts = {}) {
if (systemCertsCache) return systemCertsCache;
try {
systemCertsCache = await systemCertsAsync(systemCAOpts);
return systemCertsCache;
} catch (error) {
console.error(error);
return [];
}
}
function certToString(cert) {
return typeof cert === 'string'
? cert
: Buffer.from(cert.buffer, cert.byteOffset, cert.byteLength).toString('utf8');
}
function mergeCA(...args) {
const ca = new Set();
for (const item of args) {
if (!item) continue;
const caList = Array.isArray(item) ? item : [item];
for (const cert of caList) {
ca.add(certToString(cert));
}
}
return [...ca].join('\n');
}
function getNodeExtraCACerts() {
const extraCACertPath = process.env.NODE_EXTRA_CA_CERTS;
if (!extraCACertPath) return [];
try {
if (fs.existsSync(extraCACertPath)) {
const extraCACert = fs.readFileSync(extraCACertPath, 'utf8');
if (extraCACert && extraCACert.trim()) {
return [extraCACert];
}
}
} catch (err) {
console.error(`Failed to read NODE_EXTRA_CA_CERTS from ${extraCACertPath}:`, err.message);
}
return [];
}
const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true }) => {
try {
let caCertificates = '';
let caCertificatesCount = {
system: 0,
root: 0,
custom: 0,
extra: 0
}
let systemCerts = [];
let rootCerts = [];
let customCerts = [];
let nodeExtraCerts = [];
if (shouldKeepDefaultCerts) {
// get system certs
systemCerts = await getSystemCerts();
caCertificatesCount.system = systemCerts.length;
// get root certs
rootCerts = [...rootCertificates];
caCertificatesCount.root = rootCerts.length;
}
// handle user-provided custom CA certificate file with optional default certificates
if (caCertFilePath) {
// validate custom CA certificate file
if (fs.existsSync(caCertFilePath)) {
try {
const customCert = fs.readFileSync(caCertFilePath, 'utf8');
if (customCert && customCert.trim()) {
customCerts.push(customCert);
caCertificatesCount.custom = customCerts.length;
}
} catch (err) {
console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err).message);
throw new Error(`Unable to load custom CA certificate: ${(err).message}`);
}
}
}
// get NODE_EXTRA_CA_CERTS
nodeExtraCerts = getNodeExtraCACerts();
caCertificatesCount.extra = nodeExtraCerts.length;
// merge certs
const mergedCerts = mergeCA(systemCerts, rootCerts, customCerts, nodeExtraCerts);
caCertificates = mergedCerts;
return {
caCertificates,
caCertificatesCount
}
} catch (err) {
console.error('Error configuring CA certificates:', (err).message);
throw err; // Re-throw certificate loading errors as they're critical
}
}
module.exports = {
getCACertificates
};

View File

@@ -67,6 +67,7 @@
"nanoid": "3.3.8",
"qs": "^6.11.0",
"socks-proxy-agent": "^8.0.2",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0",
"uuid": "^9.0.0",
"yup": "^0.32.11"

View File

@@ -1,10 +1,10 @@
const fs = require('node:fs');
const path = require('path');
const { get } = require('lodash');
const { getCACertificates } = require('@usebruno/requests');
const { preferencesUtil } = require('../../store/preferences');
const { getBrunoConfig } = require('../../store/bruno-config');
const { interpolateString } = require('./interpolate-string');
const { getCACertificates } = require('../../utils/ca-cert');
/**
* Gets certificates and proxy configuration for a request
@@ -27,22 +27,13 @@ const getCertsAndProxyConfig = async ({
}
let caCertFilePath = preferencesUtil.shouldUseCustomCaCertificate() && preferencesUtil.getCustomCaCertificateFilePath();
let caCertificatesWithCertType = getCACertificates({
let caCertificatesData = await getCACertificates({
caCertFilePath,
shouldKeepDefaultCerts: preferencesUtil.shouldKeepDefaultCaCertificates()
});
let caCertificates = caCertificatesWithCertType.map(certData => certData.certificate);
let caCertificateDetails = caCertificatesWithCertType.reduce((details, certificateData) => {
// get the count for each certificate type
details[certificateData.type] += 1;
return details;
}, {
custom: 0,
bundled: 0,
system: 0,
extra: 0
});
let caCertificates = caCertificatesData.caCertificates;
let caCertificateDetails = caCertificatesData.caCertificatesCount;
// configure HTTPS agent with aggregated CA certificates
if (caCertificates?.length > 0) {

View File

@@ -0,0 +1,119 @@
const { systemCertsAsync } = require('system-ca');
const { rootCertificates } = require('node:tls');
const fs = require('node:fs');
let systemCertsCache;
async function getSystemCerts(systemCAOpts = {}) {
if (systemCertsCache) return systemCertsCache;
try {
systemCertsCache = await systemCertsAsync(systemCAOpts);
return systemCertsCache;
} catch (error) {
console.error(error);
return [];
}
}
function certToString(cert) {
return typeof cert === 'string'
? cert
: Buffer.from(cert.buffer, cert.byteOffset, cert.byteLength).toString('utf8');
}
function mergeCA(...args) {
const ca = new Set();
for (const item of args) {
if (!item) continue;
const caList = Array.isArray(item) ? item : [item];
for (const cert of caList) {
ca.add(certToString(cert));
}
}
return [...ca].join('\n');
}
function getNodeExtraCACerts() {
const extraCACertPath = process.env.NODE_EXTRA_CA_CERTS;
if (!extraCACertPath) return [];
try {
if (fs.existsSync(extraCACertPath)) {
const extraCACert = fs.readFileSync(extraCACertPath, 'utf8');
if (extraCACert && extraCACert.trim()) {
return [extraCACert];
}
}
} catch (err) {
console.error(`Failed to read NODE_EXTRA_CA_CERTS from ${extraCACertPath}:`, err.message);
}
return [];
}
const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true }) => {
try {
let caCertificates = '';
let caCertificatesCount = {
system: 0,
root: 0,
custom: 0,
extra: 0
}
let systemCerts = [];
let rootCerts = [];
let customCerts = [];
let nodeExtraCerts = [];
if (shouldKeepDefaultCerts) {
// get system certs
systemCerts = await getSystemCerts();
caCertificatesCount.system = systemCerts.length;
// get root certs
rootCerts = [...rootCertificates];
caCertificatesCount.root = rootCerts.length;
}
// handle user-provided custom CA certificate file with optional default certificates
if (caCertFilePath) {
// validate custom CA certificate file
if (fs.existsSync(caCertFilePath)) {
try {
const customCert = fs.readFileSync(caCertFilePath, 'utf8');
if (customCert && customCert.trim()) {
customCerts.push(customCert);
caCertificatesCount.custom = customCerts.length;
}
} catch (err) {
console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err).message);
throw new Error(`Unable to load custom CA certificate: ${(err).message}`);
}
}
}
// get NODE_EXTRA_CA_CERTS
nodeExtraCerts = getNodeExtraCACerts();
caCertificatesCount.extra = nodeExtraCerts.length;
// merge certs
const mergedCerts = mergeCA(systemCerts, rootCerts, customCerts, nodeExtraCerts);
caCertificates = mergedCerts;
return {
caCertificates,
caCertificatesCount
}
} catch (err) {
console.error('Error configuring CA certificates:', (err).message);
throw err; // Re-throw certificate loading errors as they're critical
}
}
module.exports = {
getCACertificates
};

View File

@@ -152,7 +152,7 @@ function createTimelineAgentClass(BaseAgentClass) {
});
}
const bundledCerts = this.caCertificateDetails.bundled || 0;
const rootCerts = this.caCertificateDetails.root || 0;
const systemCerts = this.caCertificateDetails.system || 0;
const extraCerts = this.caCertificateDetails.extra || 0;
const customCerts = this.caCertificateDetails.custom || 0;
@@ -160,7 +160,7 @@ function createTimelineAgentClass(BaseAgentClass) {
this.timeline.push({
timestamp: new Date(),
type: 'tls',
message: `CA Certificates: ${bundledCerts} bundled, ${systemCerts} system, ${extraCerts} extra, ${customCerts} custom`,
message: `CA Certificates: ${rootCerts} root, ${systemCerts} system, ${extraCerts} extra, ${customCerts} custom`,
});
// Log "Trying host:port..."

View File

@@ -2,6 +2,4 @@ export { addDigestInterceptor, getOAuth2Token } from './auth';
export { GrpcClient, generateGrpcSampleMessage } from './grpc';
export { default as cookies } from './cookies';
export { getCACertificates } from './network';
export * as scripting from './scripting';

View File

@@ -1,165 +0,0 @@
import * as fs from 'node:fs';
import { spawnSync } from 'node:child_process';
type T_CACertSource = 'bundled' | 'system' | 'extra'
type T_CACertificateData = {
type: T_CACertSource | 'custom';
certificate: string
}
/**
* Safely executes tls.getCACertificates in a separate Node.js process
* Returns empty array if the process fails or exits
*/
const safeTlsGetCACertificates = (certType: T_CACertSource): string[] => {
try {
// adding seperate script for each cert type
// to make sure no unexpected code can be included in the script
const getBundledCACertificatesScript = `
const tls = require('node:tls');
try {
const result = tls.getCACertificates('bundled');
console.log(JSON.stringify(result || []));
} catch (error) {
console.log('[]');
}
`;
const getSystemCACertificatesScript = `
const tls = require('node:tls');
try {
const result = tls.getCACertificates('system');
console.log(JSON.stringify(result || []));
} catch (error) {
console.log('[]');
}
`;
const getExtraCACertificatesScript = `
const tls = require('node:tls');
try {
const result = tls.getCACertificates('extra');
console.log(JSON.stringify(result || []));
} catch (error) {
console.log('[]');
}
`;
// bundled
let script = getBundledCACertificatesScript;
// system
if (certType === 'system') script = getSystemCACertificatesScript;
// extra
if (certType === 'extra') script = getExtraCACertificatesScript;
const result = spawnSync('node', ['-e', script], {
encoding: 'utf8',
timeout: 5000, // 5 second timeout
stdio: 'pipe',
maxBuffer: 1024 * 1024 * 50
});
if (result.error || result.status !== 0) {
return [];
}
const output = result.stdout.trim();
return JSON.parse(output);
} catch (error) {
// Return empty array if child process fails
return [];
}
};
/**
* retrieves default CA certificates from multiple sources using Node.js TLS API
*
* this function aggregates CA certificates from three sources:
* - 'bundled': mozilla CA certificates bundled with Node.js (same as tls.rootCertificates)
* - 'system': CA certificates from the system's trusted certificate store
* - 'extra': additional CA certificates loaded from `NODE_EXTRA_CA_CERTS` environment variable
*
* @returns {string[]} Array of PEM-encoded CA certificate strings
* @see https://nodejs.org/docs/latest-v22.x/api/tls.html#tlsgetcacertificatestype
*/
const getCerts = (sources: T_CACertSource[] = ['bundled', 'system', 'extra']): T_CACertificateData[] => {
let certificates: T_CACertificateData[] = [];
// iterate through different certificate store types to build comprehensive CA list
(sources).forEach(certType => {
try {
// get certificates from specific store type
const certList = safeTlsGetCACertificates(certType);
if (certList && Array.isArray(certList)) {
// filter out empty/invalid certificates to ensure we only include valid data
const validCertificates = certList.filter(cert => cert && cert.trim());
const validCertificatesWithCertType = validCertificates.map(certificate => ({
type: certType,
certificate
}));
certificates.push(...validCertificatesWithCertType);
}
} catch (err) {
console.warn(`Failed to load ${certType} CA certificates:`, (err as Error).message);
}
});
return certificates;
};
const getCACertificates = ({ caCertFilePath, shouldKeepDefaultCerts = true }: { caCertFilePath: string, shouldKeepDefaultCerts: boolean }) : T_CACertificateData[] => {
// CA certificate configuration
try {
let caCertificates: T_CACertificateData[] = [];
// handle user-provided custom CA certificate file with optional default certificates
if (caCertFilePath) {
// validate custom CA certificate file
if (fs.existsSync(caCertFilePath)) {
try {
const customCert = fs.readFileSync(caCertFilePath, 'utf8');
if (customCert && customCert.trim()) {
caCertificates.push({
type: 'custom',
certificate: customCert.trim()
});
}
} catch (err) {
console.error(`Failed to read custom CA certificate from ${caCertFilePath}:`, (err as Error).message);
throw new Error(`Unable to load custom CA certificate: ${(err as Error).message}`);
}
}
// optionally augment custom CA with default certificates
if (shouldKeepDefaultCerts) {
const defaultCertificates = getCerts(['bundled', 'system', 'extra']);
if (defaultCertificates?.length > 0) {
caCertificates.push(...defaultCertificates);
}
}
} else {
// use default CA certificates when no custom configuration is specified
const defaultCertificates = getCerts(['bundled', 'system', 'extra']);
if (defaultCertificates?.length > 0) {
caCertificates.push(...defaultCertificates);
}
}
return caCertificates;
} catch (err) {
console.error('Error configuring CA certificates:', (err as Error).message);
throw err; // Re-throw certificate loading errors as they're critical
}
}
export {
getCACertificates
};

View File

@@ -1,3 +1 @@
export { makeAxiosInstance } from './axios-instance';
export { getCACertificates } from './ca-cert';
export { makeAxiosInstance } from './axios-instance';