Skip to content

Commit fb6c617

Browse files
SDK-6180 proxyCaCertificate: accept DER certs + multi-cert bundles (any extension)
caCertHelper read the cert as utf8 and addCACert'd it once: a DER (binary) cert (e.g. a Windows .cer export) was corrupted/dropped, and a multi-cert PEM bundle had only its first cert trusted. Added loadCaCertsAsPem (content-sniffs PEM single/bundle vs DER, converting DER->PEM via base64 wrap) and addCACert each cert. NODE_EXTRA_CA_CERTS now points at a PEM file (the customer's path when already PEM, else a PEM-converted temp) since Node can't load a raw DER through that var. So .pem/.crt/.cer/.der all work regardless of extension. Verified addCACert accepts both PEM and DER inputs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1046b54 commit fb6c617

1 file changed

Lines changed: 38 additions & 4 deletions

File tree

bin/helpers/caCertHelper.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ function _log(level, msg) {
3030
try { if (logger && typeof logger[level] === 'function') { logger[level](msg); } } catch (e) { /* ignore */ }
3131
}
3232

33+
// Convert a DER (binary) certificate Buffer to a PEM string (base64, 64-char lines).
34+
function derToPem(der) {
35+
const b64 = der.toString('base64').replace(/(.{64})/g, '$1\n');
36+
return `-----BEGIN CERTIFICATE-----\n${b64}${b64.endsWith('\n') ? '' : '\n'}-----END CERTIFICATE-----\n`;
37+
}
38+
39+
// Read a customer CA Buffer into an array of PEM cert strings, supporting BOTH PEM
40+
// (single or multi-cert bundle) and DER (binary) — any extension (.pem/.crt/.cer/.der).
41+
function loadCaCertsAsPem(buf) {
42+
if (buf.includes('-----BEGIN CERTIFICATE-----')) {
43+
return buf.toString('utf8').match(/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/g) || [];
44+
}
45+
return [derToPem(buf)]; // DER (binary) single cert
46+
}
47+
3348
function resolveCaCertPath(bsConfig) {
3449
let p = process.env.BROWSERSTACK_EXTRA_CA_CERTS;
3550
const cs = (bsConfig && bsConfig.connection_settings) || {};
@@ -51,17 +66,36 @@ function setupCaCertificate(bsConfig) {
5166
try {
5267
const certPath = resolveCaCertPath(bsConfig);
5368
if (!certPath) { return; }
54-
const caPem = fs.readFileSync(certPath, 'utf8');
69+
const buf = fs.readFileSync(certPath);
70+
const isPem = buf.includes('-----BEGIN CERTIFICATE-----');
71+
const pemCerts = loadCaCertsAsPem(buf);
72+
if (!pemCerts.length) {
73+
_log('warn', `proxyCaCertificate: no certificate found in ${certPath}; falling back to system trust store.`);
74+
return;
75+
}
5576

5677
const originalCreateSecureContext = tls.createSecureContext;
5778
tls.createSecureContext = function (options = {}) {
5879
const context = originalCreateSecureContext(options);
59-
try { context.context.addCACert(caPem); } catch (e) { /* best-effort merge */ }
80+
// addCACert one cert at a time so multi-cert bundles are all trusted.
81+
for (const pem of pemCerts) {
82+
try { context.context.addCACert(pem); } catch (e) { /* best-effort merge */ }
83+
}
6084
return context;
6185
};
6286

87+
// NODE_EXTRA_CA_CERTS must be a PEM file for child Node processes: reuse the
88+
// customer's path when it's already PEM, else write a PEM-converted copy (Node
89+
// can't load a raw DER file through that var).
6390
if (!process.env.NODE_EXTRA_CA_CERTS) {
64-
process.env.NODE_EXTRA_CA_CERTS = certPath;
91+
let nodeExtra = certPath;
92+
if (!isPem) {
93+
const os = require('os');
94+
const path = require('path');
95+
nodeExtra = path.join(os.tmpdir(), `browserstack_sdk_ca_${process.pid}.pem`);
96+
fs.writeFileSync(nodeExtra, pemCerts.join(''));
97+
}
98+
process.env.NODE_EXTRA_CA_CERTS = nodeExtra;
6599
}
66100

67101
_patched = true;
@@ -71,4 +105,4 @@ function setupCaCertificate(bsConfig) {
71105
}
72106
}
73107

74-
module.exports = { resolveCaCertPath, setupCaCertificate };
108+
module.exports = { resolveCaCertPath, setupCaCertificate, loadCaCertsAsPem, derToPem };

0 commit comments

Comments
 (0)