@rightkit/release 0.2.134 → 0.2.135
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/artifact-storage.mjs +7 -3
- package/build-release.mjs +4 -3
- package/codesign-argv.mjs +64 -0
- package/codesign-environment.mjs +34 -0
- package/codesign-shim.mjs +50 -0
- package/development.mjs +4 -2
- package/macos-notarization.mjs +24 -8
- package/macos-signing-session.mjs +47 -2
- package/notarize-macos.mjs +5 -5
- package/ownership-helper-paths.json +2 -2
- package/package.json +1 -1
- package/release.mjs +2 -1
- package/rightkit-signing-agent.mjs +157 -17
- package/rightkit-versions.json +16 -12
- package/signing-agent-client.mjs +76 -21
- package/signing-agent-command.mjs +5 -4
- package/signing-agent-install.mjs +30 -4
package/artifact-storage.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
writeFileSync,
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import path from "node:path";
|
|
14
|
+
import os from "node:os";
|
|
14
15
|
|
|
15
16
|
export const ARTIFACT_MARKER_SCHEMA = 1;
|
|
16
17
|
export const DEFAULT_MANAGED_ROOT = process.platform === "win32"
|
|
@@ -19,9 +20,12 @@ export const DEFAULT_MANAGED_ROOT = process.platform === "win32"
|
|
|
19
20
|
|
|
20
21
|
const SAFE_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,159}$/;
|
|
21
22
|
|
|
22
|
-
export function resolveManagedRoot({ env = process.env } = {}) {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
export function resolveManagedRoot({ env = process.env, exists = existsSync } = {}) {
|
|
24
|
+
if (env.RIGHTKIT_MANAGED_ROOT) return path.resolve(env.RIGHTKIT_MANAGED_ROOT);
|
|
25
|
+
// Workstation default only when its drive exists; hosted CI runners fall back to RUNNER_TEMP, then the system temp dir.
|
|
26
|
+
const volume = process.platform === "win32" ? "D:\\" : "/Volumes/D";
|
|
27
|
+
if (exists(volume)) return path.resolve(DEFAULT_MANAGED_ROOT);
|
|
28
|
+
return path.join(env.RUNNER_TEMP && exists(env.RUNNER_TEMP) ? env.RUNNER_TEMP : os.tmpdir(), "rightkit-managed");
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
export function resolveArtifactRoot({ app, managedRoot } = {}) {
|
package/build-release.mjs
CHANGED
|
@@ -39,13 +39,14 @@ import { acquireHeavyWorkSlot, heavyCommandEnvironment, summarizePreBrokerWaits,
|
|
|
39
39
|
import { processGroupCpuSeconds } from "./process-liveness.mjs";
|
|
40
40
|
import { cancelAndReapManagedRequest, firstManagedRequestId, releaseProgressLimits } from "./progress-control.mjs";
|
|
41
41
|
import { beginArtifactLease, completeArtifactLease, materializeArtifacts, retainArtifacts, resolveArtifactDir, resolveManagedRoot } from "./artifact-storage.mjs";
|
|
42
|
+
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
42
43
|
|
|
43
44
|
// Fingerprint of the pipeline code that can change the bytes we ship or the way
|
|
44
45
|
// they are signed — deliberately NOT the package version, which moves for docs
|
|
45
46
|
// and test-only edits and would force a cold Rust rebuild of every app each time.
|
|
46
47
|
// Cached target directories must not outlive changes to these signing helpers.
|
|
47
48
|
|
|
48
|
-
const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
|
|
49
|
+
const PIPELINE_FINGERPRINT_SOURCES = ["release.mjs", "sign-windows.mjs", "sign-macos.mjs", "macos-signing-session.mjs", "codesign-environment.mjs", "codesign-shim.mjs", "codesign-argv.mjs", "rightkit-signing-agent.mjs", "notarize-macos.mjs", "macos-notarization.mjs", "signing-receipts.mjs", "tauri-bundle-marker.mjs", "nsis-payload.mjs"];
|
|
49
50
|
const PIPELINE_FINGERPRINT = createHash("sha256")
|
|
50
51
|
.update(PIPELINE_FINGERPRINT_SOURCES.map((file) => `${file}:${createHash("sha256").update(readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), file))).digest("hex")}`).join("\n"))
|
|
51
52
|
.digest("hex")
|
|
@@ -251,7 +252,7 @@ try {
|
|
|
251
252
|
// resolves to MSYS Perl, which cannot configure OpenSSL. Put the real Windows
|
|
252
253
|
// Perl first so the build behaves the same from any shell.
|
|
253
254
|
const pathPrefix = buildPathPrefix({ platform });
|
|
254
|
-
const env = {
|
|
255
|
+
const env = macosCodesignEnvironment({
|
|
255
256
|
...heavyCommandEnvironment(process.env),
|
|
256
257
|
...releaseEnvironment({ root: vaultRoot, cacheRoot: sharedCacheRoot, platform, architecture: cacheIdentity.architecture, app: config.app, cacheKey, kind: "release", appRoot, mode: cacheMode }),
|
|
257
258
|
...(pathPrefix ? { PATH: `${pathPrefix}${path.delimiter}${process.env.PATH ?? ""}` } : {}),
|
|
@@ -259,7 +260,7 @@ try {
|
|
|
259
260
|
RIGHT_RELEASE_APP_ROOT: layout.appRoot,
|
|
260
261
|
RIGHTKIT_MANAGED_ROOT: resolveManagedRoot({ env: process.env }),
|
|
261
262
|
RIGHT_RELEASE_ARTIFACT_DIR: resolveArtifactDir({ app: config.app, version: config.version, managedRoot: resolveManagedRoot({ env: process.env }) }),
|
|
262
|
-
};
|
|
263
|
+
});
|
|
263
264
|
if (managedCargoTarget) {
|
|
264
265
|
for (const name of ["CARGO_HOME", "CARGO_TARGET_DIR", "RUSTC_WRAPPER", "SCCACHE_DIR", "SCCACHE_BASEDIRS", "SCCACHE_CACHE_SIZE", "RUSTFLAGS"]) {
|
|
265
266
|
delete env[name];
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
// Closed grammar shared by shim & agent. Never accept executable names, an
|
|
4
|
+
// environment, keychains, detached signatures, or file-producing display flags.
|
|
5
|
+
export function parseCodesignArgv(argv, cwd) {
|
|
6
|
+
const deny = () => { const error = new Error("Unsupported codesign argv"); error.code = "MAC_SIGNING_AGENT_ARGV_DENIED"; throw error; };
|
|
7
|
+
if (!Array.isArray(argv) || !argv.length || argv.length > 8192
|
|
8
|
+
|| argv.some((arg) => typeof arg !== "string" || !arg || arg.includes("\0") || arg.length > 32768)
|
|
9
|
+
|| typeof cwd !== "string" || !path.isAbsolute(cwd)) deny();
|
|
10
|
+
const targets = [], inputs = [], options = [];
|
|
11
|
+
let mode, verbose = false, operands = false;
|
|
12
|
+
const operation = (next) => { if (mode) deny(); mode = next; };
|
|
13
|
+
const value = (inline, i) => {
|
|
14
|
+
const result = inline ?? argv[i + 1];
|
|
15
|
+
if (!result || result.startsWith("--")) deny();
|
|
16
|
+
return result;
|
|
17
|
+
};
|
|
18
|
+
for (let i = 0; i < argv.length; i++) {
|
|
19
|
+
const arg = argv[i];
|
|
20
|
+
if (operands || !arg.startsWith("-")) { targets.push(path.isAbsolute(arg) ? arg : `${cwd}/${arg}`); continue; }
|
|
21
|
+
if (arg === "--") { operands = true; continue; }
|
|
22
|
+
if (/^-[dv]+$/.test(arg)) {
|
|
23
|
+
if (arg.includes("d")) { if ((arg.match(/d/g) ?? []).length !== 1) deny(); operation("display"); }
|
|
24
|
+
if (arg.includes("v")) verbose = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const equal = arg.indexOf("=");
|
|
28
|
+
const flag = equal < 0 ? arg : arg.slice(0, equal);
|
|
29
|
+
const inline = equal < 0 ? undefined : arg.slice(equal + 1);
|
|
30
|
+
if (flag === "--display" && inline === undefined) { operation("display"); continue; }
|
|
31
|
+
if (flag === "--verify" && inline === undefined) { operation("verify"); continue; }
|
|
32
|
+
if (flag === "--verbose") { if (inline !== undefined && !/^\d{1,2}$/.test(inline)) deny(); verbose = true; continue; }
|
|
33
|
+
if (["-s", "--sign"].includes(flag) || /^-s.+/.test(arg)) {
|
|
34
|
+
const attached = flag === "-s" || flag === "--sign" ? inline : arg.slice(2);
|
|
35
|
+
const identity = value(attached, i);
|
|
36
|
+
if (identity.length > 512 || (identity.startsWith("-") && identity !== "-")) deny();
|
|
37
|
+
operation("sign"); if (attached === undefined) i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (["--force", "--deep", "--strict"].includes(flag) && inline === undefined) { options.push(flag); continue; }
|
|
41
|
+
if (flag === "--timestamp" && (inline === undefined || inline === "none")) { options.push(flag); continue; }
|
|
42
|
+
if (["--options", "--identifier", "--preserve-metadata", "--entitlements", "--requirements"].includes(flag) || arg === "-r-") {
|
|
43
|
+
const parameter = arg === "-r-" ? "-" : value(inline, i);
|
|
44
|
+
if (arg !== "-r-" && inline === undefined) i++;
|
|
45
|
+
if (flag === "--options" && !/^(runtime|hard|kill|host|expires|restrict|library)(,(runtime|hard|kill|host|expires|restrict|library))*$/.test(parameter)) deny();
|
|
46
|
+
if (flag === "--preserve-metadata" && !/^(identifier|entitlements|requirements|flags|runtime)(,(identifier|entitlements|requirements|flags|runtime))*$/.test(parameter)) deny();
|
|
47
|
+
if (flag === "--identifier" && (parameter.startsWith("-") || parameter.length > 512)) deny();
|
|
48
|
+
options.push(arg === "-r-" ? "--requirements" : flag);
|
|
49
|
+
if (flag === "--entitlements") inputs.push({ value: parameter, path: path.isAbsolute(parameter) ? parameter : `${cwd}/${parameter}` });
|
|
50
|
+
if ((flag === "--requirements" || arg === "-r-") && parameter !== "-") deny();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
deny();
|
|
54
|
+
}
|
|
55
|
+
mode ??= verbose ? "verify" : undefined;
|
|
56
|
+
if (!mode || !targets.length || targets.length > 4096) deny();
|
|
57
|
+
const permitted = mode === "sign"
|
|
58
|
+
? ["--force", "--options", "--timestamp", "--entitlements", "--deep", "--preserve-metadata", "--identifier"]
|
|
59
|
+
: mode === "verify" ? ["--strict", "--deep"] : ["--entitlements", "--requirements"];
|
|
60
|
+
if (options.some((flag) => !permitted.includes(flag))) deny();
|
|
61
|
+
if (mode === "display" && inputs.some((input) => ![":-", "-"].includes(input.value))) deny();
|
|
62
|
+
if (mode === "sign" && inputs.some((input) => [":-", "-"].includes(input.value))) deny();
|
|
63
|
+
return { mode, targets, entitlements: mode === "sign" ? inputs.map((input) => input.path) : [] };
|
|
64
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { detectMacSigningSession } from "./macos-signing-session.mjs";
|
|
6
|
+
import { ensurePrivateDirectory, privatePath } from "./rightkit-signing-agent.mjs";
|
|
7
|
+
import { resolveManagedRoot } from "./artifact-storage.mjs";
|
|
8
|
+
|
|
9
|
+
const quote = (text) => `'${String(text).replaceAll("'", "'\\''")}'`;
|
|
10
|
+
export function codesignShimScript({ route, node = process.execPath,
|
|
11
|
+
entrypoint = fileURLToPath(new URL("./codesign-shim.mjs", import.meta.url)), realCodesign = "/usr/bin/codesign" }) {
|
|
12
|
+
return `#!/bin/sh\nif [ -n "\${SSH_CONNECTION-}\${SSH_TTY-}\${SSH_CLIENT-}" ] || [ ${quote(route)} = agent ]; then
|
|
13
|
+
exec ${quote(node)} ${quote(entrypoint)} "$@"
|
|
14
|
+
fi
|
|
15
|
+
exec ${quote(realCodesign)} "$@"\n`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function macosCodesignEnvironment(env = process.env, { platform = process.platform, io = fs,
|
|
19
|
+
detect = detectMacSigningSession, node = process.execPath } = {}) {
|
|
20
|
+
if (platform !== "darwin") return env;
|
|
21
|
+
const route = detect({ env }).guiRunner ? "agent" : "direct";
|
|
22
|
+
const script = codesignShimScript({ route, node });
|
|
23
|
+
// Immutable wrappers avoid races between GUI & SSH builds or package versions.
|
|
24
|
+
const identity = createHash("sha256").update(script).digest("hex").slice(0, 24);
|
|
25
|
+
const bin = path.join(resolveManagedRoot({ env }), "signing", "codesign-bin", identity);
|
|
26
|
+
ensurePrivateDirectory(bin, { io });
|
|
27
|
+
const file = path.join(bin, "codesign");
|
|
28
|
+
if (io.existsSync(file)) {
|
|
29
|
+
privatePath(file, 0o700, { io });
|
|
30
|
+
if (io.readFileSync(file, "utf8") !== script) throw new Error("Managed codesign shim content changed");
|
|
31
|
+
} else io.writeFileSync(file, script, { mode: 0o700, flag: "wx" });
|
|
32
|
+
// Keep every other shim (especially managed Cargo) in its original order.
|
|
33
|
+
return { ...env, PATH: [bin, ...(env.PATH ?? "").split(path.delimiter).filter((entry) => entry !== bin)].join(path.delimiter) };
|
|
34
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSync } from "./background-process.mjs";
|
|
7
|
+
import { parseCodesignArgv } from "./codesign-argv.mjs";
|
|
8
|
+
import { detectMacSigningSession } from "./macos-signing-session.mjs";
|
|
9
|
+
import { requestSigningAgent } from "./signing-agent-client.mjs";
|
|
10
|
+
import { defaultSigningRoots, privatePath, signingAgentPaths, SigningAgentError } from "./rightkit-signing-agent.mjs";
|
|
11
|
+
|
|
12
|
+
export async function runCodesignShim(argv, { cwd = process.cwd(), env = process.env, home = os.userInfo().homedir,
|
|
13
|
+
io = fs, run = spawnSync, detect = detectMacSigningSession, request = requestSigningAgent } = {}) {
|
|
14
|
+
if (!detect({ env, run }).guiRunner) {
|
|
15
|
+
// Installed shell shim uses exec here; this branch also supports API callers.
|
|
16
|
+
const result = run("/usr/bin/codesign", argv, { cwd, env, stdio: "inherit", shell: false, windowsHide: true });
|
|
17
|
+
if (result.error) throw result.error;
|
|
18
|
+
return { status: result.status ?? 1, stdout: "", stderr: "" };
|
|
19
|
+
}
|
|
20
|
+
const paths = signingAgentPaths(home);
|
|
21
|
+
let roots = defaultSigningRoots(home, env.RIGHTKIT_MANAGED_ROOT);
|
|
22
|
+
if (io.existsSync(paths.config)) {
|
|
23
|
+
privatePath(paths.config, 0o600, { io });
|
|
24
|
+
roots = JSON.parse(io.readFileSync(paths.config, "utf8")).roots;
|
|
25
|
+
}
|
|
26
|
+
const { targets } = parseCodesignArgv(argv, cwd);
|
|
27
|
+
const report = await request({ schema: 1, type: "codesign-argv", argv, cwd, targets,
|
|
28
|
+
allowedRoots: roots.filter((root) => io.existsSync(root)), timeoutMs: 180000 }, { paths });
|
|
29
|
+
const result = report.results?.[0];
|
|
30
|
+
if (report.schema !== 1 || report.ok !== true || report.results?.length !== 1
|
|
31
|
+
|| !Number.isInteger(result?.status) || result.status < 0 || result.status > 255
|
|
32
|
+
|| typeof result.stdout !== "string" || typeof result.stderr !== "string") {
|
|
33
|
+
throw new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Invalid codesign result");
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
39
|
+
try {
|
|
40
|
+
// Only the managed shell wrapper invokes this entry: session detection was
|
|
41
|
+
// already performed by right-release before selecting its wrapper directory.
|
|
42
|
+
const result = await runCodesignShim(process.argv.slice(2), { detect: () => ({ guiRunner: true }) });
|
|
43
|
+
process.stdout.write(result.stdout);
|
|
44
|
+
process.stderr.write(result.stderr);
|
|
45
|
+
process.exitCode = result.status;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
process.stderr.write(`rightkit codesign: [${error.code ?? "MAC_SIGNING_AGENT_JOB_FAILED"}] ${error.message}\n`);
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/development.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { acquireManagedRunLease, managedRuntimeClient, runCargoSync } from "./ma
|
|
|
9
9
|
import { BUNDLE_MARKERS, bundleMarkerBuffer, readTauriBundleMarkers } from "./tauri-bundle-marker.mjs";
|
|
10
10
|
import { installPortableDevelopment } from "./portable-development.mjs";
|
|
11
11
|
import { signMacosDevelopmentBundle } from "./development-signing.mjs";
|
|
12
|
+
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
12
13
|
|
|
13
14
|
export const DEVELOPMENT_PROFILE = "release-iterate";
|
|
14
15
|
const RELEASE_ONLY_APPLE_ENV = ["APPLE_ID", "APPLE_PASSWORD", "APPLE_API_KEY", "APPLE_API_ISSUER", "APPLE_API_KEY_PATH", "APPLE_CERTIFICATE", "APPLE_CERTIFICATE_PASSWORD"];
|
|
@@ -60,7 +61,8 @@ export function validateDevelopmentConfig(config, platform) {
|
|
|
60
61
|
return target;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync
|
|
64
|
+
export function runDevelopment({ root, platform, config, buildOnly = false, dryRun = false, env = process.env, run = spawnSync,
|
|
65
|
+
codesignEnvironment = macosCodesignEnvironment }) {
|
|
64
66
|
if (!new Set(["win", "mac"]).has(platform)) throw new Error("--platform must be win or mac");
|
|
65
67
|
assertNativeDevelopmentHost(platform);
|
|
66
68
|
const target = validateDevelopmentConfig(config, platform);
|
|
@@ -68,7 +70,7 @@ export function runDevelopment({ root, platform, config, buildOnly = false, dryR
|
|
|
68
70
|
assertPrimaryReleaseCheckout(repoRoot, env);
|
|
69
71
|
if (dryRun) return { built: false, installed: false, plan: { build: target.build, install: buildOnly ? null : target.install ?? null, profile: DEVELOPMENT_PROFILE } };
|
|
70
72
|
assertReleaseExecutionAllowed({ cwd: repoRoot, environment: env, command: `right-release dev --platform ${platform}` });
|
|
71
|
-
const childEnv = developmentEnvironment(env);
|
|
73
|
+
const childEnv = codesignEnvironment(developmentEnvironment(env));
|
|
72
74
|
// One lease spans every managed request this run makes, so a broker cutover cannot land between them.
|
|
73
75
|
const lease = acquireManagedRunLease({ command: `right-release dev --platform ${platform}`, env });
|
|
74
76
|
try {
|
package/macos-notarization.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
6
|
+
import { detectMacSigningSession, runMacGuiNotarization } from "./macos-signing-session.mjs";
|
|
7
|
+
import { notarytoolAuthArgs } from "./notary-auth.mjs";
|
|
6
8
|
|
|
7
9
|
function checkedRun(args, run) {
|
|
8
10
|
const result = run("xcrun", args, { encoding: "utf8" });
|
|
@@ -10,9 +12,13 @@ function checkedRun(args, run) {
|
|
|
10
12
|
return result.stdout ?? "";
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export function stapleMacosArtifact(file,
|
|
15
|
+
export function stapleMacosArtifact(file, options = {}) {
|
|
16
|
+
const { run = spawnSync, env = process.env } = options;
|
|
14
17
|
file = path.resolve(file);
|
|
15
18
|
if (!/\.(dmg|pkg|app)$/i.test(file)) throw new Error(`not a stapleable macOS artifact: ${file}`);
|
|
19
|
+
if (detectMacSigningSession({ env, run }).guiRunner) {
|
|
20
|
+
return { ...runMacGuiNotarization({ ...options, kind: "staple", file }).stapling[0], file };
|
|
21
|
+
}
|
|
16
22
|
const probe = run("xcrun", ["stapler", "validate", file], { encoding: "utf8" });
|
|
17
23
|
if (probe.error) throw new Error(`cannot validate macOS staple: ${probe.error.message}`);
|
|
18
24
|
const alreadyStapled = probe.status === 0;
|
|
@@ -27,7 +33,7 @@ function fileEvidence(file) {
|
|
|
27
33
|
|
|
28
34
|
// DMG/PKG receive their own ticket. For a separately submitted app ZIP,
|
|
29
35
|
// target.notarize.app (CLI --app) identifies the bundle to staple before packaging.
|
|
30
|
-
export function notarizeMacosArtifact({ archive, receiptPath, authArgs, app, run = spawnSync }) {
|
|
36
|
+
export function notarizeMacosArtifact({ archive, receiptPath, authArgs, profile, app, run = spawnSync, env = process.env, ...agentOptions }) {
|
|
31
37
|
archive = path.resolve(archive);
|
|
32
38
|
const stapleTargets = /\.(dmg|pkg)$/i.test(archive) ? [archive] : [];
|
|
33
39
|
if (app) {
|
|
@@ -36,11 +42,21 @@ export function notarizeMacosArtifact({ archive, receiptPath, authArgs, app, run
|
|
|
36
42
|
stapleTargets.push(bundle);
|
|
37
43
|
}
|
|
38
44
|
if (!stapleTargets.length) throw new Error("notarization archives require notarize.app (or --app) for stapling their app bundle");
|
|
39
|
-
let result;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
let result, stapling;
|
|
46
|
+
if (detectMacSigningSession({ env, run }).guiRunner) {
|
|
47
|
+
// SSH ignores API credential environment/argv entirely. Only profile name
|
|
48
|
+
// reaches Aqua, where Keychain authentication already works.
|
|
49
|
+
const selectedProfile = profile ?? (authArgs?.[0] === "--keychain-profile" && authArgs.length === 2 ? authArgs[1] : undefined);
|
|
50
|
+
const report = runMacGuiNotarization({ ...agentOptions, kind: "notarize", file: archive, app, profile: selectedProfile, env, run });
|
|
51
|
+
result = report.notarization;
|
|
52
|
+
stapling = report.stapling.map((item) => ({ ...item, file: stapleTargets.find((file) => realpathSync.native(file) === item.file) ?? item.file }));
|
|
53
|
+
} else {
|
|
54
|
+
authArgs ??= notarytoolAuthArgs({ profile, env });
|
|
55
|
+
try { result = JSON.parse(checkedRun(["notarytool", "submit", archive, "--wait", "--output-format", "json", ...authArgs], run)); }
|
|
56
|
+
catch (error) { throw new Error(`macOS notarization failed for ${archive}: ${error.message}`); }
|
|
57
|
+
if (result.status !== "Accepted") throw new Error(`notarization was not accepted for ${archive}: ${result.status ?? "missing status"}`);
|
|
58
|
+
stapling = [...new Set(stapleTargets)].map((file) => stapleMacosArtifact(file, { run, env }));
|
|
59
|
+
}
|
|
44
60
|
// Stapling changes container bytes. Record final bytes, never the submission digest.
|
|
45
61
|
const receipt = {
|
|
46
62
|
schema: 1,
|
|
@@ -3,9 +3,9 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
|
|
6
|
-
import { requestSigningAgentSync } from "./signing-agent-client.mjs";
|
|
6
|
+
import { requestSigningAgentSync, validateNotaryAgentReport } from "./signing-agent-client.mjs";
|
|
7
7
|
import { installSigningAgent, requireMacGuiSession } from "./signing-agent-install.mjs";
|
|
8
|
-
import { signingAgentPaths, defaultSigningRoots, ensurePrivateDirectory, privatePath, within } from "./rightkit-signing-agent.mjs";
|
|
8
|
+
import { signingAgentPaths, defaultSigningRoots, ensurePrivateDirectory, privatePath, within, NOTARIZATION_TIMEOUT_MS, MAX_NOTARIZATION_TIMEOUT_MS } from "./rightkit-signing-agent.mjs";
|
|
9
9
|
|
|
10
10
|
export class MacSigningSessionError extends Error {
|
|
11
11
|
constructor(code, message) {
|
|
@@ -126,6 +126,51 @@ export function runMacCodesignCommands(commands, options = {}) {
|
|
|
126
126
|
return results;
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
export function runMacGuiNotarization({ kind = "notarize", file, profile = "apple-dev-notary", app, validateOnly,
|
|
130
|
+
cwd = process.cwd(), env = process.env, run = spawnSync, io = fs, home = os.userInfo().homedir,
|
|
131
|
+
uid = process.getuid?.(), username = os.userInfo().username, allowedRoots, timeoutMs = NOTARIZATION_TIMEOUT_MS,
|
|
132
|
+
request = requestSigningAgentSync, install = installSigningAgent } = {}) {
|
|
133
|
+
if (!["notarize", "staple"].includes(kind) || !Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_NOTARIZATION_TIMEOUT_MS) throw new TypeError("Invalid notarization request or timeout");
|
|
134
|
+
if (kind === "notarize" && (typeof profile !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_. -]{0,127}$/.test(profile))) throw new TypeError("Notarization requires a profile name only");
|
|
135
|
+
requireMacGuiSession({ run, uid });
|
|
136
|
+
const paths = signingAgentPaths(home);
|
|
137
|
+
let roots = allowedRoots ?? defaultSigningRoots(home, env.RIGHTKIT_MANAGED_ROOT);
|
|
138
|
+
if (!allowedRoots && io.existsSync(paths.config)) {
|
|
139
|
+
privatePath(paths.config, 0o600, { io, uid });
|
|
140
|
+
roots = JSON.parse(io.readFileSync(paths.config, "utf8")).roots;
|
|
141
|
+
}
|
|
142
|
+
const plan = { schema: 1, type: kind, path: path.resolve(file), cwd: path.resolve(cwd),
|
|
143
|
+
allowedRoots: roots.filter((root) => io.existsSync(root)), timeoutMs,
|
|
144
|
+
...(kind === "notarize" ? { profile, staple: true, ...(app ? { app: path.resolve(app) } : {}) } : { validateOnly: validateOnly === true }) };
|
|
145
|
+
const options = { home, timeoutMs: timeoutMs + 5000 };
|
|
146
|
+
let report;
|
|
147
|
+
try { report = request(plan, options); }
|
|
148
|
+
catch (error) {
|
|
149
|
+
let missing = error.code === "MAC_SIGNING_AGENT_UNAVAILABLE";
|
|
150
|
+
// Upgrade earlier installed agents once; never turn a rejected path or
|
|
151
|
+
// malformed request into an install/retry loop.
|
|
152
|
+
if (error.code === "MAC_SIGNING_AGENT_PROTOCOL") {
|
|
153
|
+
const health = request({ schema: 1, type: "health" }, { home, timeoutMs: 1500 });
|
|
154
|
+
missing = !health.capabilities?.includes(kind);
|
|
155
|
+
}
|
|
156
|
+
if (!missing) throw error;
|
|
157
|
+
install({ home, username, uid, run, io, managedRoot: env.RIGHTKIT_MANAGED_ROOT });
|
|
158
|
+
report = request(plan, options);
|
|
159
|
+
}
|
|
160
|
+
validateNotaryAgentReport(report);
|
|
161
|
+
const targets = kind === "staple" ? [plan.path] : [...new Set([...( /\.(dmg|pkg)$/i.test(plan.path) ? [plan.path] : []), ...(plan.app ? [plan.app] : [])])];
|
|
162
|
+
const canonical = (value) => (io.realpathSync.native ?? io.realpathSync)(value);
|
|
163
|
+
if (!report.ok || (kind === "notarize" && (report.notarization.status !== "Accepted" || !report.notarization.id
|
|
164
|
+
|| !report.steps.some((step) => step.step === "submit" && step.exitCode === 0)))
|
|
165
|
+
|| report.stapling.length !== targets.length || targets.some((target) => !report.stapling.some((item) => item.file === canonical(target)
|
|
166
|
+
&& report.steps.some((step) => step.step === "validate" && step.file === item.file && step.exitCode === 0)
|
|
167
|
+
&& (kind !== "notarize" || report.steps.some((step) => step.step === "staple" && step.file === item.file && step.exitCode === 0))))
|
|
168
|
+
|| report.steps.some((step) => step.step !== "probe" && step.exitCode !== 0)) {
|
|
169
|
+
throw new MacSigningSessionError("MAC_SIGNING_AGENT_PROTOCOL", "Signing agent returned incomplete notarization or stapling evidence");
|
|
170
|
+
}
|
|
171
|
+
return report;
|
|
172
|
+
}
|
|
173
|
+
|
|
129
174
|
export function signMacUpdaterFiles(files, options = {}) {
|
|
130
175
|
const { cwd = process.cwd(), env = process.env, run = spawnSync, username = os.userInfo().username } = options;
|
|
131
176
|
const commands = files.map((file) => ["exec", "tauri", "signer", "sign", file]);
|
package/notarize-macos.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { notarytoolAuthArgs } from "./notary-auth.mjs";
|
|
5
4
|
import { notarizeMacosArtifact } from "./macos-notarization.mjs";
|
|
6
5
|
|
|
7
6
|
const args = process.argv.slice(2);
|
|
@@ -24,10 +23,11 @@ if (dryRun) {
|
|
|
24
23
|
if (process.platform !== "darwin") fail("macOS notarization must run on macOS");
|
|
25
24
|
if (!existsSync(archive)) fail(`missing archive: ${archive}`);
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
try { notarizeMacosArtifact({ archive, receiptPath, profile, app }); }
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.report) console.error(JSON.stringify(error.report));
|
|
29
|
+
fail(`${error.code ? `[${error.code}] ` : ""}${error.message}`);
|
|
30
|
+
}
|
|
31
31
|
|
|
32
32
|
function fail(message) {
|
|
33
33
|
console.error(`right-notarize-macos: ${message}`);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"sha256": "8d12e39ee6bac3bf8332df8dc61c74637e2eda6becbcc39746fe1ed5a1fe7ef6"
|
|
6
6
|
},
|
|
7
7
|
"macos": {
|
|
8
|
-
"path": "/Users/adrdsouza/.rightkit-managed/ownership-approval/macos/content-
|
|
9
|
-
"sha256": "
|
|
8
|
+
"path": "/Users/adrdsouza/.rightkit-managed/ownership-approval/macos/content-2c30864bdb666a1ca1e4388ea7a3bc83cb985196d666b96cc30e1da7cd5b0e6b/RightKitOwnershipApproval.app/Contents/MacOS/ownership-approval",
|
|
9
|
+
"sha256": "2c30864bdb666a1ca1e4388ea7a3bc83cb985196d666b96cc30e1da7cd5b0e6b"
|
|
10
10
|
}
|
|
11
11
|
}
|
package/package.json
CHANGED
package/release.mjs
CHANGED
|
@@ -28,6 +28,7 @@ import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
|
28
28
|
import { runConfiguredChecks } from "./release-checks.mjs";
|
|
29
29
|
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
30
30
|
import { DEFAULT_STORAGE_THRESHOLD_BYTES, findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
|
|
31
|
+
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
31
32
|
|
|
32
33
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
33
34
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
@@ -962,7 +963,7 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
962
963
|
const useShell = process.platform === "win32" && !/\.(?:exe|com)$/i.test(cmd);
|
|
963
964
|
const child = spawn(cmd, runArgs, {
|
|
964
965
|
cwd,
|
|
965
|
-
env: { ...process.env, ...releaseEnv, ...env },
|
|
966
|
+
env: macosCodesignEnvironment({ ...process.env, ...releaseEnv, ...env }),
|
|
966
967
|
stdio: options.captureStdout ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
967
968
|
shell: useShell,
|
|
968
969
|
windowsHide: true,
|
|
@@ -5,10 +5,13 @@ import os from "node:os";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { randomBytes, timingSafeEqual, randomUUID } from "node:crypto";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
-
import { spawnSync } from "./background-process.mjs";
|
|
8
|
+
import { spawn, spawnSync } from "./background-process.mjs";
|
|
9
|
+
import { parseCodesignArgv } from "./codesign-argv.mjs";
|
|
9
10
|
|
|
10
11
|
export const AGENT_LABEL = "dev.rightkit.signing-agent";
|
|
11
12
|
export const MAX_FRAME = 1024 * 1024;
|
|
13
|
+
export const NOTARIZATION_TIMEOUT_MS = 1800000;
|
|
14
|
+
export const MAX_NOTARIZATION_TIMEOUT_MS = 7200000;
|
|
12
15
|
export class SigningAgentError extends Error {
|
|
13
16
|
constructor(code, message) { super(message); this.name = "SigningAgentError"; this.code = code; }
|
|
14
17
|
}
|
|
@@ -19,8 +22,9 @@ export function signingAgentPaths(home = os.userInfo().homedir) {
|
|
|
19
22
|
config: path.join(directory, "config.json"), runtime: path.join(directory, "runtime"),
|
|
20
23
|
log: path.join(directory, "requests.jsonl"), plist: path.join(home, "Library/LaunchAgents", `${AGENT_LABEL}.plist`) };
|
|
21
24
|
}
|
|
22
|
-
export function defaultSigningRoots(home = os.userInfo().homedir, managedRoot = "/Volumes/D/.rightkit-managed") {
|
|
23
|
-
return ["/Volumes/D/claude", managedRoot, "/Volumes/D/rightkit-build-control",
|
|
25
|
+
export function defaultSigningRoots(home = os.userInfo().homedir, managedRoot = "/Volumes/D/.rightkit-managed", cargoTargetDirs = []) {
|
|
26
|
+
return [...new Set(["/Volumes/D/claude", managedRoot, "/Volumes/D/rightkit-build-control",
|
|
27
|
+
path.join(home, "Library/Caches/RightSuite/release"), "/tmp", "/private/tmp", os.tmpdir(), ...cargoTargetDirs])];
|
|
24
28
|
}
|
|
25
29
|
export const within = (root, file) => file === root || file.startsWith(`${root}${path.sep}`);
|
|
26
30
|
export function privatePath(file, mode, { io = fs, uid = process.getuid?.(), kind = "file" } = {}) {
|
|
@@ -55,15 +59,19 @@ function absolute(value) {
|
|
|
55
59
|
return path.resolve(value);
|
|
56
60
|
}
|
|
57
61
|
export function validateSigningRequest(request, { roots, stagingRoot, io = fs } = {}) {
|
|
62
|
+
// Node's JS realpath normalizes .. before following links. Native realpath
|
|
63
|
+
// follows the same path traversal as codesign & catches link/.. escapes.
|
|
64
|
+
const realpath = io.realpathSync.native ?? io.realpathSync;
|
|
58
65
|
const common = ["schema", "token", "type", "allowedRoots", "cwd", "timeoutMs"];
|
|
59
66
|
if (request?.type === "health") {
|
|
60
67
|
shape(request, ["schema", "token", "type"]);
|
|
61
68
|
if (request.schema !== 1) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported protocol schema");
|
|
62
69
|
return request;
|
|
63
70
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
const notary = ["notarize", "staple"].includes(request?.type);
|
|
72
|
+
if (!["codesign", "codesign-argv", "updater", "notarize", "staple"].includes(request?.type)) fail("MAC_SIGNING_AGENT_PROTOCOL", "Unsupported signing request type");
|
|
73
|
+
shape(request, [...common, ...(notary ? ["path", "profile", "staple", "app", "validateOnly"] : request.type === "codesign-argv" ? ["argv", "targets"] : [request.type === "codesign" ? "items" : "files"])]);
|
|
74
|
+
if (request.schema !== 1 || !Number.isInteger(request.timeoutMs) || request.timeoutMs < 1 || request.timeoutMs > (notary ? MAX_NOTARIZATION_TIMEOUT_MS : 180000)) {
|
|
67
75
|
fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid schema or signing deadline");
|
|
68
76
|
}
|
|
69
77
|
if (!Array.isArray(request.allowedRoots) || !request.allowedRoots.length || request.allowedRoots.length > 32) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed roots required");
|
|
@@ -73,15 +81,16 @@ export function validateSigningRequest(request, { roots, stagingRoot, io = fs }
|
|
|
73
81
|
const allowed = request.allowedRoots.map((root) => {
|
|
74
82
|
const lexical = absolute(root);
|
|
75
83
|
if (![...installed, stagingRoot].filter(Boolean).some((base) => within(base, lexical))) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Caller root is outside installed signing policy");
|
|
76
|
-
const canonical =
|
|
84
|
+
const canonical = realpath(root);
|
|
77
85
|
if (![...installed, stagingRoot].filter(Boolean).some((base) => {
|
|
78
|
-
try { return within(
|
|
86
|
+
try { return within(realpath(base), canonical); } catch { return false; }
|
|
79
87
|
})) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Allowed root escapes installed policy");
|
|
80
88
|
return { lexical, canonical };
|
|
81
89
|
});
|
|
82
90
|
function checked(file, { directory = false, entitlements = false } = {}) {
|
|
83
91
|
const lexical = absolute(file);
|
|
84
|
-
|
|
92
|
+
let canonical;
|
|
93
|
+
try { canonical = realpath(file); } catch { fail("MAC_SIGNING_AGENT_PATH_DENIED", "Signing path is unavailable"); }
|
|
85
94
|
if (!allowed.some((root) => within(root.lexical, lexical) && within(root.canonical, canonical))
|
|
86
95
|
|| (!entitlements && stagingRoot && within(stagingRoot, canonical))) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Signing path escapes allowed roots");
|
|
87
96
|
const info = io.statSync(canonical);
|
|
@@ -89,6 +98,33 @@ export function validateSigningRequest(request, { roots, stagingRoot, io = fs }
|
|
|
89
98
|
return canonical;
|
|
90
99
|
}
|
|
91
100
|
const cwd = checked(request.cwd, { directory: true });
|
|
101
|
+
if (notary) {
|
|
102
|
+
const file = checked(request.path);
|
|
103
|
+
if (request.type === "staple") {
|
|
104
|
+
if (request.profile !== undefined || request.app !== undefined || request.staple !== undefined
|
|
105
|
+
|| (request.validateOnly !== undefined && typeof request.validateOnly !== "boolean")) fail("MAC_SIGNING_AGENT_PROTOCOL", "Invalid stapling options");
|
|
106
|
+
if (!/\.(dmg|pkg|app)$/i.test(file) || (/\.app$/i.test(file) ? !io.statSync(file).isDirectory() : !io.statSync(file).isFile())) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Invalid staple target");
|
|
107
|
+
return { ...request, cwd, path: file };
|
|
108
|
+
}
|
|
109
|
+
const profile = request.profile ?? "apple-dev-notary";
|
|
110
|
+
if (typeof profile !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_. -]{0,127}$/.test(profile)
|
|
111
|
+
|| request.staple !== true || request.validateOnly !== undefined) fail("MAC_SIGNING_AGENT_PROTOCOL", "Notarization requires a profile name & staple: true");
|
|
112
|
+
if (!/\.(dmg|zip|pkg)$/i.test(file) || !io.statSync(file).isFile()) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Notarization requires DMG, ZIP, or PKG");
|
|
113
|
+
const app = request.app === undefined ? undefined : checked(request.app, { directory: true });
|
|
114
|
+
if (app && !/\.app$/i.test(app)) fail("MAC_SIGNING_AGENT_PATH_DENIED", "Stapling app must be an app bundle");
|
|
115
|
+
if (/\.zip$/i.test(file) && !app) fail("MAC_SIGNING_AGENT_PROTOCOL", "ZIP submission requires app bundle for stapling");
|
|
116
|
+
return { ...request, cwd, path: file, profile, ...(app ? { app } : {}) };
|
|
117
|
+
}
|
|
118
|
+
if (request.type === "codesign-argv") {
|
|
119
|
+
const parsed = parseCodesignArgv(request.argv, request.cwd);
|
|
120
|
+
if (!Array.isArray(request.targets) || request.targets.length !== parsed.targets.length
|
|
121
|
+
|| request.targets.some((target, index) => target !== parsed.targets[index])) fail("MAC_SIGNING_AGENT_PROTOCOL", "Codesign target paths do not match argv");
|
|
122
|
+
for (const target of parsed.targets) checked(target);
|
|
123
|
+
for (const file of parsed.entitlements) checked(file, { entitlements: true });
|
|
124
|
+
// Preserve argv & cwd spelling: relative arguments must resolve exactly as
|
|
125
|
+
// they would for the original caller, including symlink/.. semantics.
|
|
126
|
+
return { ...request };
|
|
127
|
+
}
|
|
92
128
|
if (request.type === "updater") {
|
|
93
129
|
if (!Array.isArray(request.files) || !request.files.length || request.files.length > 4096) fail("MAC_SIGNING_AGENT_PROTOCOL", "Updater files required");
|
|
94
130
|
const files = request.files.map((file) => {
|
|
@@ -119,9 +155,17 @@ export function authenticateSigningToken(candidate, token) {
|
|
|
119
155
|
}
|
|
120
156
|
export function executeSigningRequest(request, { run = spawnSync, home = os.userInfo().homedir,
|
|
121
157
|
username = os.userInfo().username, pnpm, environmentPath, now = Date.now } = {}) {
|
|
122
|
-
if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid };
|
|
158
|
+
if (request.type === "health") return { schema: 1, ok: true, service: AGENT_LABEL, pid: process.pid, capabilities: ["codesign", "codesign-argv", "updater", "notarize", "staple"] };
|
|
123
159
|
const deadline = now() + request.timeoutMs;
|
|
124
160
|
const env = { HOME: home, PATH: environmentPath, TMPDIR: path.join(signingAgentPaths(home).directory, "tmp") };
|
|
161
|
+
if (request.type === "codesign-argv") {
|
|
162
|
+
const r = run("/usr/bin/codesign", request.argv, { cwd: request.cwd, env, encoding: "utf8", stdio: "pipe", shell: false,
|
|
163
|
+
windowsHide: true, timeout: request.timeoutMs, killSignal: "SIGKILL", maxBuffer: MAX_FRAME });
|
|
164
|
+
if (r.error || !Number.isInteger(r.status)) fail(r.error?.code === "ETIMEDOUT" ? "MAC_SIGNING_AGENT_TIMEOUT" : "MAC_SIGNING_AGENT_JOB_FAILED", "Codesign execution failed");
|
|
165
|
+
const report = { schema: 1, ok: true, results: [{ status: r.status, stdout: String(r.stdout ?? ""), stderr: String(r.stderr ?? "") }] };
|
|
166
|
+
if (Buffer.byteLength(`${JSON.stringify(report)}\n`) > MAX_FRAME) fail("MAC_SIGNING_AGENT_PROTOCOL", "Codesign output exceeds protocol limit");
|
|
167
|
+
return report;
|
|
168
|
+
}
|
|
125
169
|
const results = [];
|
|
126
170
|
const outputLimit = Math.min(4096, Math.floor(MAX_FRAME / (4 * (request.items?.length ?? request.files.length))));
|
|
127
171
|
const invoke = (command, args, sensitive = false) => {
|
|
@@ -158,6 +202,92 @@ export function executeSigningRequest(request, { run = spawnSync, home = os.user
|
|
|
158
202
|
throw error;
|
|
159
203
|
} finally { delete env.TAURI_SIGNING_PRIVATE_KEY; delete env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD; }
|
|
160
204
|
}
|
|
205
|
+
// No notarytool/stapler output is forwarded verbatim: diagnostics can contain
|
|
206
|
+
// account details. Only typed status, submission UUID, counts & issue codes leave Aqua.
|
|
207
|
+
export async function executeNotarizationRequest(request, { launch = spawn, home = os.userInfo().homedir,
|
|
208
|
+
environmentPath = "/usr/bin:/bin", now = Date.now, id = randomUUID(), progress = () => {}, signal } = {}) {
|
|
209
|
+
const deadline = now() + request.timeoutMs;
|
|
210
|
+
const env = { HOME: home, PATH: environmentPath, TMPDIR: path.join(signingAgentPaths(home).directory, "tmp") };
|
|
211
|
+
const report = { schema: 1, ok: false, id, notarization: { id: null, status: "Unknown", logSummary: null }, steps: [], stapling: [] };
|
|
212
|
+
const invoke = async (step, args, file) => {
|
|
213
|
+
progress({ schema: 1, event: "status", id, step });
|
|
214
|
+
const remaining = deadline - now();
|
|
215
|
+
if (remaining <= 0 || signal?.aborted) throw new SigningAgentError("MAC_SIGNING_AGENT_TIMEOUT", "Notarization deadline expired");
|
|
216
|
+
const result = await new Promise((resolve) => {
|
|
217
|
+
let child, output = "", bytes = 0, timedOut = false, overflow = false, failed = false;
|
|
218
|
+
let timer;
|
|
219
|
+
const kill = () => { try { child?.kill("SIGKILL"); } catch {} };
|
|
220
|
+
const cancel = () => { timedOut = true; kill(); };
|
|
221
|
+
const finish = (exitCode) => {
|
|
222
|
+
clearTimeout(timer); signal?.removeEventListener("abort", cancel);
|
|
223
|
+
resolve({ exitCode: Number.isInteger(exitCode) ? exitCode : null, output, timedOut, overflow, failed });
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
child = launch("/usr/bin/xcrun", args, { cwd: request.cwd, env, stdio: ["ignore", "pipe", "pipe"], shell: false });
|
|
227
|
+
timer = setTimeout(cancel, remaining);
|
|
228
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
229
|
+
child.stdout.on("data", (chunk) => {
|
|
230
|
+
bytes += chunk.length;
|
|
231
|
+
if (bytes > MAX_FRAME) { overflow = true; kill(); } else output += chunk.toString();
|
|
232
|
+
});
|
|
233
|
+
child.stderr.on("data", () => {}); // Drain, never disclose.
|
|
234
|
+
child.once("error", () => { failed = true; finish(null); });
|
|
235
|
+
child.once("close", finish);
|
|
236
|
+
} catch { failed = true; finish(null); }
|
|
237
|
+
});
|
|
238
|
+
report.steps.push({ step, ...(file ? { file } : {}), exitCode: result.exitCode });
|
|
239
|
+
if (result.timedOut) throw new SigningAgentError("MAC_SIGNING_AGENT_TIMEOUT", "Notarization deadline expired");
|
|
240
|
+
if (result.failed || result.overflow) throw new SigningAgentError("MAC_SIGNING_AGENT_JOB_FAILED", `${step} execution failed`);
|
|
241
|
+
return result;
|
|
242
|
+
};
|
|
243
|
+
const parse = (output) => { try { return JSON.parse(output); } catch { return {}; } };
|
|
244
|
+
try {
|
|
245
|
+
if (request.type === "notarize") {
|
|
246
|
+
const submitted = await invoke("submit", ["notarytool", "submit", request.path, "--keychain-profile", request.profile ?? "apple-dev-notary", "--wait", "--output-format", "json"]);
|
|
247
|
+
const result = parse(submitted.output);
|
|
248
|
+
const submissionId = result.id ?? result.submissionId;
|
|
249
|
+
report.notarization.id = typeof submissionId === "string" && /^[a-f0-9-]{36}$/i.test(submissionId) ? submissionId : null;
|
|
250
|
+
report.notarization.status = ["Accepted", "Invalid", "In Progress", "Rejected"].includes(result.status) ? result.status : "Unknown";
|
|
251
|
+
if (report.notarization.status === "Invalid" && report.notarization.id) {
|
|
252
|
+
const logged = await invoke("log", ["notarytool", "log", report.notarization.id, "--keychain-profile", request.profile ?? "apple-dev-notary", "--output-format", "json"]);
|
|
253
|
+
if (logged.exitCode === 0) {
|
|
254
|
+
const log = parse(logged.output);
|
|
255
|
+
const issues = Array.isArray(log.issues) ? log.issues : [];
|
|
256
|
+
report.notarization.logSummary = { statusCode: Number.isInteger(log.statusCode) ? log.statusCode : null,
|
|
257
|
+
errors: issues.filter((issue) => issue.severity === "error").length,
|
|
258
|
+
warnings: issues.filter((issue) => issue.severity === "warning").length,
|
|
259
|
+
issues: issues.slice(0, 100).map((issue) => ({ severity: ["error", "warning"].includes(issue.severity) ? issue.severity : "unknown",
|
|
260
|
+
code: Number.isInteger(issue.code) ? issue.code : null })) };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (submitted.exitCode !== 0 || report.notarization.status !== "Accepted" || !report.notarization.id) {
|
|
264
|
+
throw new SigningAgentError("MAC_SIGNING_NOTARIZATION_FAILED", `Notarization ${report.notarization.status}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const targets = request.type === "staple" ? [request.path]
|
|
268
|
+
: [...new Set([...( /\.(dmg|pkg)$/i.test(request.path) ? [request.path] : []), ...(request.app ? [request.app] : [])])];
|
|
269
|
+
for (const file of targets) {
|
|
270
|
+
let alreadyStapled = false;
|
|
271
|
+
if (request.type === "staple" && !request.validateOnly) {
|
|
272
|
+
const probe = await invoke("probe", ["stapler", "validate", file], file);
|
|
273
|
+
alreadyStapled = probe.exitCode === 0;
|
|
274
|
+
}
|
|
275
|
+
if (!request.validateOnly && !alreadyStapled) {
|
|
276
|
+
const stapled = await invoke("staple", ["stapler", "staple", file], file);
|
|
277
|
+
if (stapled.exitCode !== 0) throw new SigningAgentError("MAC_SIGNING_STAPLE_FAILED", "Stapling failed");
|
|
278
|
+
}
|
|
279
|
+
const validated = await invoke("validate", ["stapler", "validate", file], file);
|
|
280
|
+
if (validated.exitCode !== 0) throw new SigningAgentError("MAC_SIGNING_STAPLE_FAILED", "Staple validation failed");
|
|
281
|
+
report.stapling.push({ file, alreadyStapled, validated: true });
|
|
282
|
+
}
|
|
283
|
+
report.ok = true;
|
|
284
|
+
} catch (error) {
|
|
285
|
+
report.code = error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_JOB_FAILED";
|
|
286
|
+
report.message = report.code === "MAC_SIGNING_AGENT_TIMEOUT" ? "Notarization deadline expired" : "Notarization or stapling failed inside signing agent";
|
|
287
|
+
}
|
|
288
|
+
return report;
|
|
289
|
+
}
|
|
290
|
+
|
|
161
291
|
export function managedSigningLog(file, event, { io = fs } = {}) {
|
|
162
292
|
if (io.existsSync(file)) {
|
|
163
293
|
privatePath(file, 0o600, { io });
|
|
@@ -170,7 +300,7 @@ export function managedSigningLog(file, event, { io = fs } = {}) {
|
|
|
170
300
|
// Only service-authored metadata is logged, never request/token/key/argv/output.
|
|
171
301
|
io.appendFileSync(file, `${JSON.stringify({ at: new Date().toISOString(), ...event })}\n`, { mode: 0o600 });
|
|
172
302
|
}
|
|
173
|
-
export async function startSigningAgent({ paths = signingAgentPaths(), config, io = fs, run = spawnSync,
|
|
303
|
+
export async function startSigningAgent({ paths = signingAgentPaths(), config, io = fs, run = spawnSync, launch = spawn, heartbeatMs = 10000,
|
|
174
304
|
log = (event) => managedSigningLog(paths.log, event), createServer = net.createServer } = {}) {
|
|
175
305
|
privatePath(paths.directory, 0o700, { io, kind: "directory" });
|
|
176
306
|
privatePath(paths.config, 0o600, { io });
|
|
@@ -211,9 +341,14 @@ export async function startSigningAgent({ paths = signingAgentPaths(), config, i
|
|
|
211
341
|
return;
|
|
212
342
|
}
|
|
213
343
|
const receivedAt = Date.now();
|
|
344
|
+
const notary = ["notarize", "staple"].includes(request.type);
|
|
345
|
+
const controller = new AbortController();
|
|
346
|
+
const progress = (frame) => { if (!socket.destroyed) socket.write(`${JSON.stringify(frame)}\n`); };
|
|
347
|
+
const heartbeat = notary ? setInterval(() => progress({ schema: 1, event: "heartbeat", id }), heartbeatMs) : null;
|
|
348
|
+
if (notary) socket.on("close", () => { clearInterval(heartbeat); controller.abort(); });
|
|
214
349
|
pending++;
|
|
215
|
-
log({ event: "request.accepted", id, type: request.type, count: request.items?.length ?? request.files?.length ?? 0 });
|
|
216
|
-
queue = queue.then(() => {
|
|
350
|
+
log({ event: "request.accepted", id, type: request.type, count: notary ? 1 : request.targets?.length ?? request.items?.length ?? request.files?.length ?? 0 });
|
|
351
|
+
queue = queue.then(async () => {
|
|
217
352
|
if (socket.destroyed) { log({ event: "request.cancelled", id, type: request.type }); return; }
|
|
218
353
|
log({ event: "request.started", id, type: request.type });
|
|
219
354
|
try {
|
|
@@ -223,15 +358,20 @@ export async function startSigningAgent({ paths = signingAgentPaths(), config, i
|
|
|
223
358
|
// Revalidate immediately before effects, not merely on admission.
|
|
224
359
|
request = validateSigningRequest(request, { roots: config.roots, stagingRoot: path.join(paths.directory, "requests"), io });
|
|
225
360
|
}
|
|
226
|
-
const report =
|
|
361
|
+
const report = notary
|
|
362
|
+
? await executeNotarizationRequest(request, { launch, home: config.home, environmentPath: config.environmentPath, id, progress, signal: controller.signal })
|
|
363
|
+
: executeSigningRequest(request, { run, home: config.home, username: config.username, pnpm: config.pnpm, environmentPath: config.environmentPath });
|
|
227
364
|
respond(report);
|
|
228
|
-
log({ event: "request.completed", id, type: request.type
|
|
365
|
+
log({ event: report.ok ? "request.completed" : "request.failed", id, type: request.type,
|
|
366
|
+
...(report.code ? { code: report.code } : {}),
|
|
367
|
+
...(notary ? { status: report.notarization.status, steps: report.steps.map(({ step, exitCode }) => ({ step, exitCode })) } : {}),
|
|
368
|
+
...(request.type === "codesign-argv" ? { status: report.results[0].status } : {}) });
|
|
229
369
|
} catch (error) {
|
|
230
370
|
const code = error.code?.startsWith("MAC_SIGNING_") ? error.code : "MAC_SIGNING_AGENT_JOB_FAILED";
|
|
231
|
-
respond({ schema: 1, ok: false, code, message: request.type === "updater" ? "Updater signing failed" : error.message });
|
|
371
|
+
respond({ schema: 1, ok: false, ...(notary ? { id } : {}), code, message: request.type === "updater" ? "Updater signing failed" : notary ? "Notarization job failed" : error.message });
|
|
232
372
|
log({ event: "request.failed", id, type: request.type, code });
|
|
233
373
|
}
|
|
234
|
-
}).finally(() => { pending--; }).catch(() => { socket.destroy(); });
|
|
374
|
+
}).finally(() => { clearInterval(heartbeat); pending--; }).catch(() => { socket.destroy(); });
|
|
235
375
|
});
|
|
236
376
|
});
|
|
237
377
|
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(paths.socket, resolve); });
|
package/rightkit-versions.json
CHANGED
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"@rightkit/onboarding": "0.2.3",
|
|
28
28
|
"@rightkit/pdf-ui": "0.1.2",
|
|
29
29
|
"@rightkit/platform-ui": "0.1.2",
|
|
30
|
-
"@rightkit/qa": "0.3.
|
|
30
|
+
"@rightkit/qa": "0.3.2",
|
|
31
31
|
"@rightkit/qa-chrome": "0.1.3",
|
|
32
|
-
"@rightkit/release": "0.2.
|
|
32
|
+
"@rightkit/release": "0.2.135",
|
|
33
33
|
"@rightkit/settings-ui": "0.2.7",
|
|
34
34
|
"@rightkit/shell": "0.1.0",
|
|
35
35
|
"@rightkit/tauri": "0.1.1",
|
|
@@ -157,7 +157,8 @@
|
|
|
157
157
|
"0.2.130",
|
|
158
158
|
"0.2.131",
|
|
159
159
|
"0.2.132",
|
|
160
|
-
"0.2.133"
|
|
160
|
+
"0.2.133",
|
|
161
|
+
"0.2.134"
|
|
161
162
|
],
|
|
162
163
|
"@rightkit/qa": [
|
|
163
164
|
"0.1.0",
|
|
@@ -166,7 +167,8 @@
|
|
|
166
167
|
"0.2.2",
|
|
167
168
|
"0.2.3",
|
|
168
169
|
"0.2.4",
|
|
169
|
-
"0.3.0"
|
|
170
|
+
"0.3.0",
|
|
171
|
+
"0.3.1"
|
|
170
172
|
],
|
|
171
173
|
"@rightkit/app-shell": [
|
|
172
174
|
"0.1.0",
|
|
@@ -240,7 +242,7 @@
|
|
|
240
242
|
"rightkit-automation": "0.1.0",
|
|
241
243
|
"rightkit-browser": "0.2.2",
|
|
242
244
|
"rightkit-cache": "0.1.0",
|
|
243
|
-
"rightkit-control": "0.1.
|
|
245
|
+
"rightkit-control": "0.1.9",
|
|
244
246
|
"rightkit-crash": "0.1.0",
|
|
245
247
|
"rightkit-db": "0.3.0",
|
|
246
248
|
"rightkit-embed": "0.2.2",
|
|
@@ -258,7 +260,7 @@
|
|
|
258
260
|
"rightkit-pdf": "0.1.0",
|
|
259
261
|
"rightkit-playback": "0.1.1",
|
|
260
262
|
"rightkit-process": "0.3.1",
|
|
261
|
-
"rightkit-qa": "0.2.
|
|
263
|
+
"rightkit-qa": "0.2.12",
|
|
262
264
|
"rightkit-qts": "0.2.1",
|
|
263
265
|
"rightkit-qts-ggml": "0.1.0",
|
|
264
266
|
"rightkit-qts-ggml-sys": "0.1.0",
|
|
@@ -290,7 +292,7 @@
|
|
|
290
292
|
"rightkit-audio": "0.1.1",
|
|
291
293
|
"rightkit-browser": "0.2.2",
|
|
292
294
|
"rightkit-cache": "0.1.0",
|
|
293
|
-
"rightkit-control": "0.1.
|
|
295
|
+
"rightkit-control": "0.1.9",
|
|
294
296
|
"rightkit-crash": "0.1.0",
|
|
295
297
|
"rightkit-db": "0.3.0",
|
|
296
298
|
"rightkit-embed": "0.2.2",
|
|
@@ -308,7 +310,7 @@
|
|
|
308
310
|
"rightkit-pdf": "0.1.0",
|
|
309
311
|
"rightkit-playback": "0.1.1",
|
|
310
312
|
"rightkit-process": "0.3.1",
|
|
311
|
-
"rightkit-qa": "0.2.
|
|
313
|
+
"rightkit-qa": "0.2.12",
|
|
312
314
|
"rightkit-qts": "0.2.1",
|
|
313
315
|
"rightkit-qts-ggml": "0.1.0",
|
|
314
316
|
"rightkit-qts-ggml-sys": "0.1.0",
|
|
@@ -337,9 +339,9 @@
|
|
|
337
339
|
"@rightkit/onboarding": "0.2.3",
|
|
338
340
|
"@rightkit/pdf-ui": "0.1.2",
|
|
339
341
|
"@rightkit/platform-ui": "0.1.2",
|
|
340
|
-
"@rightkit/qa": "0.3.
|
|
342
|
+
"@rightkit/qa": "0.3.2",
|
|
341
343
|
"@rightkit/qa-chrome": "0.1.3",
|
|
342
|
-
"@rightkit/release": "0.2.
|
|
344
|
+
"@rightkit/release": "0.2.134",
|
|
343
345
|
"@rightkit/settings-ui": "0.2.7",
|
|
344
346
|
"@rightkit/shell": "0.1.0",
|
|
345
347
|
"@rightkit/tauri": "0.1.1",
|
|
@@ -392,7 +394,8 @@
|
|
|
392
394
|
"0.2.7",
|
|
393
395
|
"0.2.8",
|
|
394
396
|
"0.2.9",
|
|
395
|
-
"0.2.10"
|
|
397
|
+
"0.2.10",
|
|
398
|
+
"0.2.11"
|
|
396
399
|
],
|
|
397
400
|
"rightkit-sandbox": [
|
|
398
401
|
"0.1.0",
|
|
@@ -416,7 +419,8 @@
|
|
|
416
419
|
],
|
|
417
420
|
"rightkit-control": [
|
|
418
421
|
"0.1.6",
|
|
419
|
-
"0.1.7"
|
|
422
|
+
"0.1.7",
|
|
423
|
+
"0.1.8"
|
|
420
424
|
]
|
|
421
425
|
}
|
|
422
426
|
}
|
package/signing-agent-client.mjs
CHANGED
|
@@ -6,7 +6,34 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { spawnSync } from "./background-process.mjs";
|
|
7
7
|
import { signingAgentPaths, privatePath, SigningAgentError, MAX_FRAME } from "./rightkit-signing-agent.mjs";
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export function validateNotaryAgentReport(report) {
|
|
10
|
+
const fields = (value, allowed) => value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every((key) => allowed.includes(key));
|
|
11
|
+
const uuid = (value) => typeof value === "string" && /^[a-f0-9-]{36}$/i.test(value);
|
|
12
|
+
const n = report?.notarization;
|
|
13
|
+
const summary = n?.logSummary;
|
|
14
|
+
if (!fields(report, ["schema", "ok", "id", "notarization", "steps", "stapling", "code", "message"])
|
|
15
|
+
|| report.schema !== 1 || typeof report.ok !== "boolean" || !uuid(report.id)
|
|
16
|
+
|| (report.code !== undefined && !["MAC_SIGNING_AGENT_TIMEOUT", "MAC_SIGNING_AGENT_JOB_FAILED", "MAC_SIGNING_NOTARIZATION_FAILED", "MAC_SIGNING_STAPLE_FAILED"].includes(report.code))
|
|
17
|
+
|| (report.message !== undefined && !["Notarization deadline expired", "Notarization or stapling failed inside signing agent"].includes(report.message))
|
|
18
|
+
|| !fields(n, ["id", "status", "logSummary"]) || !(n.id === null || uuid(n.id))
|
|
19
|
+
|| !["Accepted", "Invalid", "Unknown", "In Progress", "Rejected"].includes(n.status)
|
|
20
|
+
|| !(summary === null || (fields(summary, ["statusCode", "errors", "warnings", "issues"])
|
|
21
|
+
&& (summary.statusCode === null || Number.isInteger(summary.statusCode))
|
|
22
|
+
&& Number.isInteger(summary.errors) && summary.errors >= 0 && Number.isInteger(summary.warnings) && summary.warnings >= 0
|
|
23
|
+
&& Array.isArray(summary.issues) && summary.issues.length <= 100 && summary.issues.every((issue) => fields(issue, ["severity", "code"])
|
|
24
|
+
&& ["error", "warning", "unknown"].includes(issue.severity) && (issue.code === null || Number.isInteger(issue.code)))))
|
|
25
|
+
|| !Array.isArray(report.steps) || report.steps.length > 10 || report.steps.some((step) => !fields(step, ["step", "file", "exitCode"])
|
|
26
|
+
|| !["submit", "log", "probe", "staple", "validate"].includes(step.step)
|
|
27
|
+
|| !(step.exitCode === null || (Number.isInteger(step.exitCode) && step.exitCode >= 0 && step.exitCode <= 255)))
|
|
28
|
+
|| !Array.isArray(report.stapling) || report.stapling.length > 2 || report.stapling.some((item) => !fields(item, ["file", "alreadyStapled", "validated"])
|
|
29
|
+
|| typeof item.file !== "string" || !path.isAbsolute(item.file) || typeof item.alreadyStapled !== "boolean" || item.validated !== true)) {
|
|
30
|
+
throw new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Invalid notarization agent report");
|
|
31
|
+
}
|
|
32
|
+
return report;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function requestSigningAgent(request, { paths = signingAgentPaths(), timeoutMs = (request.timeoutMs ?? 180000) + 5000,
|
|
36
|
+
onProgress = () => {},
|
|
10
37
|
io = fs, connect = net.createConnection } = {}) {
|
|
11
38
|
try {
|
|
12
39
|
privatePath(paths.directory, 0o700, { io, kind: "directory" });
|
|
@@ -23,7 +50,7 @@ export async function requestSigningAgent(request, { paths = signingAgentPaths()
|
|
|
23
50
|
if (frame.length > MAX_FRAME) throw new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Signing request is too large");
|
|
24
51
|
return new Promise((resolve, reject) => {
|
|
25
52
|
const socket = connect({ path: paths.socket });
|
|
26
|
-
let input = Buffer.alloc(0), settled = false;
|
|
53
|
+
let input = Buffer.alloc(0), settled = false, jobId;
|
|
27
54
|
const finish = (error, report) => {
|
|
28
55
|
if (settled) return;
|
|
29
56
|
settled = true; clearTimeout(timer); socket.destroy();
|
|
@@ -38,30 +65,53 @@ export async function requestSigningAgent(request, { paths = signingAgentPaths()
|
|
|
38
65
|
socket.on("data", (chunk) => {
|
|
39
66
|
input = Buffer.concat([input, chunk]);
|
|
40
67
|
if (input.length > MAX_FRAME) { finish(new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Signing agent response is too large")); return; }
|
|
41
|
-
const end = input.indexOf(10);
|
|
42
|
-
if (end < 0) return;
|
|
43
68
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
69
|
+
while (input.indexOf(10) >= 0 && !settled) {
|
|
70
|
+
const end = input.indexOf(10);
|
|
71
|
+
const report = JSON.parse(input.subarray(0, end).toString("utf8"));
|
|
72
|
+
input = input.subarray(end + 1);
|
|
73
|
+
const notary = ["notarize", "staple"].includes(request.type);
|
|
74
|
+
if (report.event) {
|
|
75
|
+
if (!notary || report.schema !== 1 || !["status", "heartbeat"].includes(report.event)
|
|
76
|
+
|| typeof report.id !== "string" || !/^[a-f0-9-]{36}$/i.test(report.id)
|
|
77
|
+
|| Object.keys(report).some((key) => !["schema", "event", "id", "step"].includes(key))
|
|
78
|
+
|| (report.step !== undefined && !["submit", "log", "probe", "staple", "validate"].includes(report.step))) throw new Error();
|
|
79
|
+
if (jobId && jobId !== report.id) throw new Error();
|
|
80
|
+
jobId = report.id;
|
|
81
|
+
onProgress(report);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (input.length || report.schema !== 1 || typeof report.ok !== "boolean") throw new Error();
|
|
85
|
+
if (notary && report.notarization) validateNotaryAgentReport(report);
|
|
86
|
+
if (notary && jobId && jobId !== report.id) throw new Error();
|
|
87
|
+
if (!report.ok) {
|
|
88
|
+
const code = typeof report.code === "string" && report.code.startsWith("MAC_SIGNING_") ? report.code : "MAC_SIGNING_AGENT_JOB_FAILED";
|
|
89
|
+
const error = new SigningAgentError(code, request.type === "updater" ? "Updater signing failed inside signing agent" : notary ? "Notarization or stapling failed inside signing agent" : String(report.message ?? "Signing agent rejected request"));
|
|
90
|
+
if (notary && report.notarization) error.report = report;
|
|
91
|
+
finish(error);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (notary) {
|
|
95
|
+
validateNotaryAgentReport(report);
|
|
96
|
+
} else if (request.type === "health") {
|
|
97
|
+
if (report.service !== "dev.rightkit.signing-agent" || !Number.isInteger(report.pid)) throw new Error();
|
|
98
|
+
} else if (request.type === "codesign-argv") {
|
|
99
|
+
if (!Array.isArray(report.results) || report.results.length !== 1
|
|
100
|
+
|| report.results.some((r) => !Number.isInteger(r.status) || r.status < 0 || r.status > 255
|
|
101
|
+
|| typeof r.stdout !== "string" || typeof r.stderr !== "string")) throw new Error();
|
|
102
|
+
} else if (!Array.isArray(report.results) || report.results.length !== (request.items?.length ?? request.files?.length)
|
|
103
|
+
|| report.results.some((r) => r.status !== 0 || typeof r.stdout !== "string" || typeof r.stderr !== "string")
|
|
104
|
+
|| (request.type === "updater" && report.results.some((r) => r.stdout || r.stderr))) throw new Error();
|
|
105
|
+
finish(null, report);
|
|
50
106
|
}
|
|
51
|
-
if (request.type === "health") {
|
|
52
|
-
if (report.service !== "dev.rightkit.signing-agent" || !Number.isInteger(report.pid)) throw new Error();
|
|
53
|
-
} else if (!Array.isArray(report.results) || report.results.length !== (request.items?.length ?? request.files?.length)
|
|
54
|
-
|| report.results.some((r) => r.status !== 0 || typeof r.stdout !== "string" || typeof r.stderr !== "string")
|
|
55
|
-
|| (request.type === "updater" && report.results.some((r) => r.stdout || r.stderr))) throw new Error();
|
|
56
|
-
finish(null, report);
|
|
57
107
|
} catch { finish(new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Invalid signing agent response")); }
|
|
58
108
|
});
|
|
59
109
|
});
|
|
60
110
|
}
|
|
61
|
-
export function requestSigningAgentSync(request, { runClient = spawnSync, home, timeoutMs =
|
|
111
|
+
export function requestSigningAgentSync(request, { runClient = spawnSync, home, timeoutMs = (request.timeoutMs ?? 180000) + 5000 } = {}) {
|
|
62
112
|
// Existing signing call sites are synchronous. Only typed JSON goes to this
|
|
63
113
|
// child's stdin; credentials & authentication token never enter argv/env.
|
|
64
|
-
const result = runClient(process.execPath, [fileURLToPath(import.meta.url)], { encoding: "utf8", stdio: "pipe", shell: false,
|
|
114
|
+
const result = runClient(process.execPath, [fileURLToPath(import.meta.url)], { encoding: "utf8", stdio: ["pipe", "pipe", "inherit"], shell: false,
|
|
65
115
|
input: JSON.stringify({ request, home, timeoutMs }), timeout: timeoutMs + 1000, maxBuffer: MAX_FRAME,
|
|
66
116
|
env: { PATH: process.env.PATH, HOME: process.env.HOME } });
|
|
67
117
|
let report;
|
|
@@ -69,17 +119,22 @@ export function requestSigningAgentSync(request, { runClient = spawnSync, home,
|
|
|
69
119
|
throw new SigningAgentError(result.error?.code === "ETIMEDOUT" ? "MAC_SIGNING_AGENT_TIMEOUT" : "MAC_SIGNING_AGENT_CONNECTION", "Signing agent client failed");
|
|
70
120
|
}
|
|
71
121
|
if (report.schema !== 1 || typeof report.ok !== "boolean") throw new SigningAgentError("MAC_SIGNING_AGENT_PROTOCOL", "Invalid signing agent client report");
|
|
72
|
-
if (!report.ok)
|
|
122
|
+
if (!report.ok) {
|
|
123
|
+
const error = new SigningAgentError(report.code ?? "MAC_SIGNING_AGENT_JOB_FAILED", report.message ?? "Signing agent failed");
|
|
124
|
+
if (["notarize", "staple"].includes(request.type) && report.notarization) error.report = validateNotaryAgentReport(report);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
73
127
|
if (result.error || result.status !== 0) throw new SigningAgentError("MAC_SIGNING_AGENT_CONNECTION", "Signing agent client failed");
|
|
74
128
|
return report;
|
|
75
129
|
}
|
|
76
130
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
77
131
|
try {
|
|
78
132
|
const { request, home, timeoutMs } = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
79
|
-
const report = await requestSigningAgent(request, { paths: signingAgentPaths(home), timeoutMs
|
|
133
|
+
const report = await requestSigningAgent(request, { paths: signingAgentPaths(home), timeoutMs,
|
|
134
|
+
onProgress: (frame) => process.stderr.write(`right-release signing-agent: ${frame.id} ${frame.step ?? frame.event}\n`) });
|
|
80
135
|
process.stdout.write(JSON.stringify(report));
|
|
81
136
|
} catch (error) {
|
|
82
|
-
process.stdout.write(JSON.stringify({ schema: 1, ok: false, code: error.code ?? "MAC_SIGNING_AGENT_PROTOCOL", message: error.message }));
|
|
137
|
+
process.stdout.write(JSON.stringify(error.report ?? { schema: 1, ok: false, code: error.code ?? "MAC_SIGNING_AGENT_PROTOCOL", message: error.message }));
|
|
83
138
|
process.exitCode = 1;
|
|
84
139
|
}
|
|
85
140
|
}
|
|
@@ -5,10 +5,11 @@ const [command, ...args] = process.argv.slice(2);
|
|
|
5
5
|
try {
|
|
6
6
|
let result;
|
|
7
7
|
if (command === "install") {
|
|
8
|
-
const options = {};
|
|
9
|
-
|
|
10
|
-
if (
|
|
11
|
-
options.managedRoot = args[1];
|
|
8
|
+
const options = { cargoTargetDirs: [] };
|
|
9
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
10
|
+
if (!["--managed-root", "--cargo-target-dir"].includes(args[i]) || !args[i + 1]) throw new TypeError("Expected install [--managed-root <directory>] [--cargo-target-dir <directory>]...");
|
|
11
|
+
if (args[i] === "--managed-root") options.managedRoot = args[i + 1];
|
|
12
|
+
else options.cargoTargetDirs.push(args[i + 1]);
|
|
12
13
|
}
|
|
13
14
|
result = installSigningAgent(options);
|
|
14
15
|
} else if (command === "uninstall" && !args.length) result = uninstallSigningAgent();
|
|
@@ -6,6 +6,8 @@ import { AGENT_LABEL, signingAgentPaths, defaultSigningRoots, ensurePrivateDirec
|
|
|
6
6
|
import { requestSigningAgentSync } from "./signing-agent-client.mjs";
|
|
7
7
|
|
|
8
8
|
const capture = { encoding: "utf8", stdio: "pipe", shell: false, timeout: 10000 };
|
|
9
|
+
function sleepMs(ms) { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
10
|
+
|
|
9
11
|
export function requireMacGuiSession({ run = spawnSync, uid = process.getuid?.() } = {}) {
|
|
10
12
|
const session = run("/bin/launchctl", ["print", `gui/${uid}`], capture);
|
|
11
13
|
if (session.error || session.status !== 0) throw new SigningAgentError("MAC_SIGNING_NO_GUI_SESSION", `No logged-in Aqua session for uid ${uid}`);
|
|
@@ -27,7 +29,7 @@ export function signingAgentPlist({ paths, node = process.execPath, home }) {
|
|
|
27
29
|
}
|
|
28
30
|
export function installSigningAgent({ home = os.userInfo().homedir, username = os.userInfo().username,
|
|
29
31
|
uid = process.getuid?.(), platform = process.platform, run = spawnSync, io = fs, node = process.execPath,
|
|
30
|
-
managedRoot = "/Volumes/D/.rightkit-managed", health = requestSigningAgentSync,
|
|
32
|
+
managedRoot = "/Volumes/D/.rightkit-managed", cargoTargetDirs = [], health = requestSigningAgentSync,
|
|
31
33
|
now = Date.now, sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms) } = {}) {
|
|
32
34
|
if (platform !== "darwin") throw new SigningAgentError("MAC_SIGNING_AGENT_PLATFORM", "Signing agent requires macOS");
|
|
33
35
|
requireMacGuiSession({ run, uid });
|
|
@@ -41,14 +43,26 @@ export function installSigningAgent({ home = os.userInfo().homedir, username = o
|
|
|
41
43
|
const pnpm = String(found.stdout ?? "").trim();
|
|
42
44
|
if (found.error || found.status !== 0 || !path.isAbsolute(pnpm) || pnpm.includes("\n")) throw new SigningAgentError("MAC_SIGNING_AGENT_INSTALL_FAILED", "Provisioned pnpm executable is unavailable");
|
|
43
45
|
// Resident runtime is self-contained; no package/checkout imports after install.
|
|
44
|
-
for (const
|
|
46
|
+
for (const root of cargoTargetDirs) {
|
|
47
|
+
if (typeof root !== "string" || !path.isAbsolute(root) || ["/", home, "/Volumes", "/Volumes/D"].includes(path.resolve(root))) throw new SigningAgentError("MAC_SIGNING_AGENT_PATH_DENIED", "Cargo target root must be a dedicated absolute directory");
|
|
48
|
+
}
|
|
49
|
+
for (const name of ["rightkit-signing-agent.mjs", "background-process.mjs", "codesign-argv.mjs"]) {
|
|
45
50
|
const target = path.join(paths.runtime, name);
|
|
46
51
|
if (io.existsSync(target)) privatePath(target, 0o600, { io, uid });
|
|
47
52
|
io.writeFileSync(target, io.readFileSync(new URL(`./${name}`, import.meta.url)), { mode: 0o600 });
|
|
48
53
|
}
|
|
49
54
|
if (io.existsSync(paths.config)) privatePath(paths.config, 0o600, { io, uid });
|
|
55
|
+
// SSH & Aqua can inherit different TMPDIR spellings. Include both aliases of
|
|
56
|
+
// this uid's Darwin temp root, since bundlers can canonicalize temporary paths.
|
|
57
|
+
const temp = run("/usr/bin/getconf", ["DARWIN_USER_TEMP_DIR"], capture);
|
|
58
|
+
const directory = String(temp.stdout ?? "").trim();
|
|
59
|
+
const temporaryRoots = [];
|
|
60
|
+
if (!temp.error && temp.status === 0 && path.isAbsolute(directory) && !directory.includes("\n")) {
|
|
61
|
+
temporaryRoots.push(path.resolve(directory));
|
|
62
|
+
try { temporaryRoots.push((io.realpathSync.native ?? io.realpathSync)(directory)); } catch {}
|
|
63
|
+
}
|
|
50
64
|
io.writeFileSync(paths.config, JSON.stringify({ schema: 1, home, username, pnpm,
|
|
51
|
-
roots: defaultSigningRoots(home, path.resolve(managedRoot)),
|
|
65
|
+
roots: defaultSigningRoots(home, path.resolve(managedRoot), [...cargoTargetDirs.map((root) => path.resolve(root)), ...temporaryRoots]),
|
|
52
66
|
environmentPath: [...new Set([path.dirname(node), path.dirname(pnpm), "/opt/homebrew/bin", "/usr/local/bin", "/usr/bin", "/bin", "/usr/sbin", "/sbin"])].join(":") }), { mode: 0o600 });
|
|
53
67
|
ensurePrivateDirectory(path.dirname(paths.plist), { io, uid });
|
|
54
68
|
if (io.existsSync(paths.plist)) privatePath(paths.plist, 0o600, { io, uid });
|
|
@@ -57,8 +71,20 @@ export function installSigningAgent({ home = os.userInfo().homedir, username = o
|
|
|
57
71
|
if (!loaded.error && loaded.status === 0) {
|
|
58
72
|
const stopped = run("/bin/launchctl", ["bootout", `gui/${uid}/${AGENT_LABEL}`], capture);
|
|
59
73
|
if (stopped.error || stopped.status !== 0) throw new SigningAgentError("MAC_SIGNING_AGENT_INSTALL_FAILED", "Could not stop existing signing agent");
|
|
74
|
+
// bootout is asynchronous: wait until launchd has released the label before bootstrapping again.
|
|
75
|
+
const gone = now() + 10000;
|
|
76
|
+
while (now() < gone) {
|
|
77
|
+
const still = run("/bin/launchctl", ["print", `gui/${uid}/${AGENT_LABEL}`], capture);
|
|
78
|
+
if (still.error || still.status !== 0) break;
|
|
79
|
+
sleepMs(100);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
let boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
|
|
83
|
+
// Exit 5 (EIO) is launchd still tearing the old job down; retry briefly.
|
|
84
|
+
for (let attempt = 0; attempt < 20 && !boot.error && boot.status === 5; attempt++) {
|
|
85
|
+
sleepMs(250);
|
|
86
|
+
boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
|
|
60
87
|
}
|
|
61
|
-
const boot = run("/bin/launchctl", ["bootstrap", `gui/${uid}`, paths.plist], capture);
|
|
62
88
|
if (boot.error || boot.status !== 0) {
|
|
63
89
|
requireMacGuiSession({ run, uid });
|
|
64
90
|
throw new SigningAgentError("MAC_SIGNING_AGENT_INSTALL_FAILED", `Signing agent bootstrap failed (exit ${boot.status ?? "unavailable"})`);
|