@rightkit/release 0.6.13 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +49 -1
  2. package/adapter-env.mjs +165 -0
  3. package/android-ndk.mjs +5 -1
  4. package/android-release.mjs +73 -8
  5. package/app-check.mjs +22 -8
  6. package/app-closure.mjs +214 -0
  7. package/assemble-app-command.mjs +24 -7
  8. package/atomic-write.mjs +57 -0
  9. package/bin/rightkit-gate.mjs +1193 -337
  10. package/broker-client.mjs +106 -0
  11. package/broker-exception.mjs +211 -0
  12. package/broker-handshake.mjs +160 -0
  13. package/broker-pins.mjs +27 -8
  14. package/broker-wire.mjs +262 -0
  15. package/build-launch.mjs +364 -12
  16. package/build-ownership.mjs +117 -49
  17. package/build-release.mjs +3 -2
  18. package/cache-command.mjs +7 -2
  19. package/candidate-build-receipt.mjs +96 -0
  20. package/candidate-receipt-command.mjs +29 -0
  21. package/cargo-contract.mjs +4 -3
  22. package/cargo-ledger.mjs +1 -1
  23. package/cargo-license-notices.mjs +2 -2
  24. package/cargo-shapes.mjs +332 -0
  25. package/cargo-target-shim.mjs +44 -5
  26. package/cargo-target.mjs +5 -0
  27. package/carried-adapter-run.mjs +31 -0
  28. package/carried-cargo-run.mjs +9 -0
  29. package/carried-cargo.mjs +237 -0
  30. package/carrier-census.mjs +124 -0
  31. package/class-a-launch-guard.mjs +266 -33
  32. package/cli/right-release.mjs +2 -0
  33. package/clippy-route.mjs +23 -11
  34. package/compile-inputs.mjs +9 -12
  35. package/control-plane.mjs +18 -0
  36. package/deps-cargo-lock.mjs +208 -0
  37. package/deps.mjs +5 -0
  38. package/deterministic-scans.mjs +273 -44
  39. package/development-signing.mjs +14 -1
  40. package/development.mjs +112 -26
  41. package/direct-origin.mjs +107 -0
  42. package/disk-command.mjs +1 -0
  43. package/disk-policy.mjs +18 -3
  44. package/freshness-census.mjs +206 -58
  45. package/gate-executor.mjs +30 -12
  46. package/gate-stages.mjs +93 -16
  47. package/github-release.mjs +2 -1
  48. package/github-upload-gate.mjs +2 -1
  49. package/install-tauri-authenticode.mjs +346 -0
  50. package/ios-dev.mjs +77 -23
  51. package/ios-package-recipe.mjs +100 -41
  52. package/linux-artifacts.mjs +17 -2
  53. package/linux-qualifier.mjs +83 -0
  54. package/local-publish.mjs +36 -14
  55. package/local-runtime.mjs +2 -2
  56. package/lock-identity.mjs +138 -0
  57. package/lsclean.sh +0 -0
  58. package/managed-cargo.mjs +161 -44
  59. package/managed-roots.mjs +130 -12
  60. package/media-sidecar-catalog.mjs +1 -1
  61. package/native-adapters.mjs +411 -0
  62. package/native-helpers.mjs +26 -18
  63. package/native-tool-planning.mjs +94 -0
  64. package/ownership-mark.mjs +182 -0
  65. package/pack-digest.mjs +122 -0
  66. package/package.json +12 -12
  67. package/pipeline-receipts.mjs +302 -302
  68. package/platform-dir.mjs +14 -0
  69. package/pnpm-adapter-grammar.mjs +226 -0
  70. package/progress-control.mjs +7 -2
  71. package/provenance-real-receipt.pins.json +7 -0
  72. package/provenance-verify.mjs +262 -1
  73. package/publication-outcomes.mjs +21 -0
  74. package/publish-update.mjs +2 -1
  75. package/qa-journeys-prebuilt-run.mjs +18 -0
  76. package/qa-journeys-prebuilt.mjs +148 -0
  77. package/qa-snapshot-run.mjs +53 -0
  78. package/qa-snapshot.mjs +164 -0
  79. package/qualification-verify.mjs +144 -0
  80. package/qualify-reuse.mjs +38 -0
  81. package/recipe-build.mjs +16 -3
  82. package/release-qa-audit.mjs +2 -2
  83. package/release-qa-command.mjs +2 -1
  84. package/release-qa-receipt.mjs +3 -2
  85. package/release-state.mjs +3 -2
  86. package/release.mjs +14 -3
  87. package/rightkit-drift.mjs +27 -5
  88. package/rightkit-versions.json +15 -11
  89. package/rustright-names.mjs +32 -0
  90. package/scan-tool-enrolment.mjs +167 -0
  91. package/sign-windows.mjs +212 -112
  92. package/signing-files.mjs +35 -0
  93. package/signing-identity.mjs +7 -0
  94. package/snapshot-broker.mjs +528 -57
  95. package/snapshot-check.mjs +95 -11
  96. package/snapshot-metadata.mjs +123 -0
  97. package/snapshot-request.mjs +389 -96
  98. package/stage-binary.mjs +22 -4
  99. package/stage-commands.mjs +56 -13
  100. package/stage-sidecar.mjs +93 -21
  101. package/suite-signing-policy.mjs +657 -0
  102. package/suite-signing.mjs +223 -0
  103. package/swift-scratch.mjs +25 -0
  104. package/tauri-locked.mjs +25 -0
  105. package/test-command.mjs +164 -29
  106. package/unit-gate.mjs +201 -31
  107. package/upload-large.mjs +0 -0
  108. package/web-build.mjs +2 -2
  109. package/win-package-recipe.mjs +26 -2
  110. package/bin/darwin/rightkit-swap +0 -0
  111. package/bin/darwin/rightkit-swap.sign.json +0 -6
package/sign-windows.mjs CHANGED
@@ -1,10 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ // Windows Authenticode signing through Azure Artifact Signing. Two entries: the CLI (`node sign-windows.mjs [--dry-run|--verify-only] [--receipt <file>] <file>...`,
3
+ // unchanged) and the functions `signWindowsFiles` / `verifyWindowsFiles` that the gate's in-process signing steps reuse (suite-signing.mjs), so there is
4
+ // one signer and one verifier. The identity is the signer SUBJECT (WINDOWS_SIGNER_SUBJECT), never a thumbprint.
2
5
  import { createHash } from "node:crypto";
3
6
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
4
7
  import os from "node:os";
5
8
  import path from "node:path";
6
9
  import { spawnSync } from "./background-process.mjs";
10
+ import { pathToFileURL } from "node:url";
7
11
  import { azureCliCandidates, signingEnv } from "./signing-env.mjs";
12
+ import { WINDOWS_SIGNER_SUBJECT } from "./signing-identity.mjs";
13
+
14
+
15
+ export { WINDOWS_SIGNER_SUBJECT };
8
16
 
9
17
  const EXCLUDE_CREDENTIALS = [
10
18
  "ManagedIdentityCredential",
@@ -15,140 +23,232 @@ const EXCLUDE_CREDENTIALS = [
15
23
  "AzureDeveloperCliCredential",
16
24
  "InteractiveBrowserCredential",
17
25
  ];
18
- const userEnvCache = new Map();
19
-
20
- const args = process.argv.slice(2);
21
- const dryRun = args.includes("--dry-run");
22
- const verifyOnly = args.includes("--verify-only");
23
- const receiptIndex = args.indexOf("--receipt");
24
- const receiptPath = receiptIndex >= 0 ? args[receiptIndex + 1] : null;
25
- const files = args
26
- .filter((arg, index) => arg !== "--dry-run" && arg !== "--verify-only" && arg !== "--receipt" && (receiptIndex < 0 || index !== receiptIndex + 1))
27
- .map((arg) => path.resolve(arg));
28
-
29
- if (!files.length) fail("usage: node sign-windows.mjs [--dry-run|--verify-only] <file>...");
30
- if (receiptIndex >= 0 && !receiptPath) fail("--receipt requires a path");
31
- if (dryRun) {
32
- console.log(`dry-run: ${verifyOnly ? "Authenticode verification" : "Azure Artifact Signing"} ${files.join(", ")}`);
33
- process.exit(0);
34
- }
35
- if (process.platform !== "win32") fail("Windows signing must run on Windows");
36
- for (const file of files) if (!existsSync(file)) fail(`missing file: ${file}`);
37
-
38
- const childEnv = signingEnv(process.env, env);
39
- const signtool = findFirst([env("AZURE_SIGNTOOL_PATH"), env("SIGNTOOL_PATH"), ...signtoolCandidates()]);
40
- if (!signtool) fail("signtool.exe not found; set AZURE_SIGNTOOL_PATH or install Windows SDK Build Tools");
41
- if (verifyOnly) {
42
- for (const file of files) assertVerification(file, run(signtool, ["verify", "/pa", "/v", file]));
43
- process.exit(0);
44
- }
45
- const dlib = findFirst([
46
- env("AZURE_CODESIGN_DLIB_PATH"),
47
- env("AZURE_ARTIFACT_SIGNING_DLIB_PATH"),
48
- path.join(env("LOCALAPPDATA") || "", "AzureArtifactSigningTools", "Microsoft.ArtifactSigning.Client", "bin", "x64", "Azure.CodeSigning.Dlib.dll"),
49
- "C:\\Program Files\\Microsoft Azure Artifact Signing Client Tools\\x64\\Azure.CodeSigning.Dlib.dll",
50
- "C:\\Program Files (x86)\\Microsoft\\ArtifactSigningClientTools\\bin\\x64\\Azure.CodeSigning.Dlib.dll",
51
- ]);
52
- if (!dlib) fail("Azure.CodeSigning.Dlib.dll not found; set AZURE_CODESIGN_DLIB_PATH or install Azure Artifact Signing Client Tools");
53
-
54
- const tempDir = mkdtempSync(path.join(os.tmpdir(), "right-sign-"));
55
- const metadata = metadataPath(tempDir);
56
- const evidence = [];
57
- try {
58
- for (const file of files) {
59
- const before = fileEvidence(file);
60
- run(signtool, ["sign", "/v", "/fd", "SHA256", "/tr", "http://timestamp.acs.microsoft.com", "/td", "SHA256", "/dlib", dlib, "/dmdf", metadata, file]);
61
- const verification = assertVerification(file, run(signtool, ["verify", "/pa", "/v", file]));
62
- evidence.push({ file, before, after: fileEvidence(file), ...verification });
63
- }
64
- } finally {
65
- rmSync(tempDir, { recursive: true, force: true });
66
- }
67
- if (receiptPath) {
68
- const absoluteReceipt = path.resolve(receiptPath);
69
- mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
70
- writeFileSync(absoluteReceipt, `${JSON.stringify({ schema: 1, files: evidence }, null, 2)}\n`);
71
- }
72
26
 
73
- function metadataPath(dir) {
74
- const existing = env("AZURE_ARTIFACT_SIGNING_METADATA") || env("AZURE_SIGNING_METADATA");
75
- if (existing) {
76
- if (!existsSync(existing)) fail(`metadata file not found: ${existing}`);
77
- return existing;
27
+ /** A signing or verification failure. `exitCode` is the failing tool's status when a tool failed (the CLI exits with it), `silent` that the tool already printed. */
28
+ export class WindowsSigningError extends Error {
29
+ constructor(message, { exitCode = 1, silent = false } = {}) {
30
+ super(message);
31
+ this.name = "WindowsSigningError";
32
+ this.code = "WINDOWS_SIGNING_FAILED";
33
+ this.exitCode = exitCode;
34
+ this.silent = silent;
78
35
  }
79
- const Endpoint = env("AZURE_ARTIFACT_SIGNING_ENDPOINT") || env("AZURE_SIGNING_ENDPOINT") || env("AZURE_ENDPOINT");
80
- const CodeSigningAccountName = env("AZURE_ARTIFACT_SIGNING_ACCOUNT") || env("AZURE_SIGNING_ACCOUNT_NAME") || env("AZURE_ACCOUNT");
81
- const CertificateProfileName = env("AZURE_ARTIFACT_SIGNING_PROFILE") || env("AZURE_CERTIFICATE_PROFILE_NAME") || env("AZURE_PROFILE");
82
- if (!Endpoint || !CodeSigningAccountName || !CertificateProfileName) {
83
- fail("set AZURE_ARTIFACT_SIGNING_ENDPOINT, AZURE_ARTIFACT_SIGNING_ACCOUNT, and AZURE_ARTIFACT_SIGNING_PROFILE");
36
+ }
37
+ const fail = (message) => { throw new WindowsSigningError(message); };
38
+
39
+ /** The signer's Azure sign-in is not usable: a typed failure (code SIGN_WINDOWS_TOKEN_UNAVAILABLE), never a process exit. */
40
+ export const TOKEN_UNAVAILABLE_CODE = "SIGN_WINDOWS_TOKEN_UNAVAILABLE";
41
+ export class WindowsSigningTokenError extends WindowsSigningError {
42
+ constructor(detail) {
43
+ super(`${TOKEN_UNAVAILABLE_CODE}: the Azure signer has no silent token (${detail}). Fix: sign in to the Windows desktop session as the signing user (interactive \`az login\` there, so AzureCliCredential finds a token in the MSAL cache); \`az account show\` passing is not enough`);
44
+ this.code = TOKEN_UNAVAILABLE_CODE;
84
45
  }
85
- const file = path.join(dir, "metadata.json");
86
- writeFileSync(file, `${JSON.stringify({ Endpoint, CodeSigningAccountName, CertificateProfileName, ExcludeCredentials: EXCLUDE_CREDENTIALS }, null, 2)}\n`);
87
- return file;
88
46
  }
89
-
90
- function signtoolCandidates() {
91
- return [
92
- ...sdkBuildToolsCandidates(),
47
+ const CODESIGNING_RESOURCE = "https://codesigning.azure.net";
48
+ const EXPIRY = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(\.\d+)?$/;
49
+
50
+ /**
51
+ * The tools and environment one signing session uses. Everything that touches the host is injectable (run, exists, platform, the environment
52
+ * lookup, the user-environment reader), so the sequence below runs with fakes in tests; production passes nothing.
53
+ */
54
+ export function windowsSigningContext({ processEnv = process.env, run = spawnSync, exists = existsSync, platform = process.platform, userEnvReader, quiet = false } = {}) {
55
+ const userEnvCache = new Map();
56
+ const readUserEnv = userEnvReader ?? ((name) => {
57
+ const quoted = name.replace(/'/g, "''");
58
+ const result = run("powershell", ["-NoProfile", "-Command", `[Environment]::GetEnvironmentVariable('${quoted}','User')`], { encoding: "utf8", windowsHide: true });
59
+ return result.status === 0 ? String(result.stdout ?? "").trim() : "";
60
+ });
61
+ const userEnv = (name) => {
62
+ if (!userEnvCache.has(name)) userEnvCache.set(name, readUserEnv(name));
63
+ return userEnvCache.get(name);
64
+ };
65
+ const env = (name) => processEnv[name] || userEnv(name);
66
+ const findFirst = (candidates) => candidates.find((candidate) => candidate && exists(candidate));
67
+ const windowsKitCandidates = (bin) => {
68
+ if (!exists(bin)) return [];
69
+ return readdirSync(bin, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(bin, entry.name, "x64", "signtool.exe")).sort().reverse();
70
+ };
71
+ const signtoolCandidates = () => [
72
+ ...windowsKitCandidates(path.join(env("LOCALAPPDATA") || "", "AzureArtifactSigningTools", "Microsoft.Windows.SDK.BuildTools", "bin")),
93
73
  ...windowsKitCandidates(path.join(env("ProgramFiles(x86)") || "C:\\Program Files (x86)", "Windows Kits", "10", "bin")),
94
74
  ];
75
+ const childEnv = signingEnv(processEnv, env);
76
+ const runTool = (cmd, runArgs) => {
77
+ const result = run(cmd, runArgs, { env: childEnv, encoding: "utf8", windowsHide: true });
78
+ if (!quiet) {
79
+ if (result.stdout) process.stdout.write(result.stdout);
80
+ if (result.stderr) process.stderr.write(result.stderr);
81
+ }
82
+ if (result.status !== 0) throw new WindowsSigningError(`${path.basename(cmd)} ${runArgs[0]} failed (exit ${result.status ?? "unavailable"})${quiet ? `: ${String(result.stderr || result.stdout || result.error?.message || "").trim()}` : ""}`, { exitCode: result.status ?? 1, silent: !quiet });
83
+ return `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
84
+ };
85
+ // The token preflight asks `az` the way the signer's AzureCliCredential does (cmd.exe /d /c az, the signer env) and keeps only the expiry: az is asked for
86
+ // `expiresOn` alone, so no token is printed, and anything token-shaped in an error is redacted. Never throws a process exit; returns { ok, detail }.
87
+ const checkToken = ({ now = Date.now(), minMinutes = 10, timeoutMs = 120000 } = {}) => {
88
+ const command = `az account get-access-token --resource ${CODESIGNING_RESOURCE} --query expiresOn -o tsv`;
89
+ const result = run(process.env.ComSpec || "cmd.exe", ["/d", "/c", command], { env: childEnv, encoding: "utf8", windowsHide: true, timeout: timeoutMs, cwd: env("SystemRoot") ? path.win32.join(env("SystemRoot"), "System32") : undefined });
90
+ const redact = (text) => String(text ?? "").replace(/(eyJ[A-Za-z0-9_-]+\.)+[A-Za-z0-9_-]*/g, "<redacted>").trim();
91
+ if (result?.error) return { ok: false, detail: `az could not be run: ${redact(result.error.message)}` };
92
+ if (result?.status !== 0) return { ok: false, detail: `az account get-access-token exit ${result?.status ?? "unavailable"}: ${redact(result?.stderr) || "no output"}` };
93
+ const stdout = String(result.stdout ?? "").trim();
94
+ if (!EXPIRY.test(stdout)) return { ok: false, detail: `az printed something other than an expiry (not shown, ${stdout.length} chars)` };
95
+ const expires = new Date(stdout.replace(" ", "T")).getTime();
96
+ if (!Number.isFinite(expires) || expires < now + minMinutes * 60000) return { ok: false, detail: `the token expires at ${stdout} (local), under ${minMinutes} minutes from now` };
97
+ return { ok: true, detail: stdout };
98
+ };
99
+ return { env, platform, exists, findFirst, signtoolCandidates, run: runTool, checkToken };
95
100
  }
96
101
 
97
- function sdkBuildToolsCandidates() {
98
- const bin = path.join(env("LOCALAPPDATA") || "", "AzureArtifactSigningTools", "Microsoft.Windows.SDK.BuildTools", "bin");
99
- return windowsKitCandidates(bin);
102
+ /** Before any copy, temp file or output: the signer's Azure CLI sign-in must yield a silent token. */
103
+ export function requireSignerToken(ctx = windowsSigningContext({ quiet: true })) {
104
+ const state = ctx.checkToken();
105
+ if (!state.ok) throw new WindowsSigningTokenError(state.detail);
100
106
  }
101
107
 
102
- function windowsKitCandidates(bin) {
103
- if (!existsSync(bin)) return [];
104
- return readdirSync(bin, { withFileTypes: true })
105
- .filter((entry) => entry.isDirectory())
106
- .map((entry) => path.join(bin, entry.name, "x64", "signtool.exe"))
107
- .sort()
108
- .reverse();
108
+ /**
109
+ * Check one `signtool verify /pa /v` report: valid chain, signer subject, timestamp. Throws through `fail`; returns the evidence fields.
110
+ * Self-contained on purpose (release-cli-contract.test.mjs evaluates this function alone): the subject is WINDOWS_SIGNER_SUBJECT, spelled out.
111
+ */
112
+ function assertVerification(file, output) {
113
+ if (!/^[ \t]*successfully verified\b/im.test(output)) fail(`Authenticode verification did not report Valid for ${file}`);
114
+ if (!/(?:timestamp|time stamped|timestamped)/i.test(output)) fail(`Authenticode timestamp is missing for ${file}`);
115
+ // SignTool prints this chain root first, with each child indented four more spaces.
116
+ // Timestamp/cross-certificate chains are separate sections, never signer evidence.
117
+ const lines = String(output).replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
118
+ const headers = lines.flatMap((line, index) => line.trim() === "Signing Certificate Chain:" ? [index] : []);
119
+ const invalidChain = () => fail(`Authenticode signing certificate chain is missing, malformed or ambiguous for ${file}`);
120
+ if (headers.length !== 1 || lines[headers[0]] !== "Signing Certificate Chain:") invalidChain();
121
+ let depth = 0;
122
+ let leaf = null;
123
+ let ended = false;
124
+ for (const line of lines.slice(headers[0] + 1)) {
125
+ if (!line.trim()) continue;
126
+ if (!/^ /.test(line)) {
127
+ if (!/^(?:The signature is (?:timestamped|time stamped):|Timestamp(?: Verified by:|:)|Successfully verified\b|Cross Certificate Chain:)/i.test(line)) invalidChain();
128
+ ended = true;
129
+ break;
130
+ }
131
+ const subject = /^( +)Issued to:[ ]*(\S(?:.*\S)?)[ ]*$/.exec(line);
132
+ if (subject) {
133
+ if (subject[1].length !== depth + 4) invalidChain();
134
+ depth = subject[1].length;
135
+ leaf = subject[2];
136
+ } else {
137
+ const field = /^( +)(?:Issued by|Expires|SHA1 hash):[ ]*\S.*$/.exec(line);
138
+ if (!field || field[1].length !== depth || depth === 0) invalidChain();
139
+ }
140
+ }
141
+ if (!ended || leaf === null) invalidChain();
142
+ if (leaf !== "Damned Ventures LLC") fail(`Authenticode subject must be Damned Ventures LLC for ${file}`);
143
+ return { authenticode: "Valid", subject: "CN=Damned Ventures LLC", timestampPresent: true };
109
144
  }
110
145
 
111
- function findFirst(candidates) {
112
- return candidates.find((candidate) => candidate && existsSync(candidate));
146
+ function fileEvidence(file) {
147
+ const bytes = readFileSync(file);
148
+ return { sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: statSync(file).size };
113
149
  }
114
150
 
115
- function env(name) {
116
- return process.env[name] || userEnv(name);
151
+ export const assertWindowsVerification = assertVerification;
152
+
153
+ function requireSigntool(ctx) {
154
+ const signtool = ctx.findFirst([ctx.env("AZURE_SIGNTOOL_PATH"), ctx.env("SIGNTOOL_PATH"), ...ctx.signtoolCandidates()]);
155
+ if (!signtool) fail("signtool.exe not found; set AZURE_SIGNTOOL_PATH or install Windows SDK Build Tools");
156
+ return signtool;
117
157
  }
118
158
 
119
- function userEnv(name) {
120
- if (userEnvCache.has(name)) return userEnvCache.get(name);
121
- const result = spawnSync("powershell", [
122
- "-NoProfile",
123
- "-Command",
124
- `[Environment]::GetEnvironmentVariable('${name.replace(/'/g, "''")}','User')`,
125
- ], { encoding: "utf8", windowsHide: true });
126
- const value = result.status === 0 ? result.stdout.trim() : "";
127
- userEnvCache.set(name, value);
128
- return value;
159
+ function requireWindowsHost(ctx, files) {
160
+ if (ctx.platform !== "win32") fail("Windows signing must run on Windows");
161
+ for (const file of files) if (!ctx.exists(file)) fail(`missing file: ${file}`);
129
162
  }
130
163
 
131
- function run(cmd, runArgs) {
132
- const result = spawnSync(cmd, runArgs, { env: childEnv, encoding: "utf8", windowsHide: true });
133
- if (result.stdout) process.stdout.write(result.stdout);
134
- if (result.stderr) process.stderr.write(result.stderr);
135
- if (result.status !== 0) process.exit(result.status ?? 1);
136
- return `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
164
+ /** Verify Authenticode on each file (signtool verify /pa /v) against the signer subject. Returns one evidence record per file. */
165
+ export function verifyWindowsFiles(files, { ctx = windowsSigningContext() } = {}) {
166
+ const absolute = files.map((file) => path.resolve(file));
167
+ requireWindowsHost(ctx, absolute);
168
+ const signtool = requireSigntool(ctx);
169
+ return absolute.map((file) => ({ file, ...assertVerification(file, ctx.run(signtool, ["verify", "/pa", "/v", file])) }));
137
170
  }
138
171
 
139
- function assertVerification(file, output) {
140
- if (!/successfully verified/i.test(output)) fail(`Authenticode verification did not report Valid for ${file}`);
141
- if (!/^[ \t]*Issued to:[ \t]*Damned Ventures LLC[ \t]*$/m.test(output)) fail(`Authenticode subject must be Damned Ventures LLC for ${file}`);
142
- if (!/(?:timestamp|time stamped|timestamped)/i.test(output)) fail(`Authenticode timestamp is missing for ${file}`);
143
- return { authenticode: "Valid", subject: "CN=Damned Ventures LLC", timestampPresent: true };
172
+ /**
173
+ * Sign each file with Azure Artifact Signing, verify it, and return one evidence record per file `{ file, before, after, authenticode, subject,
174
+ * timestampPresent }`. `receiptPath` also writes the receipt the release path reads (`{ schema: 1, files }`).
175
+ */
176
+ export function signWindowsFiles(files, { ctx = windowsSigningContext(), receiptPath = null } = {}) {
177
+ const absolute = files.map((file) => path.resolve(file));
178
+ requireWindowsHost(ctx, absolute);
179
+ requireSignerToken(ctx);
180
+ const signtool = requireSigntool(ctx);
181
+ const dlib = ctx.findFirst([
182
+ ctx.env("AZURE_CODESIGN_DLIB_PATH"),
183
+ ctx.env("AZURE_ARTIFACT_SIGNING_DLIB_PATH"),
184
+ path.join(ctx.env("LOCALAPPDATA") || "", "AzureArtifactSigningTools", "Microsoft.ArtifactSigning.Client", "bin", "x64", "Azure.CodeSigning.Dlib.dll"),
185
+ "C:\\Program Files\\Microsoft Azure Artifact Signing Client Tools\\x64\\Azure.CodeSigning.Dlib.dll",
186
+ "C:\\Program Files (x86)\\Microsoft\\ArtifactSigningClientTools\\bin\\x64\\Azure.CodeSigning.Dlib.dll",
187
+ ]);
188
+ if (!dlib) fail("Azure.CodeSigning.Dlib.dll not found; set AZURE_CODESIGN_DLIB_PATH or install Azure Artifact Signing Client Tools");
189
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "right-sign-"));
190
+ const evidence = [];
191
+ try {
192
+ const metadata = metadataPath(ctx, tempDir);
193
+ for (const file of absolute) {
194
+ const before = fileEvidence(file);
195
+ ctx.run(signtool, ["sign", "/v", "/fd", "SHA256", "/tr", "http://timestamp.acs.microsoft.com", "/td", "SHA256", "/dlib", dlib, "/dmdf", metadata, file]);
196
+ const verification = assertVerification(file, ctx.run(signtool, ["verify", "/pa", "/v", file]));
197
+ evidence.push({ file, before, after: fileEvidence(file), ...verification });
198
+ }
199
+ } finally {
200
+ rmSync(tempDir, { recursive: true, force: true });
201
+ }
202
+ if (receiptPath) {
203
+ const absoluteReceipt = path.resolve(receiptPath);
204
+ mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
205
+ writeFileSync(absoluteReceipt, `${JSON.stringify({ schema: 1, files: evidence }, null, 2)}\n`);
206
+ }
207
+ return evidence;
144
208
  }
145
209
 
146
- function fileEvidence(file) {
147
- const bytes = readFileSync(file);
148
- return { sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: statSync(file).size };
210
+ function metadataPath(ctx, dir) {
211
+ const existing = ctx.env("AZURE_ARTIFACT_SIGNING_METADATA") || ctx.env("AZURE_SIGNING_METADATA");
212
+ if (existing) {
213
+ if (!ctx.exists(existing)) fail(`metadata file not found: ${existing}`);
214
+ return existing;
215
+ }
216
+ const Endpoint = ctx.env("AZURE_ARTIFACT_SIGNING_ENDPOINT") || ctx.env("AZURE_SIGNING_ENDPOINT") || ctx.env("AZURE_ENDPOINT");
217
+ const CodeSigningAccountName = ctx.env("AZURE_ARTIFACT_SIGNING_ACCOUNT") || ctx.env("AZURE_SIGNING_ACCOUNT_NAME") || ctx.env("AZURE_ACCOUNT");
218
+ const CertificateProfileName = ctx.env("AZURE_ARTIFACT_SIGNING_PROFILE") || ctx.env("AZURE_CERTIFICATE_PROFILE_NAME") || ctx.env("AZURE_PROFILE");
219
+ if (!Endpoint || !CodeSigningAccountName || !CertificateProfileName) {
220
+ fail("set AZURE_ARTIFACT_SIGNING_ENDPOINT, AZURE_ARTIFACT_SIGNING_ACCOUNT, and AZURE_ARTIFACT_SIGNING_PROFILE");
221
+ }
222
+ const file = path.join(dir, "metadata.json");
223
+ writeFileSync(file, `${JSON.stringify({ Endpoint, CodeSigningAccountName, CertificateProfileName, ExcludeCredentials: EXCLUDE_CREDENTIALS }, null, 2)}\n`);
224
+ return file;
149
225
  }
150
226
 
151
- function fail(message) {
152
- console.error(`right-sign-windows: ${message}`);
153
- process.exit(1);
227
+ /** The CLI. Returns the exit code; prints what the script always printed. It never exits itself: only the entry below does, after this returned (its temp directory is removed in signWindowsFiles' finally). */
228
+ export function main(args = process.argv.slice(2), options = {}) {
229
+ const dryRun = args.includes("--dry-run");
230
+ const verifyOnly = args.includes("--verify-only");
231
+ const receiptIndex = args.indexOf("--receipt");
232
+ const receiptPath = receiptIndex >= 0 ? args[receiptIndex + 1] : null;
233
+ const files = args
234
+ .filter((arg, index) => arg !== "--dry-run" && arg !== "--verify-only" && arg !== "--receipt" && (receiptIndex < 0 || index !== receiptIndex + 1))
235
+ .map((arg) => path.resolve(arg));
236
+ try {
237
+ if (!files.length) fail("usage: node sign-windows.mjs [--dry-run|--verify-only] <file>...");
238
+ if (receiptIndex >= 0 && !receiptPath) fail("--receipt requires a path");
239
+ if (dryRun) {
240
+ console.log(`dry-run: ${verifyOnly ? "Authenticode verification" : "Azure Artifact Signing"} ${files.join(", ")}`);
241
+ return 0;
242
+ }
243
+ const ctx = windowsSigningContext(options);
244
+ if (verifyOnly) verifyWindowsFiles(files, { ctx });
245
+ else signWindowsFiles(files, { ctx, receiptPath });
246
+ return 0;
247
+ } catch (error) {
248
+ if (!(error instanceof WindowsSigningError)) throw error;
249
+ if (!error.silent) console.error(`right-sign-windows: ${error.message}`);
250
+ return error.exitCode;
251
+ }
154
252
  }
253
+
254
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) process.exit(main());
@@ -0,0 +1,35 @@
1
+ // The file listers the signing policy's artifact classification reads a product package's integration tests with (rightkit#1303): the policy itself stays
2
+ // pure and is handed `listFiles(dir)`, which returns every file under an app-relative directory as an app-relative `/` path ([] when the directory
3
+ // does not exist, null when it cannot be read, so the policy treats it as unclassifiable and refuses).
4
+ import { readdirSync } from "node:fs";
5
+ import path from "node:path";
6
+
7
+ /** `listFiles` over a checkout or snapshot directory. */
8
+ export function fsListRustFiles(root) {
9
+ return (dir) => {
10
+ const out = [];
11
+ const walk = (rel) => {
12
+ let entries;
13
+ try { entries = readdirSync(path.resolve(root, rel), { withFileTypes: true }); }
14
+ catch (error) { if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return true; return false; }
15
+ for (const entry of entries) {
16
+ const child = rel === "." ? entry.name : `${rel}/${entry.name}`;
17
+ if (entry.isDirectory()) { if (entry.name === "target" || entry.name === "node_modules" || entry.name === ".git") continue; if (!walk(child)) return false; }
18
+ else out.push(child);
19
+ }
20
+ return true;
21
+ };
22
+ return walk(String(dir).replaceAll("\\", "/")) ? out : null;
23
+ };
24
+ }
25
+
26
+ /** `listFiles` over a commit's object database: `git(...args)` returns `{ status, stdout }`-shaped output as the gate's `git` helper does (via `list`). */
27
+ export function gitListRustFiles(list, commit, appDir = ".") {
28
+ return (dir) => {
29
+ const rel = path.posix.normalize(appDir === "." ? String(dir) : `${appDir}/${dir}`);
30
+ const names = list(commit, rel);
31
+ if (!Array.isArray(names)) return null;
32
+ const prefix = appDir === "." ? "" : `${appDir}/`;
33
+ return names.map((name) => (prefix && name.startsWith(prefix) ? name.slice(prefix.length) : name));
34
+ };
35
+ }
@@ -0,0 +1,7 @@
1
+ // The product's one signing identity, in a file with no imports: the gate's route decision (rightkit-gate.mjs, which runs from an installed package with
2
+ // no node_modules) and the signers both read it, so the two cannot name different identities (rightkit#1303).
3
+
4
+ /** The one Apple team every product signs with, release and development. */
5
+ export const DEFAULT_TEAM = "6KLGD3LLKF";
6
+ /** The signer subject every Windows artifact carries (Azure Artifact Signing's certificate profile), as `signtool verify` reports it. */
7
+ export const WINDOWS_SIGNER_SUBJECT = "CN=Damned Ventures LLC";