UNPKG

node-msix-packager

Version:

A utility to create MSIX packages from Node.js applications with MCP server support, Node.js Single Executable Application (SEA) bundling using @vercel/ncc and postject, and enhanced build options

283 lines (244 loc) 9.96 kB
const fs = require('fs-extra'); const path = require('path'); const { executeCommand } = require('./utils'); const { CONSTANTS, CertificateError, MSIXError } = require('./constants'); /** * Finds code signing certificates in the certificate store * @returns {Array} Array of certificate objects */ async function findCodeSigningCertificates() { try { console.log('Searching for code signing certificates...'); const psScript = ` $results = @() function Has-CodeSigning($cert) { foreach($ext in $cert.Extensions) { if($ext.Oid.Value -eq '2.5.29.37') { $ekuText = $ext.Format($true) if($ekuText -match 'Code Signing') { return $true } } } return $false } function Check-Store($storeLocation, $storeName) { try { $store = New-Object System.Security.Cryptography.X509Certificates.X509Store([System.Security.Cryptography.X509Certificates.StoreName]::$storeName, [System.Security.Cryptography.X509Certificates.StoreLocation]::$storeLocation) $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly) foreach($cert in $store.Certificates) { if($cert.HasPrivateKey -and (Has-CodeSigning $cert)) { $script:results += @{ Subject = $cert.Subject Thumbprint = $cert.Thumbprint NotBefore = $cert.NotBefore.ToString('yyyy-MM-ddTHH:mm:ssZ') NotAfter = $cert.NotAfter.ToString('yyyy-MM-ddTHH:mm:ssZ') Store = $storeLocation } } } $store.Close() } catch { # Continue to next store } } Check-Store 'CurrentUser' 'My' Check-Store 'LocalMachine' 'My' if($results.Count -eq 0) { Write-Output '[]' } else { $results | ConvertTo-Json -Depth 2 } `; let result = ''; try { const tempScript = path.join(process.env.TEMP || '/tmp', 'find-certs.ps1'); await fs.writeFile(tempScript, psScript); result = executeCommand(`powershell -ExecutionPolicy Bypass -File "${tempScript}"`, { silent: true }); await fs.unlink(tempScript).catch(() => {}); // Ignore cleanup errors } catch (error) { console.warn(`Certificate search failed: ${error.message}`); return []; } const certificates = []; if (result && result.trim() && result.trim() !== '[]') { try { const parsed = JSON.parse(result); const certsArray = Array.isArray(parsed) ? parsed : [parsed]; for (const cert of certsArray) { if (cert?.Subject && cert?.Thumbprint) { certificates.push({ subject: cert.Subject, thumbprint: cert.Thumbprint.replace(/\s/g, ''), store: cert.Store || 'CurrentUser', notBefore: new Date(cert.NotBefore), notAfter: new Date(cert.NotAfter), isExpired: new Date(cert.NotAfter) < new Date(), isValid: new Date(cert.NotBefore) <= new Date() && new Date(cert.NotAfter) >= new Date() }); } } } catch (parseError) { console.warn(`Could not parse certificate results: ${parseError.message}`); } } console.log(`Found ${certificates.length} code signing certificate(s)`); return certificates; } catch (error) { console.warn(`Certificate search error: ${error.message}`); return []; // Return empty array instead of throwing } } /** * Selects the best certificate from available certificates * @param {Array} certificates - Array of available certificates * @param {Object} preferences - Certificate selection preferences * @returns {Object} Selected certificate */ function selectBestCertificate(certificates, preferences = {}) { if (certificates.length === 0) { throw new CertificateError('No code signing certificates found'); } const validCertificates = certificates.filter(cert => cert.isValid); if (validCertificates.length === 0) { throw new CertificateError('No valid (non-expired) code signing certificates found'); } // If thumbprint is specified, try to find matching certificate if (preferences.thumbprint) { const matchingCert = validCertificates.find(cert => cert.thumbprint.replace(/\s/g, '').toLowerCase() === preferences.thumbprint.replace(/\s/g, '').toLowerCase() ); if (!matchingCert) { throw new CertificateError(`Certificate with thumbprint ${preferences.thumbprint} not found`); } return matchingCert; } // If subject is specified, try to find matching certificate if (preferences.subject) { const matchingCert = validCertificates.find(cert => cert.subject.toLowerCase().includes(preferences.subject.toLowerCase()) ); if (!matchingCert) { throw new CertificateError(`Certificate with subject containing "${preferences.subject}" not found`); } return matchingCert; } // Select the certificate that expires latest return validCertificates.reduce((best, current) => current.notAfter > best.notAfter ? current : best ); } /** * Gets certificate information from PFX file and validates it * @param {string} pfxPath - Path to PFX file * @param {string} password - PFX password * @param {boolean} validateOnly - If true, only validates without returning full info * @returns {Object|boolean} Certificate information or validation result */ async function getPfxCertificateInfo(pfxPath, password = '', validateOnly = false) { try { if (!(await fs.pathExists(pfxPath))) { throw new CertificateError(`PFX file not found: ${pfxPath}`); } const psCommand = ` try { $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("${pfxPath}", "${password}") if (-not ($cert.HasPrivateKey -and $cert.Subject)) { throw "Invalid certificate" } ${validateOnly ? 'Write-Output "Valid"' : `$info = @{ Subject = $cert.Subject Thumbprint = $cert.Thumbprint NotBefore = $cert.NotBefore.ToString('yyyy-MM-ddTHH:mm:ss') NotAfter = $cert.NotAfter.ToString('yyyy-MM-ddTHH:mm:ss') } $info | ConvertTo-Json -Depth 2` } } catch { ${validateOnly ? 'Write-Output "Invalid"' : 'Write-Error "Failed to read certificate: $_"; exit 1'} } `; const output = executeCommand(`powershell -ExecutionPolicy Bypass -Command "${psCommand}"`, { silent: true }); if (validateOnly) { if (output.trim() !== "Valid") { throw new CertificateError(`Invalid PFX file or password`); } return true; } const certInfo = JSON.parse(output); return { subject: certInfo.Subject || 'Unknown', thumbprint: (certInfo.Thumbprint || '').replace(/\s/g, ''), notBefore: certInfo.NotBefore ? new Date(certInfo.NotBefore) : null, notAfter: certInfo.NotAfter ? new Date(certInfo.NotAfter) : null, source: 'PFX File', path: pfxPath, isExpired: certInfo.NotAfter ? new Date(certInfo.NotAfter) < new Date() : true, isValid: certInfo.NotBefore && certInfo.NotAfter ? (new Date(certInfo.NotBefore) <= new Date() && new Date(certInfo.NotAfter) >= new Date()) : false }; } catch (error) { if (error instanceof CertificateError) { throw error; } throw new CertificateError(`Failed to ${validateOnly ? 'validate' : 'get info from'} PFX file: ${error.message}`); } } /** * Validates that a PFX file exists and can be used for signing * @param {string} pfxPath - Path to PFX file * @param {string} password - PFX password * @returns {boolean} True if PFX is valid */ async function validatePfxFile(pfxPath, password = '') { return await getPfxCertificateInfo(pfxPath, password, true); } /** * Determines the best signing method based on available certificates and configuration * @param {Object} signingConfig - Signing configuration * @returns {Object} Signing method and parameters */ async function determineSigningMethod(signingConfig) { if (!signingConfig.sign) { return null; } // Priority 1: Use PFX file if specified if (signingConfig.certificatePath) { if (!(await fs.pathExists(signingConfig.certificatePath))) { throw new CertificateError(`Certificate file not found: ${signingConfig.certificatePath}`); } await validatePfxFile(signingConfig.certificatePath, signingConfig.certificatePassword || ''); return { method: 'pfx', parameters: { pfxPath: signingConfig.certificatePath, password: signingConfig.certificatePassword || '', timestampUrl: signingConfig.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL } }; } // Priority 2: Use certificate store const certificates = await findCodeSigningCertificates(); const selectedCert = selectBestCertificate(certificates, { thumbprint: signingConfig.certificateThumbprint, subject: signingConfig.certificateSubject }); return { method: 'store', parameters: { thumbprint: selectedCert.thumbprint, store: selectedCert.store, timestampUrl: signingConfig.timestampUrl || CONSTANTS.DEFAULT_TIMESTAMP_URL } }; } module.exports = { findCodeSigningCertificates, selectBestCertificate, validatePfxFile, getPfxCertificateInfo, determineSigningMethod };