ssl-manager.js•13.6 kB
/**
* SSL Certificate Authority Manager
* Manages CA creation, certificate generation, and SSL bumping
*/
import fs from 'fs/promises';
import path from 'path';
import { execSync } from 'child_process';
import os from 'os';
export class SSLManager {
constructor(options = {}) {
this.homeDir = os.homedir();
this.caBaseDir = path.join(this.homeDir, '.ca');
this.defaultCAName = 'default';
this.currentCA = options.caName || this.defaultCAName;
this.caDir = path.join(this.caBaseDir, this.currentCA);
this.initialized = false;
}
/**
* Initialize SSL manager and check CA status
*/
async initialize() {
try {
await this._ensureDirectories();
const caExists = await this._checkCAExists();
if (!caExists) {
console.log(`🔒 CA '${this.currentCA}' not found, will create on first use`);
} else {
console.log(`🔒 Using existing CA: ${this.currentCA}`);
await this._loadCAInfo();
}
this.initialized = true;
return { caExists, caName: this.currentCA, caDir: this.caDir };
} catch (error) {
console.error('SSL Manager initialization failed:', error.message);
throw error;
}
}
/**
* Create new Certificate Authority
* @param {string} caName - CA name (optional, uses current)
* @param {Object} options - CA creation options
*/
async createCA(caName = null, options = {}) {
if (caName) {
this.currentCA = caName;
this.caDir = path.join(this.caBaseDir, this.currentCA);
}
await this._ensureDirectories();
const caExists = await this._checkCAExists();
if (caExists && !options.overwrite) {
throw new Error(`CA '${this.currentCA}' already exists. Use overwrite option to recreate.`);
}
console.log(`🔧 Creating new Certificate Authority: ${this.currentCA}`);
// Generate CA configuration
const caConfig = this._generateCAConfig(options);
const caConfigPath = path.join(this.caDir, 'ca.conf');
await fs.writeFile(caConfigPath, caConfig);
// Generate CA private key
const caKeyPath = path.join(this.caDir, 'ca.key');
const keyGenCmd = `openssl genrsa -out "${caKeyPath}" 4096`;
this._executeSSLCommand(keyGenCmd);
// Generate CA certificate
const caCertPath = path.join(this.caDir, 'ca.crt');
const certGenCmd = `openssl req -new -x509 -key "${caKeyPath}" -out "${caCertPath}" -days 3650 -config "${caConfigPath}"`;
this._executeSSLCommand(certGenCmd);
// Create certificate database
await this._initializeCertDB();
// Save CA metadata
await this._saveCAMetadata(options);
console.log(`✅ Certificate Authority '${this.currentCA}' created successfully`);
console.log(`📁 CA Directory: ${this.caDir}`);
console.log(`🔑 CA Certificate: ${caCertPath}`);
return {
caName: this.currentCA,
caDir: this.caDir,
caCertPath,
caKeyPath,
installationInstructions: this._getInstallationInstructions(caCertPath)
};
}
/**
* Generate server certificate for domain
* @param {string} domain - Domain name for certificate
* @param {Array} altNames - Alternative names (SAN)
*/
async generateServerCertificate(domain, altNames = []) {
if (!this.initialized) {
await this.initialize();
}
const caExists = await this._checkCAExists();
if (!caExists) {
throw new Error(`CA '${this.currentCA}' does not exist. Create CA first.`);
}
console.log(`🔐 Generating certificate for: ${domain}`);
const certDir = path.join(this.caDir, 'certs');
await fs.mkdir(certDir, { recursive: true });
const sanitizedDomain = domain.replace(/[^a-zA-Z0-9.-]/g, '_');
const keyPath = path.join(certDir, `${sanitizedDomain}.key`);
const csrPath = path.join(certDir, `${sanitizedDomain}.csr`);
const certPath = path.join(certDir, `${sanitizedDomain}.crt`);
// Generate server private key
const keyGenCmd = `openssl genrsa -out "${keyPath}" 2048`;
this._executeSSLCommand(keyGenCmd);
// Generate certificate config with SAN
const certConfig = this._generateServerCertConfig(domain, altNames);
const certConfigPath = path.join(certDir, `${sanitizedDomain}.conf`);
await fs.writeFile(certConfigPath, certConfig);
// Generate CSR
const csrCmd = `openssl req -new -key "${keyPath}" -out "${csrPath}" -config "${certConfigPath}"`;
this._executeSSLCommand(csrCmd);
// Sign certificate with CA
const caKeyPath = path.join(this.caDir, 'ca.key');
const caCertPath = path.join(this.caDir, 'ca.crt');
const signCmd = `openssl x509 -req -in "${csrPath}" -CA "${caCertPath}" -CAkey "${caKeyPath}" -CAcreateserial -out "${certPath}" -days 365 -extensions v3_req -extfile "${certConfigPath}"`;
this._executeSSLCommand(signCmd);
// Clean up CSR
await fs.unlink(csrPath);
await fs.unlink(certConfigPath);
console.log(`✅ Certificate generated for ${domain}`);
return {
domain,
keyPath,
certPath,
altNames
};
}
/**
* Get all available CAs
*/
async listCAs() {
try {
await fs.access(this.caBaseDir);
const entries = await fs.readdir(this.caBaseDir, { withFileTypes: true });
const cas = [];
for (const entry of entries) {
if (entry.isDirectory()) {
const caDir = path.join(this.caBaseDir, entry.name);
const metadataPath = path.join(caDir, 'metadata.json');
let metadata = { name: entry.name, created: null, description: null };
try {
const metadataContent = await fs.readFile(metadataPath, 'utf-8');
metadata = { ...metadata, ...JSON.parse(metadataContent) };
} catch (error) {
// Metadata file doesn't exist or is corrupted
}
const caExists = await fs.access(path.join(caDir, 'ca.crt')).then(() => true).catch(() => false);
cas.push({
name: entry.name,
path: caDir,
exists: caExists,
current: entry.name === this.currentCA,
...metadata
});
}
}
return cas;
} catch (error) {
return [];
}
}
/**
* Switch to different CA
*/
async switchCA(caName) {
this.currentCA = caName;
this.caDir = path.join(this.caBaseDir, this.currentCA);
const caExists = await this._checkCAExists();
if (!caExists) {
throw new Error(`CA '${caName}' does not exist`);
}
await this._loadCAInfo();
return { caName, caDir: this.caDir };
}
/**
* Get CA certificate for installation
*/
async getCACertificate() {
const caCertPath = path.join(this.caDir, 'ca.crt');
try {
const certContent = await fs.readFile(caCertPath, 'utf-8');
return {
certPath: caCertPath,
certContent,
installationInstructions: this._getInstallationInstructions(caCertPath)
};
} catch (error) {
throw new Error(`CA certificate not found: ${error.message}`);
}
}
/**
* Get installation instructions for current platform
*/
_getInstallationInstructions(caCertPath) {
const platform = os.platform();
const caName = this.currentCA;
const instructions = {
linux: [
`🐧 Linux Installation:`,
``,
`1. Copy CA certificate to system store:`,
` sudo cp "${caCertPath}" /usr/local/share/ca-certificates/${caName}.crt`,
` sudo update-ca-certificates`,
``,
`2. For browsers (Chrome/Chromium):`,
` chrome://settings/certificates → Authorities → Import`,
` Select: ${caCertPath}`,
``,
`3. For Firefox:`,
` about:preferences#privacy → Certificates → View Certificates`,
` → Authorities → Import → Select: ${caCertPath}`,
``,
`4. Verify installation:`,
` openssl verify -CAfile "${caCertPath}" <any_generated_cert>`
],
darwin: [
`🍎 macOS Installation:`,
``,
`1. Add to system keychain:`,
` sudo security add-trusted-cert -d -r trustRoot -k /System/Library/Keychains/SystemRootCertificates.keychain "${caCertPath}"`,
``,
`2. Alternative (user keychain):`,
` security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain "${caCertPath}"`,
``,
`3. For browsers:`,
` - Chrome: Uses system keychain automatically`,
` - Firefox: Manual import required (same as Linux)`,
``,
`4. Verify in Keychain Access app`
],
win32: [
`🪟 Windows Installation:`,
``,
`1. Import via Certificate Manager:`,
` certmgr.msc → Trusted Root Certification Authorities`,
` → Certificates → Import → Select: ${caCertPath}`,
``,
`2. Command line (as Administrator):`,
` certutil -addstore -f "ROOT" "${caCertPath}"`,
``,
`3. PowerShell (as Administrator):`,
` Import-Certificate -FilePath "${caCertPath}" -CertStoreLocation Cert:\\LocalMachine\\Root`,
``,
`4. Verify installation:`,
` certutil -store root | findstr "${caName}"`
]
};
const platformInstructions = instructions[platform] || instructions.linux;
return [
`🔒 SSL Certificate Installation Instructions`,
``,
`CA Name: ${caName}`,
`CA Certificate: ${caCertPath}`,
`Platform: ${platform}`,
``,
...platformInstructions,
``,
`⚠️ Important Security Notes:`,
`- This CA can decrypt ALL HTTPS traffic routed through the proxy`,
`- Only install on development/testing systems`,
`- Remove CA when proxy testing is complete`,
`- Keep CA private key secure and never share it`,
``,
`🔄 To remove CA later:`,
`- Linux: sudo update-ca-certificates --fresh`,
`- macOS: security delete-certificate -c "${caName}" (in Keychain Access)`,
`- Windows: certmgr.msc → Remove from Trusted Root CAs`
].join('\n');
}
/**
* Generate CA configuration
* @private
*/
_generateCAConfig(options) {
const subject = options.subject || {
C: 'US',
ST: 'CA',
L: 'San Francisco',
O: 'Web Proxy MCP Server',
OU: 'Development',
CN: `${this.currentCA} Root CA`
};
return `[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no
[req_distinguished_name]
C = ${subject.C}
ST = ${subject.ST}
L = ${subject.L}
O = ${subject.O}
OU = ${subject.OU}
CN = ${subject.CN}
[v3_ca]
basicConstraints = critical,CA:TRUE
keyUsage = critical,digitalSignature,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always`;
}
/**
* Generate server certificate configuration
* @private
*/
_generateServerCertConfig(domain, altNames) {
const sanEntries = [domain, ...altNames].map((name, index) =>
`DNS.${index + 1} = ${name}`
).join('\n');
return `[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no
[req_distinguished_name]
C = US
ST = CA
L = San Francisco
O = Web Proxy MCP Server
OU = Development
CN = ${domain}
[v3_req]
basicConstraints = CA:FALSE
keyUsage = nonRepudiation,digitalSignature,keyEncipherment
subjectAltName = @alt_names
[alt_names]
${sanEntries}`;
}
/**
* Execute SSL command safely
* @private
*/
_executeSSLCommand(command) {
try {
execSync(command, { stdio: 'pipe' });
} catch (error) {
throw new Error(`SSL command failed: ${error.message}`);
}
}
/**
* Check if CA exists
* @private
*/
async _checkCAExists() {
try {
await fs.access(path.join(this.caDir, 'ca.crt'));
await fs.access(path.join(this.caDir, 'ca.key'));
return true;
} catch (error) {
return false;
}
}
/**
* Ensure required directories exist
* @private
*/
async _ensureDirectories() {
await fs.mkdir(this.caBaseDir, { recursive: true });
await fs.mkdir(this.caDir, { recursive: true });
}
/**
* Initialize certificate database
* @private
*/
async _initializeCertDB() {
const dbDir = path.join(this.caDir, 'db');
await fs.mkdir(dbDir, { recursive: true });
// Create index file
await fs.writeFile(path.join(dbDir, 'index.txt'), '');
// Create serial file
await fs.writeFile(path.join(dbDir, 'serial'), '01\n');
}
/**
* Save CA metadata
* @private
*/
async _saveCAMetadata(options) {
const metadata = {
name: this.currentCA,
created: new Date().toISOString(),
description: options.description || `Auto-generated CA for Web Proxy MCP Server`,
version: '1.0',
...options.metadata
};
const metadataPath = path.join(this.caDir, 'metadata.json');
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
}
/**
* Load CA information
* @private
*/
async _loadCAInfo() {
try {
const metadataPath = path.join(this.caDir, 'metadata.json');
const metadata = await fs.readFile(metadataPath, 'utf-8');
this.caInfo = JSON.parse(metadata);
} catch (error) {
this.caInfo = { name: this.currentCA, created: null };
}
}
/**
* Get current CA status
*/
getCAStatus() {
return {
initialized: this.initialized,
currentCA: this.currentCA,
caDir: this.caDir,
caInfo: this.caInfo || null
};
}
}