Files
bruno/tests/ssl/custom-ca-certs/server/helpers/certs.js
2025-12-04 01:37:20 +05:30

225 lines
7.7 KiB
JavaScript

const { execCommand, execCommandSilent, detectPlatform } = require('./platform');
const fs = require('node:fs');
const path = require('node:path');
function createCertsDir(certsDir) {
if (fs.existsSync(certsDir)) {
fs.rmSync(certsDir, { recursive: true, force: true });
}
fs.mkdirSync(certsDir, { recursive: true });
}
function generateCertificates(certsDir) {
execCommand('openssl version');
// Generate CA private key
execCommand('openssl genrsa -out ca-key.pem 4096', certsDir);
// Create CA configuration file with proper CA extensions and subject (LibreSSL/OpenSSL compatible)
const caConfigContent = `[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no
[req_distinguished_name]
C = US
ST = Dev
L = Local
O = Local Dev CA
CN = Local Dev CA
[v3_ca]
basicConstraints = critical, CA:TRUE
keyUsage = critical, keyCertSign, cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always`;
fs.writeFileSync(path.join(certsDir, 'ca.conf'), caConfigContent);
// Generate CA certificate with proper CA extensions using config file (no -subj needed)
execCommand('openssl req -new -x509 -key ca-key.pem -out ca-cert.pem -days 3650 -config ca.conf', certsDir);
// Generate server private key and CSR
execCommand('openssl genrsa -out localhost-key.pem 4096', certsDir);
// Create server CSR configuration file
const serverCsrConfigContent = `[req]
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
C = US
ST = Dev
L = Local
O = Local Dev
CN = localhost`;
fs.writeFileSync(path.join(certsDir, 'localhost-csr.conf'), serverCsrConfigContent);
execCommand('openssl req -new -key localhost-key.pem -out localhost.csr -config localhost-csr.conf', certsDir);
// Create server certificate configuration file (LibreSSL/OpenSSL compatible)
const serverConfigContent = `[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = Country Name
ST = State or Province Name
L = Locality Name
O = Organization Name
CN = Common Name
[v3_req]
keyUsage = critical, keyEncipherment, dataEncipherment, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
basicConstraints = critical, CA:FALSE
authorityKeyIdentifier = keyid:always,issuer:always
[alt_names]
DNS.1 = localhost
DNS.2 = localhost.localdomain
IP.1 = 127.0.0.1
IP.2 = ::1
IP.3 = ::ffff:127.0.0.1`;
fs.writeFileSync(path.join(certsDir, 'localhost.conf'), serverConfigContent);
execCommand('openssl x509 -req -in localhost.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out localhost-cert.pem -days 730 -extensions v3_req -extfile localhost.conf', certsDir);
const platform = detectPlatform();
if (platform === 'windows') {
execCommand('openssl x509 -in ca-cert.pem -outform DER -out ca-cert.der', certsDir);
execCommand('openssl pkcs12 -export -out localhost.p12 -inkey localhost-key.pem -in localhost-cert.pem -certfile ca-cert.pem -password pass:', certsDir);
execCommand('openssl x509 -in localhost-cert.pem -outform DER -out localhost-cert.der', certsDir);
}
if (platform !== 'windows') {
execCommand('chmod 600 ca-key.pem localhost-key.pem', certsDir);
execCommand('chmod 644 ca-cert.pem localhost-cert.pem', certsDir);
}
['localhost.csr', 'localhost.conf', 'localhost-csr.conf', 'ca.conf', 'ca-cert.srl'].forEach((file) => {
const filePath = path.join(certsDir, file);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
});
// Validate certificate chain
validateCertificateChain(certsDir);
}
function validateCertificateChain(certsDir) {
try {
// Verify CA certificate is valid and has proper CA extensions
const caVerifyOutput = execCommandSilent('openssl x509 -in ca-cert.pem -text -noout', certsDir).toString();
if (!caVerifyOutput.includes('CA:TRUE')) {
throw new Error('CA certificate missing basicConstraints=CA:TRUE');
}
if (!caVerifyOutput.includes('Certificate Sign')) {
throw new Error('CA certificate missing keyCertSign in keyUsage');
}
// Verify server certificate is valid and signed by CA
const serverVerifyOutput = execCommandSilent('openssl x509 -in localhost-cert.pem -text -noout', certsDir).toString();
if (!serverVerifyOutput.includes('CA:FALSE')) {
throw new Error('Server certificate should have basicConstraints=CA:FALSE');
}
if (!serverVerifyOutput.includes('TLS Web Server Authentication')) {
throw new Error('Server certificate missing serverAuth in extendedKeyUsage');
}
// Verify certificate chain
execCommandSilent('openssl verify -CAfile ca-cert.pem localhost-cert.pem', certsDir);
console.log('✅ Certificate chain validation passed');
} catch (error) {
console.error('❌ Certificate validation failed:', error.message);
throw new Error(`Certificate validation failed: ${error.message}`);
}
}
function addCAToTruststore(certsDir) {
const platform = detectPlatform();
switch (platform) {
case 'macos': {
const macCertPath = path.join(certsDir, 'ca-cert.pem');
execCommand(`sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${macCertPath}"`);
break;
}
case 'linux': {
const linuxCertPath = path.join(certsDir, 'ca-cert.pem');
execCommand(`sudo cp "${linuxCertPath}" /usr/local/share/ca-certificates/bruno-ca.crt`);
execCommand('sudo update-ca-certificates');
break;
}
case 'windows': {
const winCertPath = path.join(certsDir, 'ca-cert.der');
// Escape backslashes for PowerShell
const psPath = winCertPath.replace(/\\/g, '\\\\');
// PowerShell .NET method (works reliably in CI)
const psCommand = [
`$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2('${psPath}');`,
`$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','LocalMachine');`,
`$store.Open('ReadWrite');`,
`$store.Add($cert);`,
`$store.Close();`,
// Verify cert was added by checking if it exists in LocalMachine\Root
`$verifyStore = New-Object System.Security.Cryptography.X509Certificates.X509Store('Root','LocalMachine');`,
`$verifyStore.Open('ReadOnly');`,
`$found = $verifyStore.Certificates | Where-Object { $_.Thumbprint -eq $cert.Thumbprint };`,
`$verifyStore.Close();`,
`if (-not $found) { throw 'Certificate was not added to LocalMachine\Root' };`
].join(' ');
execCommand(`powershell -Command "${psCommand}"`);
break;
}
default:
throw new Error(`Unsupported platform: ${platform}`);
}
}
function verifyCertificates(certsDir) {
const platform = detectPlatform();
// Core PEM files required for all platforms
const requiredFiles = ['ca-cert.pem', 'ca-key.pem', 'localhost-cert.pem', 'localhost-key.pem'];
// Verify required PEM files exist
for (const file of requiredFiles) {
const filePath = path.join(certsDir, file);
if (!fs.existsSync(filePath)) {
throw new Error(`missing certificate file: ${file}`);
}
}
// Check Windows-specific files but don't require them (they're optional fallbacks)
if (platform === 'windows') {
const windowsFiles = ['ca-cert.der', 'localhost.p12', 'localhost-cert.der'];
for (const file of windowsFiles) {
const filePath = path.join(certsDir, file);
if (fs.existsSync(filePath)) {
console.log(`✅ Windows certificate file available: ${file}`);
} else {
console.log(`⚠️ Windows certificate file missing (but not required): ${file}`);
}
}
}
}
module.exports = {
createCertsDir,
generateCertificates,
addCAToTruststore,
verifyCertificates
};