From 27b5da2e305242425a7e965c27c18f97ceb32f02 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 15 Nov 2024 14:37:20 +0100 Subject: [PATCH] feat: add debug logs --- src/certificate.ts | 138 +++++++++++++++++++++++++++++++-------------- src/utils.ts | 8 +++ 2 files changed, 104 insertions(+), 42 deletions(-) diff --git a/src/certificate.ts b/src/certificate.ts index b6c8705..743cc64 100644 --- a/src/certificate.ts +++ b/src/certificate.ts @@ -1,11 +1,11 @@ -import type { AddCertOption, CertOption, GenerateCertReturn } from './types' +import type { AddCertOption, CertOption, GenerateCertReturn, TlsOption } from './types' import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { log, runCommand } from '@stacksjs/cli' import forge, { pki, tls } from 'node-forge' import { config } from './config' -import { findFoldersWithFile, makeNumberPositive } from './utils' +import { debugLog, findFoldersWithFile, makeNumberPositive } from './utils' export interface Cert { certificate: string @@ -17,7 +17,10 @@ export interface Cert { * @returns The serial number for the Certificate */ export function randomSerialNumber(): string { - return makeNumberPositive(forge.util.bytesToHex(forge.random.getBytesSync(20))) + debugLog('cert', 'Generating random serial number') + const serialNumber = makeNumberPositive(forge.util.bytesToHex(forge.random.getBytesSync(20))) + debugLog('cert', `Generated serial number: ${serialNumber}`) + return serialNumber } /** @@ -25,11 +28,14 @@ export function randomSerialNumber(): string { * @returns The Not Before Date for the Certificate */ export function getCertNotBefore(): Date { + debugLog('cert', 'Calculating certificate not-before date') const twoDaysAgo = new Date(Date.now() - 60 * 60 * 24 * 2 * 1000) const year = twoDaysAgo.getFullYear() const month = (twoDaysAgo.getMonth() + 1).toString().padStart(2, '0') const day = twoDaysAgo.getDate().toString().padStart(2, '0') - return new Date(`${year}-${month}-${day}T23:59:59Z`) + const date = new Date(`${year}-${month}-${day}T23:59:59Z`) + debugLog('cert', `Certificate not-before date: ${date.toISOString()}`) + return date } /** @@ -38,14 +44,16 @@ export function getCertNotBefore(): Date { * @returns The Not After Date for the Certificate */ export function getCertNotAfter(notBefore: Date): Date { + debugLog('cert', 'Calculating certificate not-after date') const validityDays = config.validityDays // defaults to 180 days const daysInMillis = validityDays * 60 * 60 * 24 * 1000 const notAfterDate = new Date(notBefore.getTime() + daysInMillis) const year = notAfterDate.getFullYear() const month = (notAfterDate.getMonth() + 1).toString().padStart(2, '0') const day = notAfterDate.getDate().toString().padStart(2, '0') - - return new Date(`${year}-${month}-${day}T23:59:59Z`) + const date = new Date(`${year}-${month}-${day}T23:59:59Z`) + debugLog('cert', `Certificate not-after date: ${date.toISOString()} (${validityDays} days validity)`) + return date } /** @@ -54,28 +62,39 @@ export function getCertNotAfter(notBefore: Date): Date { * @returns The Not After Date for the CA */ export function getCANotAfter(notBefore: Date): Date { + debugLog('cert', 'Calculating CA not-after date') const year = notBefore.getFullYear() + 100 const month = (notBefore.getMonth() + 1).toString().padStart(2, '0') const day = notBefore.getDate().toString().padStart(2, '0') - - return new Date(`${year}-${month}-${day}T23:59:59Z`) + const date = new Date(`${year}-${month}-${day}T23:59:59Z`) + debugLog('cert', `CA not-after date: ${date.toISOString()} (100 years validity)`) + return date } /** * Create a new Root CA Certificate * @returns The Root CA Certificate */ -export async function createRootCA(): Promise { +export async function createRootCA(options?: TlsOption): Promise { + debugLog('ca', 'Creating new Root CA Certificate') + debugLog('ca', 'Generating 2048-bit RSA key pair') const { privateKey, publicKey } = pki.rsa.generateKeyPair(2048) + const mergedOptions = { + ...config, + ...(options || {}), + } + + debugLog('ca', 'Setting certificate attributes') const attributes = [ - { shortName: 'C', value: config.countryName }, - { shortName: 'ST', value: config.stateName }, - { shortName: 'L', value: config.localityName }, + { shortName: 'C', value: mergedOptions.countryName }, + { shortName: 'ST', value: mergedOptions.stateName }, + { shortName: 'L', value: mergedOptions.localityName }, { shortName: 'O', value: 'Local Development CA' }, { shortName: 'CN', value: 'Local Development Root CA' }, ] + debugLog('ca', 'Setting certificate extensions') const extensions = [ { name: 'basicConstraints', @@ -93,6 +112,7 @@ export async function createRootCA(): Promise { }, ] + debugLog('ca', 'Creating CA certificate') const caCert = pki.createCertificate() caCert.publicKey = publicKey caCert.serialNumber = randomSerialNumber() @@ -102,12 +122,13 @@ export async function createRootCA(): Promise { caCert.setIssuer(attributes) caCert.setExtensions(extensions) - // Sign with SHA-256 for better compatibility + debugLog('ca', 'Signing certificate with SHA-256') caCert.sign(privateKey, forge.md.sha256.create()) const pemCert = pki.certificateToPem(caCert) const pemKey = pki.privateKeyToPem(privateKey) + debugLog('ca', 'Root CA Certificate created successfully') return { certificate: pemCert, privateKey: pemKey, @@ -122,32 +143,39 @@ export async function createRootCA(): Promise { * @returns The generated certificate */ export async function generateCert(options?: CertOption): Promise { - log.debug('generateCert', options) + debugLog('cert', 'Generating new host certificate') + debugLog('cert', `Options: ${JSON.stringify(options)}`) - if (!options?.hostCertCN?.trim()) + if (!options?.hostCertCN?.trim()) { + debugLog('cert', 'Error: hostCertCN is required') throw new Error('"hostCertCN" must be a String') - if (!options.domain?.trim()) + } + if (!options.domain?.trim()) { + debugLog('cert', 'Error: domain is required') throw new Error('"domain" must be a String') - if (!options.rootCAObject || !options.rootCAObject.certificate || !options.rootCAObject.privateKey) + } + if (!options.rootCAObject || !options.rootCAObject.certificate || !options.rootCAObject.privateKey) { + debugLog('cert', 'Error: rootCAObject is invalid or missing') throw new Error('"rootCAObject" must be an Object with the properties "certificate" & "privateKey"') + } - // Convert the Root CA PEM details to forge Objects + debugLog('cert', 'Converting Root CA PEM to forge objects') const caCert = pki.certificateFromPem(options.rootCAObject.certificate) const caKey = pki.privateKeyFromPem(options.rootCAObject.privateKey) - // Create a new Keypair for the Host Certificate + debugLog('cert', 'Generating 2048-bit RSA key pair for host certificate') const hostKeys = pki.rsa.generateKeyPair(2048) - // Define the attributes for the Host Certificate + debugLog('cert', 'Setting certificate attributes') const attributes = [ { shortName: 'C', value: config.countryName }, { shortName: 'ST', value: config.stateName }, { shortName: 'L', value: config.localityName }, { shortName: 'O', value: 'Local Development' }, - { shortName: 'CN', value: '*.localhost' }, // Changed to wildcard localhost + { shortName: 'CN', value: '*.localhost' }, ] - // Enhanced extensions for local development + debugLog('cert', 'Setting certificate extensions') const extensions = [ { name: 'basicConstraints', @@ -168,19 +196,19 @@ export async function generateCert(options?: CertOption): Promise { + debugLog('trust', 'Adding certificate to system trust store') + + debugLog('trust', 'Storing certificate and private key') const certPath = storeCert(cert, options) + + debugLog('trust', 'Storing CA certificate') const caCertPath = storeCACert(caCert, options) const platform = os.platform() + debugLog('trust', `Detected platform: ${platform}`) const args = 'TC, C, C' if (platform === 'darwin') { - // macOS + debugLog('trust', 'Adding certificate to macOS keychain') await runCommand( `sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ${caCertPath}`, ) } else if (platform === 'win32') { - // Windows + debugLog('trust', 'Adding certificate to Windows certificate store') await runCommand(`certutil -f -v -addstore -enterprise Root ${caCertPath}`) } else if (platform === 'linux') { - // Linux (This might vary based on the distro) - // for Ubuntu/Debian based systems + debugLog('trust', 'Adding certificate to Linux certificate store') const rootDirectory = os.homedir() const targetFileName = 'cert9.db' + debugLog('trust', `Searching for certificate databases in ${rootDirectory}`) const foldersWithFile = findFoldersWithFile(rootDirectory, targetFileName) for (const folder of foldersWithFile) { + debugLog('trust', `Processing certificate database in ${folder}`) try { - // delete existing cert from system trust store + debugLog('trust', `Attempting to delete existing cert for ${config.commonName}`) await runCommand(`certutil -d sql:${folder} -D -n ${config.commonName}`) } catch (error) { - // ignore error if no cert exists + debugLog('trust', `Warning: Error deleting existing cert: ${error}`) console.warn(`Error deleting existing cert: ${error}`) } - // add new cert to system trust store + debugLog('trust', `Adding new certificate to ${folder}`) await runCommand(`certutil -d sql:${folder} -A -t ${args} -n ${config.commonName} -i ${caCertPath}`) log.info(`Cert added to ${folder}`) } } else { + debugLog('trust', `Error: Unsupported platform ${platform}`) throw new Error(`Unsupported platform: ${platform}`) } + debugLog('trust', 'Certificate successfully added to system trust store') return certPath } export function storeCert(cert: Cert, options?: AddCertOption): string { - // Construct the path using os.homedir() and path.join() + debugLog('storage', 'Storing certificate and private key') const certPath = options?.customCertPath || config.certPath const certKeyPath = options?.customCertPath || config.keyPath + debugLog('storage', `Certificate path: ${certPath}`) + debugLog('storage', `Private key path: ${certKeyPath}`) + // Ensure the directory exists before writing the file const certDir = path.dirname(certPath) - if (!fs.existsSync(certDir)) + if (!fs.existsSync(certDir)) { + debugLog('storage', `Creating certificate directory: ${certDir}`) fs.mkdirSync(certDir, { recursive: true }) + } + debugLog('storage', 'Writing certificate file') fs.writeFileSync(certPath, cert.certificate) // Ensure the directory exists before writing the file const certKeyDir = path.dirname(certKeyPath) - if (!fs.existsSync(certKeyDir)) + if (!fs.existsSync(certKeyDir)) { + debugLog('storage', `Creating private key directory: ${certKeyDir}`) fs.mkdirSync(certKeyDir, { recursive: true }) + } + debugLog('storage', 'Writing private key file') fs.writeFileSync(certKeyPath, cert.privateKey) + debugLog('storage', 'Certificate and private key stored successfully') return certPath } @@ -286,16 +334,22 @@ export function storeCert(cert: Cert, options?: AddCertOption): string { * @returns The path to the CA Certificate */ export function storeCACert(caCert: string, options?: AddCertOption): string { - // Construct the path using os.homedir() and path.join() + debugLog('storage', 'Storing CA certificate') const caCertPath = options?.customCertPath || config.caCertPath + debugLog('storage', `CA certificate path: ${caCertPath}`) + // Ensure the directory exists before writing the file const caCertDir = path.dirname(caCertPath) - if (!fs.existsSync(caCertDir)) + if (!fs.existsSync(caCertDir)) { + debugLog('storage', `Creating CA certificate directory: ${caCertDir}`) fs.mkdirSync(caCertDir, { recursive: true }) + } + debugLog('storage', 'Writing CA certificate file') fs.writeFileSync(caCertPath, caCert) + debugLog('storage', 'CA certificate stored successfully') return caCertPath } diff --git a/src/utils.ts b/src/utils.ts index f083d85..fda0958 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { pki } from 'node-forge' +import { config } from './config' /** * Checks if a certificate is valid for a given domain. @@ -171,3 +172,10 @@ export function findFoldersWithFile(rootDir: string, fileName: string): string[] search(rootDir) return result } + +export function debugLog(category: string, message: string): void { + if (config.verbose) { + // eslint-disable-next-line no-console + console.debug(`[rpx:${category}] ${message}`) + } +}