Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/operations/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,26 @@ NSIS is downloaded by electron-builder. WSL support additionally needs the Linux
passed as `--wsl-runtime`; see the
[release runbook](./release.md#windows-payload-topology-and-update-validation).

### Local macOS signing

macOS ties privacy grants such as Local Network to the app's signature. An ad-hoc signature
changes on every rebuild, so the grant silently stops applying to the next build. Builds without
`--signed` therefore sign with a stable keychain identity, picked in this order:

1. `T3CODE_MAC_SIGNING_IDENTITY`, as a certificate name or SHA-1 from
`security find-identity -v -p codesigning`.
2. The first `Developer ID Application` identity in the keychain.
3. A self-signed certificate named `T3 Code Local Signing`.

With none of these, the build falls back to ad-hoc signing and logs a warning. To create the
self-signed certificate, open Keychain Access, choose **Keychain Access > Certificate Assistant >
Create a Certificate**, name it `T3 Code Local Signing`, and set Identity Type to **Self Signed
Root** and Certificate Type to **Code Signing**. To avoid replacing it yearly, choose **Let me
override defaults** and raise the validity period, for example to 3650 days. Then open the
certificate and set **Trust > Code Signing** to **Always Trust**; until then
`security find-identity -v -p codesigning` does not list it. The first signed build asks for
keychain access to the key; choose **Always Allow**.

### Signing and passkeys

Add `--signed` after configuring the platform credentials in the
Expand Down
26 changes: 26 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
createStageWorkspaceConfig,
createStagePatchedDependencies,
createBuildConfig,
selectLocalMacSigningIdentity,
DESKTOP_ELECTRON_LANGUAGES,
DESKTOP_FILE_EXCLUSIONS,
DESKTOP_EXTRA_RESOURCES,
Expand Down Expand Up @@ -258,6 +259,31 @@ const makeWindowsPayloadFixture = Effect.fn("test.makeWindowsPayloadFixture")(fu
});

it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
it("prefers a configured, then Developer ID, then local self-signed macOS identity", () => {
const developerId = "1111111111111111111111111111111111111111";
const selfSigned = "2222222222222222222222222222222222222222";
const other = "3333333333333333333333333333333333333333";
const output = [
` 1) ${other} "Other Local"`,
` 2) ${selfSigned} "T3 Code Local Signing"`,
` 3) ${developerId} "Developer ID Application: Example (TEAM123456)"`,
" 3 valid identities found",
].join("\n");

assert.equal(selectLocalMacSigningIdentity(output, undefined), developerId);
assert.equal(selectLocalMacSigningIdentity(output, "Other Local"), other);
assert.equal(selectLocalMacSigningIdentity(output, selfSigned.toLowerCase()), selfSigned);
assert.equal(selectLocalMacSigningIdentity(output, "Missing"), undefined);
assert.equal(
selectLocalMacSigningIdentity(output.replace(/^.*Developer ID.*$/m, ""), undefined),
selfSigned,
);
assert.equal(
selectLocalMacSigningIdentity(` 1) ${other} "Other Local"`, undefined),
undefined,
);
});

it("resolves the dedicated nightly updater channel from nightly versions", () => {
assert.equal(resolveDesktopUpdateChannel("0.0.17-nightly.20260413.42"), "nightly");
assert.equal(resolveDesktopUpdateChannel("0.0.17"), "latest");
Expand Down
66 changes: 66 additions & 0 deletions scripts/build-desktop-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,64 @@ ${associatedDomains}
`;
}

// Self-signed certificate that unsigned local macOS builds look for when no
// Developer ID identity is in the keychain. docs/operations/development.md
// explains how to create it.
export const LOCAL_MAC_SIGNING_CERTIFICATE_NAME = "T3 Code Local Signing";

/**
* Picks the signing identity for an unsigned local macOS build from
* `security find-identity -v -p codesigning` output: the configured identity
* (name or SHA-1), else the first Developer ID Application identity, else the
* local self-signed certificate. Returns the SHA-1, which electron-builder
* matches against the same list without rejecting Apple name prefixes.
*/
export function selectLocalMacSigningIdentity(
findIdentityOutput: string,
configured: string | undefined,
): string | undefined {
const identities = findIdentityOutput.split("\n").flatMap((line) => {
const match = /^\s*\d+\)\s+([0-9A-F]{40})\s+"(.+)"$/i.exec(line);
return match?.[1] && match[2] ? [{ hash: match[1].toUpperCase(), name: match[2] }] : [];
});
const find = (predicate: (identity: (typeof identities)[number]) => boolean) =>
identities.find(predicate)?.hash;

if (configured) {
return find(({ hash, name }) => name === configured || hash === configured.toUpperCase());
}
return (
find(({ name }) => name.startsWith("Developer ID Application:")) ??
find(({ name }) => name === LOCAL_MAC_SIGNING_CERTIFICATE_NAME)
);
}

// macOS records privacy grants such as Local Network against the app's
// designated requirement. An ad-hoc signature's requirement is its cdhash,
// which changes on every rebuild, so local builds sign with a stable keychain
// identity when one exists and fall back to ad-hoc with a warning otherwise.
const resolveLocalMacSigningIdentity = Effect.fn("resolveLocalMacSigningIdentity")(function* () {
const configured = Option.getOrUndefined(
yield* Config.String("T3CODE_MAC_SIGNING_IDENTITY").pipe(Config.option),
)?.trim();
const result = yield* spawnAndCollectOutput(
ChildProcess.make("security", ["find-identity", "-v", "-p", "codesigning"]),
).pipe(Effect.orElseSucceed(() => ({ stdout: "", stderr: "", exitCode: 1 })));
const identity = selectLocalMacSigningIdentity(result.stdout, configured || undefined);

if (identity) {
yield* Effect.log(`[desktop-artifact] Signing local macOS build with identity ${identity}.`);
} else {
yield* Effect.logWarning(
configured
? `[desktop-artifact] T3CODE_MAC_SIGNING_IDENTITY '${configured}' is not a valid code signing identity in the keychain.`
: `[desktop-artifact] No Developer ID or '${LOCAL_MAC_SIGNING_CERTIFICATE_NAME}' code signing identity found in the keychain.`,
"Falling back to an ad-hoc signature: macOS privacy grants such as Local Network will not survive a rebuild. See docs/operations/development.md#local-macos-signing.",
);
}
return identity;
});

export function resolveFffNativeDependencies(
platform: typeof BuildPlatform.Type,
arch: typeof BuildArch.Type,
Expand Down Expand Up @@ -2722,6 +2780,8 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
extendInfo: {
NSScreenCaptureUsageDescription:
"T3 Code captures the active window when you use the window capture shortcut.",
NSLocalNetworkUsageDescription:
"T3 Code runs your terminals and agents, which connect to devices on your local network.",
},
protocols: [
{
Expand Down Expand Up @@ -3816,6 +3876,12 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* (
delete buildEnv.APPLE_API_KEY;
delete buildEnv.APPLE_API_KEY_ID;
delete buildEnv.APPLE_API_ISSUER;
if (options.platform === "mac" && hostPlatform === "darwin") {
const localIdentity = yield* resolveLocalMacSigningIdentity();
if (localIdentity) {
buildEnv.CSC_NAME = localIdentity;
}
}
}

if (hostPlatform === "win32") {
Expand Down
Loading