signing-enclave
Version:
A code signing and notarization tool for macOS and Windows applications. Uses Azure Key Vault for secret and certificate management.
319 lines • 13 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.handleNewFile = exports.notarizationTask = exports.codeSigningTask = exports.updateReceipt = exports.createReceipt = void 0;
exports.getCertTask = getCertTask;
exports.getSecretTask = getSecretTask;
exports.getNotarizationCredentialsTask = getNotarizationCredentialsTask;
exports.doWatchFolder = doWatchFolder;
const path_1 = __importDefault(require("path"));
const crypto_1 = require("crypto");
const os_1 = __importDefault(require("os"));
const utils_1 = require("./utils");
const promises_1 = __importStar(require("fs/promises"));
const macOSSigning_1 = require("./signing/macOSSigning");
const macOSNotarization_1 = require("./signing/macOSNotarization");
const windowsSigning_1 = require("./signing/windowsSigning");
const secretClient_1 = require("./signing/secretClient");
const chokidar_1 = __importDefault(require("chokidar"));
const events_1 = require("events");
async function readRequest(filePath) {
return (0, utils_1.readJson)(filePath);
}
async function readReceipt(filePath) {
return (0, utils_1.readJson)(filePath);
}
async function writeReceipt(filePath, receipt) {
return (0, utils_1.writeJson)(filePath, receipt);
}
let watchFolder;
let options;
const createReceipt = async (request, requestPath,
// for tests
generateUUID = crypto_1.randomUUID, writeReceiptFn = writeReceipt) => {
const receipt = {
id: generateUUID().slice(-6),
originalFile: request.path,
receiptFile: path_1.default.basename(requestPath.replace(".request.json", ".receipt.json")),
requestFile: path_1.default.basename(requestPath),
platform: request.platform,
type: request.type,
provisioningProfile: request.provisioningProfile,
status: "Pending",
secretName: request.secretName,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await writeReceiptFn(getFullPath(receipt.receiptFile), receipt);
console.log(`Receipt created: ${receipt.receiptFile}`);
return receipt;
};
exports.createReceipt = createReceipt;
const updateReceipt = async (receipt, updates) => {
try {
const updatedReceipt = {
...receipt,
...updates,
updatedAt: new Date().toISOString(),
};
await writeReceipt(getFullPath(receipt.receiptFile), updatedReceipt);
console.log(`Receipt updated: ${receipt.receiptFile} | Status: ${updatedReceipt.status}`);
return updatedReceipt;
}
catch (err) {
console.error(`Failed to update receipt: ${receipt.receiptFile}`, err);
throw err;
}
};
exports.updateReceipt = updateReceipt;
const codeSigningTask = async (receipt) => {
const fileToSign = getFullPath(receipt.originalFile);
const platform = receipt.platform;
console.log(`Starting code signing for: ${path_1.default.basename(fileToSign)}`);
if (platform === "mac" && options.macCert) {
return (0, macOSSigning_1.macOsSign)(fileToSign, options.macCert);
}
else if (platform === "mas") {
return (0, macOSSigning_1.macOsSign)(fileToSign, "todesktop-2108257l5oobyoe-mac-cert");
}
else if (platform === "macPkg") {
return (0, macOSSigning_1.macOsSign)(fileToSign, "todesktop-2108257l5oobyoe-mac-cert");
}
else if (platform === "win" && options.windowsCert) {
return (0, windowsSigning_1.windowsSign)(fileToSign, options.windowsCert);
}
};
exports.codeSigningTask = codeSigningTask;
const notarizationTask = async (receipt) => {
const fileToNotarize = getFullPath(receipt.originalFile);
const platform = receipt.platform;
if (platform === "mac" && options.macNotarize) {
console.log(`Notarizing: ${JSON.stringify(options.macNotarize)}, ${fileToNotarize}`);
return (0, macOSNotarization_1.notarizeWithTimeout)(options.macNotarize,
// or
// {
// $appleApiKey: "todesktop-2108257l5oobyoe-mac-api-key",
// appleApiKeyId: "T9GPZ92M7K",
// appleApiIssuer: "c055ca8c-e5a8-4836-b61d-aa5794eeb3f4",
// }
fileToNotarize);
}
};
exports.notarizationTask = notarizationTask;
function getCertName(receipt) {
if (receipt.platform === "mac") {
if (!options.macCert) {
throw new Error("No mac certificate found");
}
return options.macCert;
}
else if (receipt.platform === "mas" && receipt.type === "distribution") {
if (!options.masDistributionCert) {
throw new Error("No mas distribution certificate found");
}
return options.masDistributionCert;
}
else if (receipt.platform === "mas" && receipt.type === "development") {
if (!options.masInstallerCert) {
throw new Error("No mas installer certificate found");
}
return options.masInstallerCert;
}
else if (receipt.platform === "mas" && receipt.type === "installer") {
if (!options.masInstallerCert) {
throw new Error("No mas installer certificate found");
}
return options.masInstallerCert;
}
else if (receipt.platform === "macPkg") {
if (!options.macInstallerCert) {
throw new Error("No mac installer certificate found");
}
return options.macInstallerCert;
}
throw new Error(`Unsupported platform: ${receipt.platform}`);
}
async function getCertTask(receipt) {
const certPath = getFullPath(receipt.originalFile);
return (0, secretClient_1.downloadCertificateFile)(getCertName(receipt), {
filePath: certPath,
});
}
async function getSecretTask(receipt) {
const { secretName } = receipt;
if (!secretName) {
throw new Error("No secret name found");
}
const secretPath = getFullPath(receipt.originalFile);
if (!Array.isArray(options.secrets)) {
throw new Error("No secrets found");
}
const matchedSecret = options.secrets.find((obj) => obj[secretName])?.[secretName];
if (!matchedSecret) {
throw new Error(`No secret found for: ${receipt.secretName}`);
}
await (0, promises_1.writeFile)(secretPath, await (0, secretClient_1.getSecret)(matchedSecret));
return {
secretFilePath: secretPath,
cleanup: () => promises_1.default.unlink(secretPath),
};
}
async function getNotarizationCredentialsTask(receipt) {
if (!options.macNotarize) {
throw new Error("No notarization details found");
}
const secretPath = getFullPath(receipt.originalFile);
const credentials = await (0, macOSNotarization_1.getNotarizationSecretsFromHSM)(options.macNotarize);
await (0, promises_1.writeFile)(secretPath, JSON.stringify(credentials));
return {
secretFilePath: secretPath,
cleanup: () => promises_1.default.unlink(secretPath),
};
}
function getFullPath(filePath) {
return path_1.default.join(watchFolder, filePath);
}
const handleNewFile = async (requestPath) => {
if (!requestPath.endsWith("request.json"))
return;
const request = await readRequest(requestPath);
console.log(`New request detected: ${path_1.default.basename(requestPath)}`);
if (request.actions.length === 0) {
console.log(`No actions found for: ${path_1.default.basename(requestPath)}`);
return;
}
const filePath = path_1.default.join(path_1.default.dirname(requestPath), request.path);
let receipt = await (0, exports.createReceipt)(request, requestPath);
try {
if (request.actions.includes("getSecret")) {
receipt = await (0, exports.updateReceipt)(receipt, {
status: "In Progress",
});
await getSecretTask(receipt);
}
if (request.actions.includes("getCert")) {
receipt = await (0, exports.updateReceipt)(receipt, {
status: "In Progress",
});
await getCertTask(receipt);
}
if (request.actions.includes("sign")) {
receipt = await (0, exports.updateReceipt)(receipt, {
status: "Signing",
signingStatus: "In Progress",
});
await (0, utils_1.ensureExists)(filePath);
await (0, exports.codeSigningTask)(receipt);
receipt = await (0, exports.updateReceipt)(receipt, { signingStatus: "Completed" });
}
if (request.actions.includes("notarize")) {
console.log(`Starting notarization for: ${path_1.default.basename(filePath)}`);
if (os_1.default.platform() === "darwin") {
receipt = await (0, exports.updateReceipt)(receipt, {
status: "Notarizing",
notarizationStatus: "In Progress",
});
await (0, utils_1.ensureExists)(filePath);
await (0, exports.notarizationTask)(receipt);
receipt = await (0, exports.updateReceipt)(receipt, {
notarizationStatus: "Completed",
status: "Completed",
});
}
else {
throw new Error("Notarization is only supported on macOS");
}
}
if (request.actions.includes("getNotarizationCredentials")) {
receipt = await (0, exports.updateReceipt)(receipt, {
status: "In Progress",
});
await getNotarizationCredentialsTask(receipt);
}
receipt = await (0, exports.updateReceipt)(receipt, { status: "Completed" });
console.log(`Processing completed for: ${path_1.default.basename(filePath)}`);
}
catch (err) {
// On failure, update receipt with detailed error messages
const updateData = { status: "Failed" };
if (os_1.default.platform() === "darwin") {
if (err.message.includes("signing")) {
updateData.signingStatus = "Failed";
updateData.signingError = err.message;
}
else if (err.message.includes("notarization")) {
updateData.notarizationStatus = "Failed";
updateData.notarizationError = err.message;
}
}
else {
updateData.signingStatus = "Failed";
updateData.signingError = err.message;
}
updateData.error = err.message; // General error field
receipt = await (0, exports.updateReceipt)(receipt, updateData);
}
};
exports.handleNewFile = handleNewFile;
async function processFile(filePath) {
if (!filePath.endsWith("request.json"))
return;
await (0, exports.handleNewFile)(filePath);
}
async function doWatchFolder(_watchFolder, _options) {
options = _options;
watchFolder = _watchFolder;
const eventEmitter = new events_1.EventEmitter();
const watcher = chokidar_1.default.watch(watchFolder, {
persistent: true,
ignoreInitial: true,
ignored: (path, stats) => {
if (stats === undefined) {
return false;
}
else if (stats.isDirectory()) {
return false;
}
else {
return !path.endsWith(".request.json");
}
},
});
console.log(`Watching for new files in: ${watchFolder}`);
watcher.on("add", async (filePath) => {
console.log(`New file detected: ${path_1.default.basename(filePath)}`);
eventEmitter.emit("processing", `Processing: ${path_1.default.basename(filePath)}`);
await processFile(filePath);
eventEmitter.emit("completed", `Completed: ${path_1.default.basename(filePath)}`);
});
eventEmitter.on("close", async () => {
await watcher.close();
});
return eventEmitter;
}
//# sourceMappingURL=watcher.js.map