UNPKG

@interopio/desktop-cli

Version:

CLI tool for setting up, building and packaging io.Connect Desktop projects

394 lines 16 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.signBinary = signBinary; exports.signIOCDBinaries = signIOCDBinaries; exports.signWithSmctl = signWithSmctl; exports.detectSigningCapabilities = detectSigningCapabilities; const logger_1 = require("../../utils/logger"); const config_service_1 = require("../config/config.service"); const path_1 = require("../../utils/path"); const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path_2 = __importDefault(require("path")); const logger = logger_1.Logger.getInstance(); const signableExtensions = ['.exe', '.dll', '.ocx', '.sys', '.node']; async function signBinary(binaryPath) { logger.debug(`Signing binary: ${path_2.default.basename(binaryPath)}`); if (!(0, fs_1.existsSync)(binaryPath)) { throw new Error(`Executable not found at path: ${binaryPath}`); } const config = config_service_1.ConfigManager.config; const codeSignConfig = config.win.codeSign; if (codeSignConfig.type === 'off') { logger.info('Code signing is disabled (type: off)'); return; } const ext = path_2.default.extname(binaryPath).toLowerCase(); if (!signableExtensions.includes(ext)) { logger.debug(`File extension ${ext} is not signable. Skipping signing for: ${binaryPath}`); return; } await signBinaryCore(binaryPath, codeSignConfig); await verifySignature(binaryPath); logger.debug(`Successfully signed: ${path_2.default.basename(binaryPath)}`); } async function signIOCDBinaries() { logger.info('Starting signing of all Windows binaries...'); const config = config_service_1.ConfigManager.config; const codeSignConfig = config.win.codeSign; if (codeSignConfig.type === 'off') { logger.info('Code signing is disabled (type: off)'); return; } storeUnsignedExeIfNeeded(); try { const iocdDir = path_1.PathUtils.getIOCDDir(); const binariesToSign = await findSignableBinaries(iocdDir); if (binariesToSign.length === 0) { logger.info('No signable binaries found'); return; } logger.info(`Signing ${binariesToSign.length} binaries...`); for (const binaryPath of binariesToSign) { await signBinaryCore(binaryPath, codeSignConfig); } logger.info('Signing binaries completed successfully!'); } catch (error) { logger.error(`Batch signing failed: ${error instanceof Error ? error.message : String(error)}`); throw error; } } function storeUnsignedExeIfNeeded() { // we store the original unsigned exe, because rcedits can be done on unsigned exe only. // this means that if we do rcedit->sign, we can not longer do rcedit without breaking the signature // to overcome this we are storing the original exe in a _temp folder and restore it before any modifications // see restoreOriginalUnsignedExeIfNeeded in modifications/windows.helper.ts for restore logic const tempDir = path_1.PathUtils.getComponentDir("_temp"); const tempExePath = path_2.default.join(tempDir, path_2.default.basename(path_1.PathUtils.getIOCDExePath())); if (!(0, fs_1.existsSync)(tempDir)) { (0, fs_1.mkdirSync)(tempDir); } if (!(0, fs_1.existsSync)(tempExePath)) { logger.debug('Storing current executable into _temp directory before signing. Will restore before next modifications'); (0, fs_1.copyFileSync)(path_1.PathUtils.getIOCDExePath(), tempExePath); } } async function findSignableBinaries(directory) { const binaries = []; // Common files that should NOT be signed const skipFiles = [ 'msvcp140.dll', 'vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'api-ms-win-*.dll', 'ucrtbase.dll' ]; function scanDirectory(dir) { try { if (!(0, fs_1.existsSync)(dir)) return; const files = (0, fs_1.readdirSync)(dir); for (const file of files) { const fullPath = path_2.default.join(dir, file); const stat = (0, fs_1.statSync)(fullPath); if (stat.isDirectory()) { // Recursively scan subdirectories scanDirectory(fullPath); } else if (stat.isFile()) { const ext = path_2.default.extname(file).toLowerCase(); const basename = path_2.default.basename(file).toLowerCase(); // Check if it's a signable extension if (signableExtensions.includes(ext)) { // Skip system files and runtime libraries const shouldSkip = skipFiles.some(skipPattern => { if (skipPattern.includes('*')) { // Simple wildcard matching const regex = new RegExp('^' + skipPattern.replace(/\*/g, '.*') + '$', 'i'); return regex.test(basename); } return basename === skipPattern.toLowerCase(); }); if (!shouldSkip) { binaries.push(fullPath); } } } } } catch (error) { logger.debug(`Could not scan directory ${dir}: ${error instanceof Error ? error.message : String(error)}`); } } scanDirectory(directory); return binaries; } async function signBinaryCore(binaryPath, codeSignConfig) { switch (codeSignConfig.type) { case "off": // do nothing break; case 'pfx': await signWithPfx(binaryPath, codeSignConfig); break; case 'wincert': await signWithWinCert(binaryPath, codeSignConfig); break; case 'smime': await signWithSmime(binaryPath, codeSignConfig); break; case 'smctl': await signWithSmctl(binaryPath); break; case 'custom': await signWithCustomScript(binaryPath, codeSignConfig); break; default: throw new Error(`Unsupported code signing type: ${codeSignConfig.type}`); } } async function signWithPfx(exePath, codeSignConfig) { logger.debug('Signing with PFX certificate...'); if (!codeSignConfig.pfxPath) { throw new Error('PFX signing requires pfxPath to be configured'); } if (!(0, fs_1.existsSync)(codeSignConfig.pfxPath)) { throw new Error(`PFX file not found: ${codeSignConfig.pfxPath}`); } const args = [ 'sign', // "/debug", '/fd', 'SHA256', '/td', 'SHA256', '/tr', config_service_1.ConfigManager.config.win.codeSign.tsaURL, '/f', codeSignConfig.pfxPath ]; if (codeSignConfig.pfxPassword) { args.push('/p', codeSignConfig.pfxPassword); } args.push(exePath); await runSigntool(args); } async function signWithWinCert(exePath, codeSignConfig) { logger.debug('Signing with Windows Certificate Store...'); if (!codeSignConfig.wincertSubjectName) { throw new Error('Windows Certificate Store signing requires wincertSubjectName to be configured'); } const args = [ 'sign', '/fd', 'SHA256', '/td', 'SHA256', '/tr', config_service_1.ConfigManager.config.win.codeSign.tsaURL, '/n', codeSignConfig.wincertSubjectName, exePath ]; await runSigntool(args); } async function signWithSmime(exePath, codeSignConfig) { logger.debug('Signing with S/MIME certificate...'); if (!codeSignConfig.smimeCertificatePath) { throw new Error('S/MIME signing requires smimeCertificatePath to be configured'); } if (!(0, fs_1.existsSync)(codeSignConfig.smimeCertificatePath)) { throw new Error(`S/MIME certificate file not found: ${codeSignConfig.smimeCertificatePath}`); } const args = [ 'sign', '/fd', 'SHA256', '/td', 'SHA256', '/tr', config_service_1.ConfigManager.config.win.codeSign.tsaURL, '/f', codeSignConfig.smimeCertificatePath ]; if (codeSignConfig.smimeCertificatePassword) { args.push('/p', codeSignConfig.smimeCertificatePassword); } args.push(exePath); await runSigntool(args); } async function signWithSmctl(exePath) { logger.debug('Signing with DigiCert smctl (KeyLocker)...'); const requiredEnvVars = ['SM_API_KEY', 'SM_CLIENT_CERT_FILE', 'SM_CLIENT_CERT_PASSWORD', 'SM_CODE_SIGNING_CERT_SHA1_HASH']; const missingVars = requiredEnvVars.filter(varName => !process.env[varName]); if (missingVars.length > 0) { throw new Error(`DigiCert smctl signing requires the following environment variables: ${missingVars.join(', ')}`); } try { // First, authenticate with DigiCert KeyLocker await runCommand('smctl', ['windows', 'certsync'], { env: { ...process.env, SM_API_KEY: process.env['SM_API_KEY'], SM_CLIENT_CERT_FILE: process.env['SM_CLIENT_CERT_FILE'], SM_CLIENT_CERT_PASSWORD: process.env['SM_CLIENT_CERT_PASSWORD'] } }); // Then sign the binary const args = [ 'sign', '--keypair-alias', process.env['SM_CODE_SIGNING_CERT_SHA1_HASH'], '--certificate', process.env['SM_CLIENT_CERT_FILE'], '--config-file', process.env['SM_CLIENT_CERT_FILE'], '--input', exePath ].filter((arg) => arg !== undefined); await runCommand('smctl', args, { env: { ...process.env, SM_API_KEY: process.env['SM_API_KEY'], SM_CLIENT_CERT_FILE: process.env['SM_CLIENT_CERT_FILE'], SM_CLIENT_CERT_PASSWORD: process.env['SM_CLIENT_CERT_PASSWORD'] } }); logger.debug('DigiCert smctl signing completed successfully!'); } catch (error) { throw new Error(`DigiCert smctl signing failed: ${error instanceof Error ? error.message : String(error)}`); } } async function detectSigningCapabilities() { logger.debug('Detecting available code signing tools...'); const result = { hasSigntool: false, hasSmctl: false, sdkPath: undefined, recommendations: [] }; // Check for signtool in PATH try { await runCommand('signtool', ['/?']); result.hasSigntool = true; logger.debug('signtool found in PATH'); } catch (error) { // Try common Windows SDK locations const sdkPaths = [ 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64\\signtool.exe', 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x86\\signtool.exe', 'C:\\Program Files\\Microsoft SDKs\\Windows\\v7.1\\Bin\\signtool.exe', 'C:\\Program Files (x86)\\Microsoft SDKs\\Windows\\v7.1A\\Bin\\signtool.exe' ]; for (const sdkPath of sdkPaths) { if ((0, fs_1.existsSync)(sdkPath)) { result.hasSigntool = true; result.sdkPath = sdkPath; logger.debug(`signtool found at: ${sdkPath}`); break; } } } // Check for DigiCert smctl try { await runCommand('smctl', ['--version']); result.hasSmctl = true; logger.debug('DigiCert smctl found'); } catch (error) { result.recommendations.push('Install DigiCert smctl for cloud-based signing: https://docs.digicert.com/en/software-trust-manager/tools/smctl.html'); } if (!result.hasSigntool) { result.recommendations.push('Install Windows SDK for signtool: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/'); } return result; } async function signWithCustomScript(exePath, codeSignConfig) { logger.debug('Signing with custom script...'); if (!codeSignConfig.customCodeSignScriptPath) { throw new Error('Custom signing requires customCodeSignScriptPath to be configured'); } if (!(0, fs_1.existsSync)(codeSignConfig.customCodeSignScriptPath)) { throw new Error(`Custom signing script not found: ${codeSignConfig.customCodeSignScriptPath}`); } logger.debug(`Loading and executing custom signing script: ${codeSignConfig.customCodeSignScriptPath}`); try { // Resolve the absolute path for the require const scriptPath = path_2.default.resolve(codeSignConfig.customCodeSignScriptPath); // Clear require cache to ensure fresh execution delete require.cache[scriptPath]; // Require the script module const customScript = require(scriptPath); // Check if the module exports a function (default export or named export) let signFunction; if (typeof customScript === 'function') { signFunction = customScript; } else if (typeof customScript.default === 'function') { signFunction = customScript.default; } else if (typeof customScript.sign === 'function') { signFunction = customScript.sign; } else { throw new Error('Custom signing script must export a function (either as default export, named "sign" export, or direct export)'); } // Execute the signing function with the binary path and full config await signFunction(exePath, codeSignConfig); logger.debug('Custom signing script executed successfully'); } catch (error) { if (error instanceof Error) { throw new Error(`Custom signing script failed: ${error.message}`); } else { throw new Error(`Custom signing script failed: ${String(error)}`); } } } async function runSigntool(args) { await runCommand('signtool', args); } async function verifySignature(exePath) { logger.debug(`Verifying signature for: ${exePath}`); try { await runSigntool(['verify', '/pa', '/all', exePath]); logger.debug('Signature verification successful'); } catch (error) { logger.warn(`Signature verification failed! The binary at ${exePath} may not be signed correctly.`); } } async function runCommand(command, args, options = {}) { return new Promise((resolve, reject) => { logger.debug(`Executing: ${command} ${args.join(' ')}`); const process = (0, child_process_1.spawn)(command, args, { stdio: ['ignore', 'pipe', 'pipe'], ...options }); let stdout = ''; let stderr = ''; if (process.stdout) { process.stdout.on('data', (data) => { stdout += data.toString(); }); } if (process.stderr) { process.stderr.on('data', (data) => { stderr += data.toString(); }); } process.on('close', (code) => { if (code === 0) { if (stdout) logger.debug(`Command output: ${stdout.trim()}`); resolve(); } else { const errorMessage = `Command failed with exit code ${code}`; if (stderr) logger.error(`${stderr.trim()}`); if (stdout) logger.debug(`${stdout.trim()}`); reject(new Error(errorMessage)); } }); process.on('error', (error) => { logger.error(`Failed to start command: ${error.message}`); reject(error); }); }); } //# sourceMappingURL=windows.helper.js.map