certificate-manager.js•13.9 kB
/**
* Certificate Authority Manager
* Manages SSL certificates for SSL Bumping functionality
*/
import fs from 'fs/promises';
import path from 'path';
import { execSync, spawn } from 'child_process';
import { promisify } from 'util';
import os from 'os';
export class CertificateManager {
constructor(options = {}) {
this.homeDir = os.homedir();
this.caBaseDir = path.join(this.homeDir, '.ca');
this.defaultCaName = options.defaultCaName || 'default';
this.currentCaPath = null;
this.certificates = new Map(); // Cache for generated certificates
// Certificate settings
this.caConfig = {
country: options.country || 'JP',
state: options.state || 'Tokyo',
city: options.city || 'Tokyo',
organization: options.organization || 'Web Proxy MCP Server',
organizationalUnit: options.organizationalUnit || 'Development Team',
emailAddress: options.emailAddress || 'admin@proxy-mcp.local'
};
}
/**
* Initialize certificate authority
* @param {string} caName - CA name (default: 'default')
* @param {boolean} createNew - Force create new CA even if exists
* @returns {Object} CA initialization result
*/
async initializeCA(caName = this.defaultCaName, createNew = false) {
const caPath = path.join(this.caBaseDir, caName);
try {
// Check if CA already exists
const caExists = await this._checkCAExists(caPath);
if (caExists && !createNew) {
console.log(`📋 Using existing CA: ${caName}`);
this.currentCaPath = caPath;
return {
status: 'existing',
caName,
caPath,
certPath: path.join(caPath, 'ca.crt'),
keyPath: path.join(caPath, 'ca.key'),
message: `Using existing certificate authority: ${caName}`
};
}
// Create new CA
console.log(`🔧 Creating new certificate authority: ${caName}`);
await this._createNewCA(caPath, caName);
this.currentCaPath = caPath;
return {
status: 'created',
caName,
caPath,
certPath: path.join(caPath, 'ca.crt'),
keyPath: path.join(caPath, 'ca.key'),
message: `Created new certificate authority: ${caName}`,
installInstructions: this._getInstallInstructions(path.join(caPath, 'ca.crt'))
};
} catch (error) {
throw new Error(`Failed to initialize CA: ${error.message}`);
}
}
/**
* Generate server certificate for domain
* @param {string} domain - Domain name
* @param {Array} altNames - Alternative names (SANs)
* @returns {Object} Certificate paths
*/
async generateServerCertificate(domain, altNames = []) {
if (!this.currentCaPath) {
throw new Error('CA not initialized. Call initializeCA() first.');
}
const certKey = domain.toLowerCase();
// Check cache first
if (this.certificates.has(certKey)) {
return this.certificates.get(certKey);
}
try {
const certDir = path.join(this.currentCaPath, 'certs', domain);
await fs.mkdir(certDir, { recursive: true });
const certPath = path.join(certDir, 'server.crt');
const keyPath = path.join(certDir, 'server.key');
const csrPath = path.join(certDir, 'server.csr');
// Generate private key
await this._execOpenSSL([
'genrsa', '-out', keyPath, '2048'
]);
// Create certificate signing request
const subject = `/C=${this.caConfig.country}/ST=${this.caConfig.state}/L=${this.caConfig.city}/O=${this.caConfig.organization}/OU=${this.caConfig.organizationalUnit}/CN=${domain}/emailAddress=${this.caConfig.emailAddress}`;
await this._execOpenSSL([
'req', '-new', '-key', keyPath, '-out', csrPath,
'-subj', subject
]);
// Create extensions file for SAN
const extPath = path.join(certDir, 'server.ext');
const allNames = [domain, ...altNames].filter((name, index, self) => self.indexOf(name) === index);
const sanList = allNames.map((name, index) => `DNS.${index + 1}:${name}`).join(',');
const extContent = `authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
${allNames.map((name, index) => `DNS.${index + 1} = ${name}`).join('\n')}`;
await fs.writeFile(extPath, extContent);
// Sign the certificate
const caCertPath = path.join(this.currentCaPath, 'ca.crt');
const caKeyPath = path.join(this.currentCaPath, 'ca.key');
await this._execOpenSSL([
'x509', '-req', '-in', csrPath,
'-CA', caCertPath, '-CAkey', caKeyPath,
'-CAcreateserial', '-out', certPath,
'-days', '365', '-sha256',
'-extensions', 'v3_req', '-extfile', extPath
]);
const result = {
domain,
certPath,
keyPath,
altNames: allNames,
createdAt: new Date().toISOString()
};
// Cache the result
this.certificates.set(certKey, result);
console.log(`📜 Generated certificate for: ${domain}`);
return result;
} catch (error) {
throw new Error(`Failed to generate certificate for ${domain}: ${error.message}`);
}
}
/**
* List available certificate authorities
* @returns {Array} List of available CAs
*/
async listCertificateAuthorities() {
try {
const caList = [];
try {
const entries = await fs.readdir(this.caBaseDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const caPath = path.join(this.caBaseDir, entry.name);
const caExists = await this._checkCAExists(caPath);
if (caExists) {
const certPath = path.join(caPath, 'ca.crt');
const info = await this._getCertificateInfo(certPath);
caList.push({
name: entry.name,
path: caPath,
certPath,
info,
isCurrent: caPath === this.currentCaPath
});
}
}
}
} catch (error) {
// CA directory might not exist yet
console.log('No certificate authorities found');
}
return caList;
} catch (error) {
throw new Error(`Failed to list CAs: ${error.message}`);
}
}
/**
* Get certificate info
* @param {string} certPath - Certificate file path
* @returns {Object} Certificate information
*/
async getCertificateInfo(certPath) {
return await this._getCertificateInfo(certPath);
}
/**
* Get current CA status
* @returns {Object} Current CA status
*/
getCurrentCAStatus() {
if (!this.currentCaPath) {
return {
initialized: false,
message: 'No certificate authority initialized'
};
}
return {
initialized: true,
caPath: this.currentCaPath,
caName: path.basename(this.currentCaPath),
certPath: path.join(this.currentCaPath, 'ca.crt'),
keyPath: path.join(this.currentCaPath, 'ca.key'),
generatedCerts: this.certificates.size
};
}
/**
* Get installation instructions for CA certificate
* @returns {Object} Installation instructions
*/
getInstallationInstructions() {
if (!this.currentCaPath) {
throw new Error('No CA initialized');
}
const certPath = path.join(this.currentCaPath, 'ca.crt');
return this._getInstallInstructions(certPath);
}
/**
* Clear certificate cache
*/
clearCertificateCache() {
this.certificates.clear();
console.log('🗑️ Certificate cache cleared');
}
/**
* Check if CA exists
* @private
*/
async _checkCAExists(caPath) {
try {
const certPath = path.join(caPath, 'ca.crt');
const keyPath = path.join(caPath, 'ca.key');
await fs.access(certPath);
await fs.access(keyPath);
return true;
} catch (error) {
return false;
}
}
/**
* Create new certificate authority
* @private
*/
async _createNewCA(caPath, caName) {
// Create CA directory structure
await fs.mkdir(caPath, { recursive: true });
await fs.mkdir(path.join(caPath, 'certs'), { recursive: true });
const certPath = path.join(caPath, 'ca.crt');
const keyPath = path.join(caPath, 'ca.key');
// Generate CA private key
await this._execOpenSSL([
'genrsa', '-out', keyPath, '4096'
]);
// Generate CA certificate
const subject = `/C=${this.caConfig.country}/ST=${this.caConfig.state}/L=${this.caConfig.city}/O=${this.caConfig.organization}/OU=${this.caConfig.organizationalUnit}/CN=${caName} Root CA/emailAddress=${this.caConfig.emailAddress}`;
await this._execOpenSSL([
'req', '-new', '-x509', '-key', keyPath, '-sha256',
'-days', '3650', '-out', certPath,
'-subj', subject
]);
// Create serial file
await fs.writeFile(path.join(caPath, 'ca.srl'), '1000');
console.log(`✅ Created certificate authority: ${caName}`);
}
/**
* Execute OpenSSL command
* @private
*/
async _execOpenSSL(args) {
return new Promise((resolve, reject) => {
const process = spawn('openssl', args, {
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
process.stdout.on('data', (data) => {
stdout += data.toString();
});
process.stderr.on('data', (data) => {
stderr += data.toString();
});
process.on('close', (code) => {
if (code === 0) {
resolve(stdout);
} else {
reject(new Error(`OpenSSL failed: ${stderr || stdout}`));
}
});
process.on('error', (error) => {
reject(new Error(`Failed to execute OpenSSL: ${error.message}`));
});
});
}
/**
* Get certificate information
* @private
*/
async _getCertificateInfo(certPath) {
try {
const output = await this._execOpenSSL([
'x509', '-in', certPath, '-text', '-noout'
]);
// Parse certificate info (simplified)
const lines = output.split('\n');
const info = {
subject: this._extractField(lines, 'Subject:'),
issuer: this._extractField(lines, 'Issuer:'),
validFrom: this._extractField(lines, 'Not Before:'),
validTo: this._extractField(lines, 'Not After:'),
serialNumber: this._extractField(lines, 'Serial Number:')
};
return info;
} catch (error) {
return { error: error.message };
}
}
/**
* Extract field from certificate text
* @private
*/
_extractField(lines, fieldName) {
const line = lines.find(line => line.trim().startsWith(fieldName));
return line ? line.split(fieldName)[1].trim() : 'Unknown';
}
/**
* Get installation instructions
* @private
*/
_getInstallInstructions(certPath) {
const platform = os.platform();
const instructions = {
certPath,
platform,
instructions: {}
};
if (platform === 'linux') {
instructions.instructions = {
system: {
title: 'System-wide installation (Ubuntu/Debian)',
commands: [
`sudo cp "${certPath}" /usr/local/share/ca-certificates/web-proxy-mcp.crt`,
'sudo update-ca-certificates'
],
description: 'Installs the certificate system-wide for all applications'
},
chrome: {
title: 'Google Chrome',
steps: [
'1. Open Chrome Settings',
'2. Go to Privacy and Security > Security',
'3. Click "Manage certificates"',
'4. Go to "Authorities" tab',
'5. Click "Import"',
`6. Select file: ${certPath}`,
'7. Check "Trust this certificate for identifying websites"'
]
},
firefox: {
title: 'Mozilla Firefox',
steps: [
'1. Open Firefox Preferences',
'2. Go to Privacy & Security',
'3. Scroll to "Certificates" section',
'4. Click "View Certificates"',
'5. Go to "Authorities" tab',
'6. Click "Import"',
`7. Select file: ${certPath}`,
'8. Check "Trust this CA to identify websites"'
]
}
};
} else if (platform === 'darwin') {
instructions.instructions = {
keychain: {
title: 'macOS Keychain',
commands: [
`security add-trusted-cert -d -r trustRoot -k ~/Library/Keychains/login.keychain "${certPath}"`
],
description: 'Adds certificate to user keychain as trusted root'
},
system: {
title: 'System Keychain (requires admin)',
commands: [
`sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "${certPath}"`
],
description: 'Installs certificate system-wide'
}
};
} else if (platform === 'win32') {
instructions.instructions = {
certmgr: {
title: 'Windows Certificate Manager',
steps: [
'1. Run "certmgr.msc" as administrator',
'2. Navigate to "Trusted Root Certification Authorities" > "Certificates"',
'3. Right-click and select "All Tasks" > "Import"',
`4. Select file: ${certPath}`,
'5. Place in "Trusted Root Certification Authorities" store'
]
},
powershell: {
title: 'PowerShell (as administrator)',
commands: [
`Import-Certificate -FilePath "${certPath}" -CertStoreLocation Cert:\\LocalMachine\\Root`
]
}
};
}
return instructions;
}
}