@rightkit/release 0.2.81 → 0.2.83
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.
- package/build-release.mjs +30 -3
- package/macos-signing-contract.mjs +1 -0
- package/notarize-macos.mjs +57 -0
- package/package.json +1 -1
- package/release.mjs +85 -2
- package/rightkit-versions.json +7 -4
- package/sign-macos.mjs +66 -0
- package/sign-release-manifest.mjs +0 -1
- package/sign-windows.mjs +0 -1
package/build-release.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimi
|
|
|
42
42
|
// These four files are the ones whose behaviour the cached target directory can
|
|
43
43
|
// outlive.
|
|
44
44
|
|
|
45
|
-
const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
|
|
45
|
+
const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "notarize-macos.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
|
|
46
46
|
const PIPELINE_FINGERPRINT = createHash("sha256")
|
|
47
47
|
.update(PIPELINE_FINGERPRINT_SOURCES.map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
|
|
48
48
|
.digest("hex")
|
|
@@ -168,7 +168,26 @@ try {
|
|
|
168
168
|
// outlived the change that was supposed to retire them.
|
|
169
169
|
pipeline: PIPELINE_FINGERPRINT,
|
|
170
170
|
configSha256: hashFile(configPath),
|
|
171
|
-
|
|
171
|
+
// Resolve each phase receipt the same way signing does: the config's
|
|
172
|
+
// sign.receipt / sign.installerReceipt override wins, the default
|
|
173
|
+
// windows-<phase>.json name is only the fallback. Hardcoding the default
|
|
174
|
+
// here made the seal read a path the signer never wrote whenever a config
|
|
175
|
+
// (e.g. an architecture-qualified receipt) overrode it.
|
|
176
|
+
receiptInputs: ["raw-exe", "installer", "embedding"].map((phase) => {
|
|
177
|
+
const configured = phase === "raw-exe" ? target.sign?.receipt : phase === "installer" ? target.sign?.installerReceipt : undefined;
|
|
178
|
+
return configured ? path.resolve(appRoot, configured) : path.join(receiptRoot, `windows-${phase}.json`);
|
|
179
|
+
}),
|
|
180
|
+
} : platform === "mac" ? {
|
|
181
|
+
contract: target.signingContract ?? "<missing>",
|
|
182
|
+
pipeline: PIPELINE_FINGERPRINT,
|
|
183
|
+
configSha256: hashFile(configPath),
|
|
184
|
+
// Mac portable releases produce one Developer ID receipt before packaging
|
|
185
|
+
// and one Accepted notarization receipt after. Both must be part of cache
|
|
186
|
+
// identity and seal evidence, including configured receipt overrides.
|
|
187
|
+
receiptInputs: [
|
|
188
|
+
target.sign?.receipt ? path.resolve(appRoot, target.sign.receipt) : path.join(receiptRoot, "macos-signing.json"),
|
|
189
|
+
target.notarize?.receipt ? path.resolve(appRoot, target.notarize.receipt) : path.join(receiptRoot, "macos-notarization.json"),
|
|
190
|
+
],
|
|
172
191
|
} : null;
|
|
173
192
|
if (signingIdentity) inputHashes[".right-release/signing-identity.json"] = hashFileText(JSON.stringify(signingIdentity));
|
|
174
193
|
const cargoLock = nativeLayout.lockPath;
|
|
@@ -435,7 +454,15 @@ function sealRelease({ configRoot, managedCargoTarget, nativeLayout, sealedDir,
|
|
|
435
454
|
commit,
|
|
436
455
|
platform,
|
|
437
456
|
cacheKey,
|
|
438
|
-
|
|
457
|
+
// Portable contracts sign only raw executables — installer/embedding
|
|
458
|
+
// receipts legitimately never exist there. Seal every receipt that was
|
|
459
|
+
// produced, but never zero: a signed platform with no receipt at all is
|
|
460
|
+
// an unsound seal, not a portable release.
|
|
461
|
+
signing: signingIdentity ? { ...signingIdentity, receipts: (() => {
|
|
462
|
+
const present = signingIdentity.receiptInputs.filter((file) => existsSync(file));
|
|
463
|
+
if (present.length === 0) throw new Error(`no signing receipts found to seal; expected at least ${signingIdentity.receiptInputs[0]}`);
|
|
464
|
+
return present.map((file) => ({ file, sha256: hashFile(file) }));
|
|
465
|
+
})() } : null,
|
|
439
466
|
files,
|
|
440
467
|
routes,
|
|
441
468
|
inputs: inputHashes,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const MACOS_PORTABLE_SIGNING_CONTRACT = "macos-developer-id-notarized-portable-v1";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { notarytoolAuthArgs } from "./notary-auth.mjs";
|
|
7
|
+
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const dryRun = args.includes("--dry-run");
|
|
11
|
+
const receiptIndex = args.indexOf("--receipt");
|
|
12
|
+
const profileIndex = args.indexOf("--profile");
|
|
13
|
+
const receiptPath = receiptIndex >= 0 ? args[receiptIndex + 1] : null;
|
|
14
|
+
const profile = profileIndex >= 0 ? args[profileIndex + 1] : undefined;
|
|
15
|
+
const archiveArgs = args.filter((arg, index) => arg !== "--dry-run" && arg !== "--receipt" && arg !== "--profile" && (receiptIndex < 0 || index !== receiptIndex + 1) && (profileIndex < 0 || index !== profileIndex + 1));
|
|
16
|
+
if (archiveArgs.length !== 1) fail("usage: node notarize-macos.mjs [--dry-run] --receipt <path> [--profile <name>] <archive>");
|
|
17
|
+
if (!receiptPath) fail("--receipt requires a path");
|
|
18
|
+
const archive = path.resolve(archiveArgs[0]);
|
|
19
|
+
if (dryRun) {
|
|
20
|
+
console.log(`dry-run: notarize ${archive}`);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
if (process.platform !== "darwin") fail("macOS notarization must run on macOS");
|
|
24
|
+
if (!existsSync(archive)) fail(`missing archive: ${archive}`);
|
|
25
|
+
|
|
26
|
+
let authArgs;
|
|
27
|
+
try { authArgs = notarytoolAuthArgs({ profile }); } catch (error) { fail(error.message); }
|
|
28
|
+
const output = run("xcrun", ["notarytool", "submit", archive, "--wait", "--output-format", "json", ...authArgs]);
|
|
29
|
+
let result;
|
|
30
|
+
try { result = JSON.parse(output); } catch { fail(`notarytool did not return JSON for ${archive}`); }
|
|
31
|
+
if (result.status !== "Accepted") fail(`notarization was not accepted for ${archive}: ${result.status ?? "missing status"}`);
|
|
32
|
+
|
|
33
|
+
const absoluteReceipt = path.resolve(receiptPath);
|
|
34
|
+
mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
|
|
35
|
+
writeFileSync(absoluteReceipt, `${JSON.stringify({
|
|
36
|
+
schema: 1,
|
|
37
|
+
signingContract: MACOS_PORTABLE_SIGNING_CONTRACT,
|
|
38
|
+
archive: { file: archive, ...fileEvidence(archive) },
|
|
39
|
+
notarization: { status: result.status, id: result.id ?? result.submissionId ?? null },
|
|
40
|
+
}, null, 2)}\n`);
|
|
41
|
+
|
|
42
|
+
function fileEvidence(file) {
|
|
43
|
+
const bytes = readFileSync(file);
|
|
44
|
+
return { sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: statSync(file).size };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function run(cmd, runArgs) {
|
|
48
|
+
const result = spawnSync(cmd, runArgs, { encoding: "utf8" });
|
|
49
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
|
|
50
|
+
if (result.status !== 0) fail(`${cmd} ${runArgs.join(" ")} failed: ${output}`);
|
|
51
|
+
return output;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fail(message) {
|
|
55
|
+
console.error(`right-notarize-macos: ${message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.83",
|
|
4
4
|
"description": "Portable Right Suite release CLI/SDK: signed native archives, direct bootstrap transactions, immutable GitHub Release upload, R2 bootstrap publication, and add-on adoption.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
package/release.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { access, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, unlinkSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import { spawn, spawnSync } from "node:child_process";
|
|
@@ -22,11 +22,14 @@ import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
|
|
|
22
22
|
import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
23
23
|
import { terminateProcessTree } from "./heavy-command.mjs";
|
|
24
24
|
import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
|
|
25
|
+
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
25
26
|
|
|
26
27
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
27
28
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
28
29
|
const UPLOAD_LARGE = path.resolve(TOOL_ROOT, "upload-large.mjs");
|
|
29
30
|
const SIGN_WINDOWS = path.resolve(TOOL_ROOT, "sign-windows.mjs");
|
|
31
|
+
const SIGN_MACOS = path.resolve(TOOL_ROOT, "sign-macos.mjs");
|
|
32
|
+
const NOTARIZE_MACOS = path.resolve(TOOL_ROOT, "notarize-macos.mjs");
|
|
30
33
|
const SIGN_UPDATER = path.resolve(TOOL_ROOT, "sign-updater.mjs");
|
|
31
34
|
const RIGHTKIT_VERSIONS = JSON.parse(readFileSync(path.resolve(TOOL_ROOT, "rightkit-versions.json"), "utf8"));
|
|
32
35
|
const VERSION = RIGHTKIT_VERSIONS.stagedNpm?.["@rightkit/release"] ?? RIGHTKIT_VERSIONS.npm["@rightkit/release"];
|
|
@@ -124,6 +127,12 @@ if (opts.platform === "win") {
|
|
|
124
127
|
if (isNsis && target.sign.prePackageFiles.some((file) => target.sign.files.includes(file))) fail(`${config.app ?? "app"} win raw EXE and installer signing files must be distinct`);
|
|
125
128
|
if (isPortable && target.sign?.files?.length) fail(`${config.app ?? "app"} win portable release must sign every executable through sign.prePackageFiles and omit sign.files`);
|
|
126
129
|
}
|
|
130
|
+
if (opts.platform === "mac") {
|
|
131
|
+
if (target.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT) fail(`${config.app ?? "app"} mac must declare signingContract: ${MACOS_PORTABLE_SIGNING_CONTRACT}`);
|
|
132
|
+
if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} mac must declare prePackage to materialize Developer ID signing files`);
|
|
133
|
+
if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
|
|
134
|
+
if (!target.notarize?.file) fail(`${config.app ?? "app"} mac must declare notarize.file`);
|
|
135
|
+
}
|
|
127
136
|
if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
|
|
128
137
|
/**
|
|
129
138
|
* Assemble preflight inputs from the app's own files. Kept here (not in
|
|
@@ -240,6 +249,11 @@ if (opts.platform === "win") {
|
|
|
240
249
|
signedFiles: rawFiles,
|
|
241
250
|
});
|
|
242
251
|
}
|
|
252
|
+
} else if (opts.platform === "mac") {
|
|
253
|
+
const signingFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
|
|
254
|
+
await runCommand(target.prePackage, root);
|
|
255
|
+
for (const file of signingFiles) await mustExist(file, `missing macOS Developer ID signing artifact: ${file}`);
|
|
256
|
+
await signMacos(signingFiles, root);
|
|
243
257
|
} else if (nativeAssembly) {
|
|
244
258
|
if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
|
|
245
259
|
if (nativeFinalizer) {
|
|
@@ -279,6 +293,12 @@ if (target.postSign && opts.platform !== "win") {
|
|
|
279
293
|
await runCommand(target.postSign, root);
|
|
280
294
|
}
|
|
281
295
|
|
|
296
|
+
if (opts.platform === "mac") {
|
|
297
|
+
const archive = path.resolve(root, target.notarize.file);
|
|
298
|
+
await mustExist(archive, `missing macOS notarization archive: ${archive}`);
|
|
299
|
+
await notarizeMacos(archive, root);
|
|
300
|
+
}
|
|
301
|
+
|
|
282
302
|
{
|
|
283
303
|
const scanTargets = target.hardening ?? target.artifacts ?? [];
|
|
284
304
|
if (scanTargets.length) {
|
|
@@ -378,6 +398,12 @@ async function runNativeFinalizer({ finalizer, nativeAssembly, target, root, sig
|
|
|
378
398
|
...finalizer,
|
|
379
399
|
args: finalizer.args.map((arg) => expandFinalizerToken(arg, { provenance, outputPath, packageIdentity })),
|
|
380
400
|
};
|
|
401
|
+
// A finalization receipt at outputPath from a PREVIOUS run must not
|
|
402
|
+
// survive into this one: the file is preferred over the finalizer's
|
|
403
|
+
// stdout below, and a stale receipt carries the prior run's provenance,
|
|
404
|
+
// which fails the identity check against this run's mint. Any file
|
|
405
|
+
// present after the finalizer returns was written by this run.
|
|
406
|
+
if (!opts.dryRun) rmSync(outputPath, { force: true });
|
|
381
407
|
const stdout = await runCommand(finalizerCommand, root, {
|
|
382
408
|
captureStdout: true,
|
|
383
409
|
env: {
|
|
@@ -645,9 +671,14 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
645
671
|
if (!source.startsWith("# Managed by right-git")) {
|
|
646
672
|
fail(`${appName} ${workflow} is not marked as right-git managed`);
|
|
647
673
|
}
|
|
674
|
+
const requestedSecrets = [...source.matchAll(/\bsecrets\.([A-Z0-9_]+)/gi)].map((match) => match[1]);
|
|
675
|
+
const allowedMacosSecrets = new Set(["APPLE_CERTIFICATE_BASE64", "APPLE_CERTIFICATE_PASSWORD", "APPLE_KEYCHAIN_PASSWORD", "APPLE_API_KEY_BASE64", "APPLE_API_KEY", "APPLE_API_ISSUER"]);
|
|
676
|
+
const macosSigningAllowed = workflow === "release-candidate.yml" && manifest?.candidate?.signMacos === true;
|
|
648
677
|
if (/^\s*permissions:\s*write-all\s*$/mi.test(source)
|
|
649
678
|
|| /^\s*(?:actions|attestations|checks|contents|deployments|discussions|id-token|issues|models|packages|pages|pull-requests|security-events|statuses):\s*write\s*$/mi.test(source)
|
|
650
|
-
||
|
|
679
|
+
|| /pull_request_target\s*:/i.test(source)
|
|
680
|
+
|| /\bsecrets\s*\[/i.test(source)
|
|
681
|
+
|| requestedSecrets.some((name) => !macosSigningAllowed || !allowedMacosSecrets.has(name))) {
|
|
651
682
|
fail(`${appName} right-git managed ${workflow} requests release-capable permissions or secrets`);
|
|
652
683
|
}
|
|
653
684
|
}
|
|
@@ -699,6 +730,58 @@ function windowsReceiptPath(root, phase) {
|
|
|
699
730
|
return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `windows-${phase}.json`);
|
|
700
731
|
}
|
|
701
732
|
|
|
733
|
+
async function signMacos(files, root) {
|
|
734
|
+
const receipt = macosReceiptPath(root, "signing");
|
|
735
|
+
const args = [SIGN_MACOS];
|
|
736
|
+
if (opts.dryRun) args.push("--dry-run");
|
|
737
|
+
args.push("--receipt", receipt, ...files);
|
|
738
|
+
if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
|
|
739
|
+
console.log("right-release: signing macOS Developer ID files");
|
|
740
|
+
await run("node", args, root);
|
|
741
|
+
if (!opts.dryRun) verifyMacosSigningReceipt(receipt, files);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function notarizeMacos(archive, root) {
|
|
745
|
+
const receipt = macosReceiptPath(root, "notarization");
|
|
746
|
+
const args = [NOTARIZE_MACOS];
|
|
747
|
+
if (opts.dryRun) args.push("--dry-run");
|
|
748
|
+
args.push("--receipt", receipt);
|
|
749
|
+
if (target.notarize.profile) args.push("--profile", target.notarize.profile);
|
|
750
|
+
args.push(archive);
|
|
751
|
+
if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
|
|
752
|
+
console.log("right-release: notarizing macOS archive");
|
|
753
|
+
await run("node", args, root);
|
|
754
|
+
if (!opts.dryRun) verifyMacosNotarizationReceipt(receipt, archive);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function macosReceiptPath(root, phase) {
|
|
758
|
+
const configured = phase === "signing" ? target.sign?.receipt : target.notarize?.receipt;
|
|
759
|
+
if (configured) return path.resolve(root, configured);
|
|
760
|
+
return path.join(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `macos-${phase}.json`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function verifyMacosSigningReceipt(receipt, files) {
|
|
764
|
+
if (!existsSync(receipt)) fail(`missing macOS Developer ID signing receipt: ${receipt}`);
|
|
765
|
+
let evidence;
|
|
766
|
+
try { evidence = JSON.parse(readFileSync(receipt, "utf8")); } catch { fail(`invalid macOS Developer ID signing receipt: ${receipt}`); }
|
|
767
|
+
const expectedIdentity = process.env.APPLE_DEVELOPER_ID?.trim();
|
|
768
|
+
const expected = files.map((file) => path.resolve(file)).sort();
|
|
769
|
+
const actual = Array.isArray(evidence.files) ? evidence.files.map((item) => path.resolve(item.file)).sort() : [];
|
|
770
|
+
const hashes = new Map(files.map((file) => [path.resolve(file), createHash("sha256").update(readFileSync(file)).digest("hex")]));
|
|
771
|
+
if (evidence.schema !== 1 || evidence.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT || !expectedIdentity || evidence.identity !== expectedIdentity
|
|
772
|
+
|| JSON.stringify(expected) !== JSON.stringify(actual)
|
|
773
|
+
|| evidence.files.some((item) => item.codesign !== "strict-valid" || item.identity !== expectedIdentity || item.timestampPresent !== true || item.after?.sha256 !== hashes.get(path.resolve(item.file)))) fail(`invalid macOS Developer ID signing receipt evidence: ${receipt}`);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function verifyMacosNotarizationReceipt(receipt, archive) {
|
|
777
|
+
if (!existsSync(receipt)) fail(`missing macOS notarization receipt: ${receipt}`);
|
|
778
|
+
let evidence;
|
|
779
|
+
try { evidence = JSON.parse(readFileSync(receipt, "utf8")); } catch { fail(`invalid macOS notarization receipt: ${receipt}`); }
|
|
780
|
+
const sha256 = createHash("sha256").update(readFileSync(archive)).digest("hex");
|
|
781
|
+
if (evidence.schema !== 1 || evidence.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT || path.resolve(evidence.archive?.file ?? "") !== archive
|
|
782
|
+
|| evidence.archive?.sha256 !== sha256 || evidence.notarization?.status !== "Accepted") fail(`invalid macOS notarization receipt evidence: ${receipt}`);
|
|
783
|
+
}
|
|
784
|
+
|
|
702
785
|
/**
|
|
703
786
|
* Close the gap between "the raw EXE was signed" and "the installer ships that
|
|
704
787
|
* EXE". Three independent facts, each able to fail on its own:
|
package/rightkit-versions.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"stagedNpm": {
|
|
16
16
|
"@rightkit/ax": "0.2.1",
|
|
17
|
-
"@rightkit/git": "0.2.
|
|
17
|
+
"@rightkit/git": "0.2.7",
|
|
18
18
|
"@rightkit/hooks": "0.1.1",
|
|
19
19
|
"@rightkit/legal": "0.3.1",
|
|
20
20
|
"@rightkit/legal-ui": "0.1.1",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@rightkit/logs": "0.1.4",
|
|
23
23
|
"@rightkit/platform-ui": "0.1.1",
|
|
24
24
|
"@rightkit/qa": "0.2.1",
|
|
25
|
-
"@rightkit/release": "0.2.
|
|
25
|
+
"@rightkit/release": "0.2.83",
|
|
26
26
|
"@rightkit/tauri": "0.1.1",
|
|
27
27
|
"@rightkit/updates": "0.2.4"
|
|
28
28
|
},
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"0.2.1",
|
|
35
35
|
"0.2.2",
|
|
36
36
|
"0.2.3",
|
|
37
|
-
"0.2.4"
|
|
37
|
+
"0.2.4",
|
|
38
|
+
"0.2.5",
|
|
39
|
+
"0.2.6"
|
|
38
40
|
],
|
|
39
41
|
"@rightkit/legal-ui": [
|
|
40
42
|
"0.1.0"
|
|
@@ -81,7 +83,8 @@
|
|
|
81
83
|
"0.2.73",
|
|
82
84
|
"0.2.74",
|
|
83
85
|
"0.2.75",
|
|
84
|
-
"0.2.76"
|
|
86
|
+
"0.2.76",
|
|
87
|
+
"0.2.82"
|
|
85
88
|
],
|
|
86
89
|
"@rightkit/qa": [
|
|
87
90
|
"0.1.0",
|
package/sign-macos.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
7
|
+
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
const dryRun = args.includes("--dry-run");
|
|
10
|
+
const receiptIndex = args.indexOf("--receipt");
|
|
11
|
+
const receiptPath = receiptIndex >= 0 ? args[receiptIndex + 1] : null;
|
|
12
|
+
const files = args
|
|
13
|
+
.filter((arg, index) => arg !== "--dry-run" && arg !== "--receipt" && (receiptIndex < 0 || index !== receiptIndex + 1))
|
|
14
|
+
.map((arg) => path.resolve(arg));
|
|
15
|
+
|
|
16
|
+
if (!files.length) fail("usage: node sign-macos.mjs [--dry-run] --receipt <path> <file>...");
|
|
17
|
+
if (!receiptPath) fail("--receipt requires a path");
|
|
18
|
+
if (dryRun) {
|
|
19
|
+
console.log(`dry-run: Developer ID signing ${files.join(", ")}`);
|
|
20
|
+
process.exit(0);
|
|
21
|
+
}
|
|
22
|
+
if (process.platform !== "darwin") fail("macOS signing must run on macOS");
|
|
23
|
+
const identity = process.env.APPLE_DEVELOPER_ID?.trim();
|
|
24
|
+
if (!identity) fail("set APPLE_DEVELOPER_ID to an installed Developer ID Application signing identity");
|
|
25
|
+
for (const file of files) if (!existsSync(file)) fail(`missing file: ${file}`);
|
|
26
|
+
|
|
27
|
+
const evidence = [];
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const before = fileEvidence(file);
|
|
30
|
+
run("codesign", ["--force", "--options", "runtime", "--timestamp", "--sign", identity, file]);
|
|
31
|
+
const verification = run("codesign", ["--verify", "--strict", "--verbose=4", file]);
|
|
32
|
+
const details = run("codesign", ["-dvv", file]);
|
|
33
|
+
if (!/valid on disk/i.test(verification) || !/satisfies its designated requirement/i.test(verification)) {
|
|
34
|
+
fail(`strict codesign verification failed for ${file}`);
|
|
35
|
+
}
|
|
36
|
+
if (!new RegExp(`^Authority=${escapeRegExp(identity)}$`, "m").test(details) && !details.includes(`Authority=${identity}`)) {
|
|
37
|
+
fail(`codesign identity mismatch for ${file}: expected ${identity}`);
|
|
38
|
+
}
|
|
39
|
+
evidence.push({ file, before, after: fileEvidence(file), codesign: "strict-valid", identity, timestampPresent: /(?:Timestamp|Signed Time)[=:]/i.test(details) });
|
|
40
|
+
if (!evidence.at(-1).timestampPresent) fail(`Developer ID timestamp is missing for ${file}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const absoluteReceipt = path.resolve(receiptPath);
|
|
44
|
+
mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
|
|
45
|
+
writeFileSync(absoluteReceipt, `${JSON.stringify({ schema: 1, signingContract: MACOS_PORTABLE_SIGNING_CONTRACT, identity, files: evidence }, null, 2)}\n`);
|
|
46
|
+
|
|
47
|
+
function fileEvidence(file) {
|
|
48
|
+
const bytes = readFileSync(file);
|
|
49
|
+
return { sha256: createHash("sha256").update(bytes).digest("hex"), sizeBytes: statSync(file).size };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function run(cmd, runArgs) {
|
|
53
|
+
const result = spawnSync(cmd, runArgs, { encoding: "utf8" });
|
|
54
|
+
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
|
|
55
|
+
if (result.status !== 0) fail(`${cmd} ${runArgs.join(" ")} failed: ${output.trim()}`);
|
|
56
|
+
return output;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function escapeRegExp(value) {
|
|
60
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fail(message) {
|
|
64
|
+
console.error(`right-sign-macos: ${message}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
package/sign-windows.mjs
CHANGED