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
JavaScript
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
};