@interopio/desktop-cli
Version:
CLI tool for setting up, building and packaging io.Connect Desktop projects
394 lines • 16 kB
JavaScript
;
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