ca certs updates and fixes (#5549)

This commit is contained in:
lohit
2025-09-12 16:03:27 +05:30
committed by lohit-bruno
parent 6d8dbeac11
commit b02639d30a
14 changed files with 104 additions and 179 deletions

View File

@@ -6,6 +6,7 @@ runs:
- name: Install additional OS dependencies for custom CA certs
shell: bash
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install -y \
libglib2.0-0 libnss3 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libgtk-3-0 libasound2t64 \
xvfb libxml2-utils

3
package-lock.json generated
View File

@@ -28363,7 +28363,6 @@
"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"
},
@@ -30129,7 +30128,6 @@
"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"
@@ -31931,6 +31929,7 @@
"axios": "^1.9.0",
"grpc-reflection-js": "^0.3.0",
"is-ip": "^5.0.1",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0"
},
"devDependencies": {

View File

@@ -176,11 +176,11 @@ const General = ({ close }) => {
name="keepDefaultCaCertificates.enabled"
checked={formik.values.keepDefaultCaCertificates.enabled}
onChange={formik.handleChange}
className={`mousetrap mr-0 ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
disabled={formik.values.customCaCertificate.enabled ? false : true}
className={`mousetrap mr-0 ${formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? '' : 'opacity-25'}`}
disabled={formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? false : true}
/>
<label
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled ? '' : 'opacity-25'}`}
className={`block ml-2 select-none ${formik.values.customCaCertificate.enabled && formik.values.customCaCertificate.filePath ? '' : 'opacity-25'}`}
htmlFor="keepDefaultCaCertificatesEnabled"
>
Keep Default CA Certificates

View File

@@ -69,7 +69,6 @@
"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

@@ -1,9 +1,7 @@
const os = require('os');
const qs = require('qs');
const chalk = require('chalk');
const decomment = require('decomment');
const fs = require('fs');
const tls = require('tls');
const { forOwn, isUndefined, isNull, each, extend, get, compact } = require('lodash');
const FormData = require('form-data');
const prepareRequest = require('./prepare-request');
@@ -26,7 +24,7 @@ const { getOAuth2Token } = require('./oauth2');
const protocolRegex = /^([-+\w]{1,25})(:?\/\/|:)/;
const { NtlmClient } = require('axios-ntlm');
const { addDigestInterceptor } = require('@usebruno/requests');
const { getCACertificates } = require('../utils/ca-cert');
const { getCACertificates } = require('@usebruno/requests');
const { encodeUrl } = require('@usebruno/common').utils;
const onConsoleLog = (type, args) => {
@@ -156,13 +154,10 @@ const runSingleRequest = async function (
if (insecure) {
httpsAgentRequestFields['rejectUnauthorized'] = false;
} else {
const caCertArray = [options['cacert'], process.env.SSL_CERT_FILE];
const caCertFilePath = caCertArray.find((el) => el);
const caCertFilePath = options['cacert'];
let caCertificatesData = await getCACertificates({ caCertFilePath, shouldKeepDefaultCerts: !options['ignoreTruststore'] });
let caCertificates = caCertificatesData.caCertificates;
if (caCertificates?.length > 0) {
httpsAgentRequestFields['ca'] = caCertificates;
}
httpsAgentRequestFields['ca'] = caCertificates || [];
}
const interpolationOptions = {

View File

@@ -67,7 +67,6 @@
"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
@@ -33,13 +33,11 @@ const getCertsAndProxyConfig = async ({
});
let caCertificates = caCertificatesData.caCertificates;
let caCertificateDetails = caCertificatesData.caCertificatesCount;
let caCertificatesCount = caCertificatesData.caCertificatesCount;
// configure HTTPS agent with aggregated CA certificates
if (caCertificates?.length > 0) {
httpsAgentRequestFields['caCertificateDetails'] = caCertificateDetails;
httpsAgentRequestFields['ca'] = caCertificates;
}
httpsAgentRequestFields['caCertificatesCount'] = caCertificatesCount;
httpsAgentRequestFields['ca'] = caCertificates || [];
const brunoConfig = getBrunoConfig(collectionUid);
const interpolationOptions = {

View File

@@ -1,119 +0,0 @@
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

@@ -88,8 +88,8 @@ function createTimelineAgentClass(BaseAgentClass) {
return class extends BaseAgentClass {
constructor(options, timeline) {
let caCertificateDetails = options.caCertificateDetails || {};
delete options.caCertificateDetails;
let caCertificatesCount = options.caCertificatesCount || {};
delete options.caCertificatesCount;
// For proxy agents, the first argument is the proxy URI and the second is options
if (options?.proxy) {
@@ -136,7 +136,7 @@ function createTimelineAgentClass(BaseAgentClass) {
});
}
this.caCertificateDetails = caCertificateDetails;
this.caCertificatesCount = caCertificatesCount;
}
@@ -152,10 +152,10 @@ function createTimelineAgentClass(BaseAgentClass) {
});
}
const rootCerts = this.caCertificateDetails.root || 0;
const systemCerts = this.caCertificateDetails.system || 0;
const extraCerts = this.caCertificateDetails.extra || 0;
const customCerts = this.caCertificateDetails.custom || 0;
const rootCerts = this.caCertificatesCount.root || 0;
const systemCerts = this.caCertificatesCount.system || 0;
const extraCerts = this.caCertificatesCount.extra || 0;
const customCerts = this.caCertificatesCount.custom || 0;
this.timeline.push({
timestamp: new Date(),

View File

@@ -27,6 +27,7 @@
"axios": "^1.9.0",
"grpc-reflection-js": "^0.3.0",
"is-ip": "^5.0.1",
"system-ca": "^2.0.1",
"tough-cookie": "^6.0.0"
},
"devDependencies": {

View File

@@ -39,6 +39,6 @@ module.exports = [
typescript({ tsconfig: './tsconfig.json' }),
terser(),
],
external: ['axios', 'qs']
external: ['axios', 'qs', 'system-ca']
}
];

View File

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

View File

@@ -1,10 +1,25 @@
const { systemCertsAsync } = require('system-ca');
const { rootCertificates } = require('node:tls');
const fs = require('node:fs');
import { systemCertsAsync, Options as SystemCAOptions } from 'system-ca';
import { rootCertificates } from 'node:tls';
import * as fs from 'node:fs';
let systemCertsCache;
type T_CACertificatesOptions = {
caCertFilePath?: string;
shouldKeepDefaultCerts?: boolean;
}
async function getSystemCerts(systemCAOpts = {}) {
type T_CACertificatesResult = {
caCertificates: string;
caCertificatesCount: {
system: number;
root: number;
custom: number;
extra: number;
};
}
let systemCertsCache: string[] | undefined;
async function getSystemCerts(systemCAOpts: SystemCAOptions = {}): Promise<string[]> {
if (systemCertsCache) return systemCertsCache;
try {
@@ -17,25 +32,27 @@ async function getSystemCerts(systemCAOpts = {}) {
}
}
function certToString(cert) {
function certToString(cert: string | Buffer) {
return typeof cert === 'string'
? cert
: Buffer.from(cert.buffer, cert.byteOffset, cert.byteLength).toString('utf8');
}
function mergeCA(...args) {
const ca = new Set();
function mergeCA(...args: (string | string[])[]): string {
const ca = new Set<string>();
for (const item of args) {
if (!item) continue;
const caList = Array.isArray(item) ? item : [item];
for (const cert of caList) {
ca.add(certToString(cert));
if (cert) {
ca.add(certToString(cert));
}
}
}
return [...ca].join('\n');
}
function getNodeExtraCACerts() {
function getNodeExtraCACerts(): string[] {
const extraCACertPath = process.env.NODE_EXTRA_CA_CERTS;
if (!extraCACertPath) return [];
@@ -47,14 +64,34 @@ function getNodeExtraCACerts() {
}
}
} catch (err) {
console.error(`Failed to read NODE_EXTRA_CA_CERTS from ${extraCACertPath}:`, err.message);
console.error(`Failed to read NODE_EXTRA_CA_CERTS from ${extraCACertPath}:`, (err as Error).message);
}
return [];
}
/**
* Get CA certificates
*
* Generic function to get CA certificates
* - System CA certificates (From OS)
* - Root CA certificates (From Node)
* - Custom CA certificates (From user-provided file)
* - NODE_EXTRA_CA_CERTS (From environment variable)
*
* If no custom CA certificate file path is provided
* return system CA certificates and root certificates + NODE_EXTRA_CA_CERTS
*
* If custom CA certificate file path is provided
* use custom CA certificate file + NODE_EXTRA_CA_CERTS
* ignore system + root certificates if shouldKeepDefaultCerts is false
*
* @param caCertFilePath - path to custom CA certificate file
* @param shouldKeepDefaultCerts - whether to keep default CA certificates
* @returns {Promise<T_CACertificatesResult>} - CA certificates and their count
*/
const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true }) => {
const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true }: T_CACertificatesOptions): Promise<T_CACertificatesResult> => {
try {
let caCertificates = '';
let caCertificatesCount = {
@@ -64,20 +101,11 @@ const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true
extra: 0
}
let systemCerts = [];
let rootCerts = [];
let customCerts = [];
let nodeExtraCerts = [];
let systemCerts: string[] = [];
let rootCerts: string[] = [];
let customCerts: string[] = [];
let nodeExtraCerts: string[] = [];
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) {
@@ -90,10 +118,30 @@ const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true
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}`);
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}`);
}
} else {
throw new Error(`Invalid custom CA certificate path: ${caCertFilePath}`);
}
if (shouldKeepDefaultCerts) {
// get system certs
systemCerts = await getSystemCerts();
caCertificatesCount.system = systemCerts.length;
// get root certs
rootCerts = [...rootCertificates];
caCertificatesCount.root = rootCerts.length;
}
} else {
// get system certs
systemCerts = await getSystemCerts();
caCertificatesCount.system = systemCerts.length;
// get root certs
rootCerts = [...rootCertificates];
caCertificatesCount.root = rootCerts.length;
}
// get NODE_EXTRA_CA_CERTS
@@ -109,11 +157,11 @@ const getCACertificates = async ({ caCertFilePath, shouldKeepDefaultCerts = true
caCertificatesCount
}
} catch (err) {
console.error('Error configuring CA certificates:', (err).message);
console.error('Error configuring CA certificates:', (err as Error).message);
throw err; // Re-throw certificate loading errors as they're critical
}
}
module.exports = {
export {
getCACertificates
};

View File

@@ -35,12 +35,14 @@ export default defineConfig({
{
command: 'npm run dev:web',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI
reuseExistingServer: !process.env.CI,
timeout: 10 * 60 * 1000
},
{
command: 'npm start --workspace=packages/bruno-tests',
url: 'http://localhost:8081/ping',
reuseExistingServer: !process.env.CI
reuseExistingServer: !process.env.CI,
timeout: 10 * 60 * 1000
}
]
});