Files
tutti/tools/scripts/windows/certum-sign-windows.cjs
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

242 lines
6.9 KiB
JavaScript

const { X509Certificate } = require("node:crypto");
const {
existsSync,
mkdtempSync,
readFileSync,
readdirSync,
rmSync,
statSync,
writeFileSync
} = require("node:fs");
const { tmpdir } = require("node:os");
const { basename, extname, join, resolve } = require("node:path");
const { spawnSync } = require("node:child_process");
const DEFAULT_TIMESTAMP_URL = "http://time.certum.pl";
const DEFAULT_DISPLAY_NAME = "Tutti";
const DEFAULT_DISPLAY_URL = "https://github.com/tutti-os/tutti";
function requiredEnv(name) {
const value = process.env[name]?.trim();
if (!value) throw new Error(`${name} is required for Certum signing`);
return value;
}
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
...options
});
if (result.error) throw result.error;
if (result.status !== 0) {
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
throw new Error(`${command} failed with exit code ${result.status}`);
}
return result;
}
function sleep(seconds) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, seconds * 1000);
}
function runWithRetries(command, args, { attempts, delaySeconds, label }) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
try {
return run(command, args);
} catch (error) {
lastError = error;
if (attempt === attempts) break;
process.stderr.write(
`${label} failed (attempt ${attempt}/${attempts}); retrying in ${delaySeconds}s.\n`
);
sleep(delaySeconds);
}
}
throw lastError;
}
function normalizedFingerprint(value) {
return value.replace(/[^0-9a-f]/giu, "").toUpperCase();
}
function verifySignature(filePath) {
const verification = run(
"osslsigncode",
[
"verify",
"-CAfile",
"/etc/ssl/certs/ca-certificates.crt",
"-TSA-CAfile",
"/etc/ssl/certs/ca-certificates.crt",
"-in",
filePath
],
{ stdio: ["ignore", "pipe", "pipe"] }
);
const output = `${verification.stdout}\n${verification.stderr}`;
if (
!output.includes("Signature verification: ok") ||
!output.includes("Timestamp Server Signature verification: ok")
) {
throw new Error(
`Authenticode or timestamp verification failed for ${filePath}`
);
}
const expected = normalizedFingerprint(
requiredEnv("CERTUM_CERT_FINGERPRINT")
);
if (!/^[0-9A-F]{64}$/u.test(expected)) {
throw new Error(
"CERTUM_CERT_FINGERPRINT must contain 64 hexadecimal characters"
);
}
const workDir = mkdtempSync(join(tmpdir(), "tutti-certum-signature-"));
try {
const signaturePath = join(workDir, "signature.der");
const certificatesPath = join(workDir, "certificates.pem");
runWithRetries(
"osslsigncode",
["extract-signature", "-in", filePath, "-out", signaturePath],
{
attempts: 3,
delaySeconds: 2,
label: `Extracting Authenticode signature from ${filePath}`
}
);
const certificates = run("openssl", [
"pkcs7",
"-inform",
"DER",
"-in",
signaturePath,
"-print_certs"
]);
writeFileSync(certificatesPath, certificates.stdout);
const blocks =
readFileSync(certificatesPath, "utf8").match(
/-----BEGIN CERTIFICATE-----[\s\S]*?-----END CERTIFICATE-----/gu
) ?? [];
const fingerprints = blocks.map((block) =>
normalizedFingerprint(new X509Certificate(block).fingerprint256)
);
if (!fingerprints.includes(expected))
throw new Error(`Pinned Certum certificate was not found in ${filePath}`);
} finally {
rmSync(workDir, { recursive: true, force: true });
}
}
function signFile(filePath, metadata = {}) {
const jsignJar = requiredEnv("JSIGN_JAR");
const pkcs11Config = requiredEnv("TSH_CERTUM_PKCS11_CONFIG");
const timestampUrl =
process.env.TSH_CERTUM_TIMESTAMP_URL?.trim() || DEFAULT_TIMESTAMP_URL;
const maxAttempts = Number.parseInt(
process.env.TSH_CERTUM_SIGN_MAX_ATTEMPTS || "3",
10
);
if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
throw new Error("TSH_CERTUM_SIGN_MAX_ATTEMPTS must be a positive integer");
}
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
process.stdout.write(
`Signing ${basename(filePath)} (attempt ${attempt}/${maxAttempts}).\n`
);
const result = spawnSync(
"java",
[
"-jar",
jsignJar,
"--storetype",
"PKCS11",
"--keystore",
pkcs11Config,
"--storepass",
"",
"--alg",
"SHA-256",
"--tsmode",
"RFC3161",
"--tsaurl",
timestampUrl,
"--replace",
"--name",
metadata.name || DEFAULT_DISPLAY_NAME,
"--url",
metadata.site || DEFAULT_DISPLAY_URL,
filePath
],
{ stdio: "inherit" }
);
if (result.status === 0) {
verifySignature(filePath);
return;
}
if (attempt === maxAttempts)
throw new Error(
`jsign failed for ${filePath} after ${maxAttempts} attempts`
);
sleep(10);
}
}
function findExecutables(root) {
const files = [];
for (const entry of readdirSync(root)) {
const path = join(root, entry);
const stat = statSync(path);
if (stat.isDirectory()) files.push(...findExecutables(path));
else if (stat.isFile() && extname(entry).toLowerCase() === ".exe")
files.push(path);
}
return files.sort((left, right) => left.localeCompare(right));
}
async function electronBuilderSign(configuration) {
if (configuration.hash !== "sha256")
throw new Error(
`Certum signer only supports sha256, received ${configuration.hash}`
);
signFile(resolve(configuration.path), {
name: configuration.name,
site: configuration.site
});
}
async function main(argv) {
if (argv.length !== 2 || !["--root", "--verify-root"].includes(argv[0])) {
throw new Error(
"Usage: node certum-sign-windows.cjs <--root|--verify-root> <directory>"
);
}
const root = resolve(argv[1]);
if (!existsSync(root) || !statSync(root).isDirectory())
throw new Error(`Signing root is not a directory: ${root}`);
const executables = findExecutables(root);
if (executables.length === 0)
throw new Error(`No Windows executables found under ${root}`);
const verifyOnly = argv[0] === "--verify-root";
process.stdout.write(
`${verifyOnly ? "Verifying" : "Signing"} ${executables.length} Windows executables.\n`
);
for (const executable of executables) {
if (verifyOnly) verifySignature(executable);
else signFile(executable);
}
}
exports.default = electronBuilderSign;
exports.findExecutables = findExecutables;
exports.verifySignature = verifySignature;
if (require.main === module) {
main(process.argv.slice(2)).catch((error) => {
process.stderr.write(`${error.stack || error.message}\n`);
process.exitCode = 1;
});
}