Validate production Iroh trust in release gates (#9118)

* test(iroh): expose retained production gate identity

* fix(iroh): validate production gate trust profile

* test(projects): cover synchronized workspace groups

* fix(projects): support synchronized workspace groups
This commit is contained in:
Abdulaziz Albahar
2026-07-28 22:49:32 -05:00
committed by GitHub
parent 955df1465f
commit 246f6eec87
9 changed files with 159 additions and 6 deletions
@@ -33,8 +33,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/XcodeProj.git",
"state" : {
"revision" : "fb79d68bc758cdb00079d72b6c8b899d99f5f15e",
"version" : "9.12.0"
"revision" : "3259a01e7193d55174c2f370d72e3ff4c277d8fd",
"version" : "9.15.0"
}
}
],
@@ -709,7 +709,7 @@ public struct XcodeProjectAdapter: ProjectAdapter, Sendable {
if resolved.pathExtension.lowercased() == "xcodeproj" {
out.append(resolved)
}
case let .group(group):
case let .group(group), let .fileSystemSynchronizedGroup(group):
let nested = collectProjectURLs(from: group.children, workspaceDir: workspaceDir)
out.append(contentsOf: nested)
}
@@ -65,6 +65,36 @@ struct XcodeProjectAdapterTests {
#expect(!model.modules.isEmpty)
}
@Test
func workspaceLoadFindsProjectsInsideFileSystemSynchronizedGroups() throws {
let temporaryDirectory = FileManager.default.temporaryDirectory
.appendingPathComponent("cmux-synchronized-workspace-\(UUID().uuidString)")
let temporaryWorkspace = temporaryDirectory
.appendingPathComponent("Synchronized.xcworkspace")
try FileManager.default.createDirectory(
at: temporaryWorkspace,
withIntermediateDirectories: true
)
defer { try? FileManager.default.removeItem(at: temporaryDirectory) }
let workspaceXML = """
<?xml version="1.0" encoding="UTF-8"?>
<Workspace version="1.0">
<FileSystemSynchronizedGroup location="container:">
<FileRef location="absolute:\(projectURL.path)"/>
</FileSystemSynchronizedGroup>
</Workspace>
"""
try Data(workspaceXML.utf8).write(
to: temporaryWorkspace.appendingPathComponent("contents.xcworkspacedata")
)
let adapter = XcodeProjectAdapter()
let model = try adapter.load(at: temporaryWorkspace)
#expect(model.modules.count == 1)
#expect(model.modules.first?.displayName == projectURL.deletingPathExtension().lastPathComponent)
}
@Test
func canLoadAcceptsXcodeprojDirectly() {
let adapter = XcodeProjectAdapter()
@@ -1,5 +1,5 @@
{
"originHash" : "cee4da9c2573722c5031e18abd5137540b76183d32cde46ba59a45c88a03bb2b",
"originHash" : "c695ab593ec950f04678a577b27492e3cf98610315012f307cf70644317c6ba6",
"pins" : [
{
"identity" : "aexml",
@@ -123,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tuist/XcodeProj.git",
"state" : {
"revision" : "621eca8d091cc110a99adc23bb0d6618a65b4544",
"version" : "9.13.0"
"revision" : "3259a01e7193d55174c2f370d72e3ff4c277d8fd",
"version" : "9.15.0"
}
}
],
@@ -0,0 +1,6 @@
// Public Ed25519 verification pins for relay policies signed by cmux production.
// Debug builds use this profile only when explicitly built with --prod-auth.
CMUX_IROH_RELAY_POLICY_KEY_ID = cmux-production-relay-policy-2026-07
CMUX_IROH_RELAY_POLICY_PUBLIC_KEY_BASE64 = qoBinRqX4TI1Ro6xAuOQxKUkeZT3pkFJuERP/+R+9aw=
CMUX_IROH_RELAY_POLICY_NEXT_KEY_ID = cmux-production-relay-policy-2026-08
CMUX_IROH_RELAY_POLICY_NEXT_PUBLIC_KEY_BASE64 = k+FND+WlELCkHs9QnWg1TfTuHXBwyv2907umX+mUOOU=
+8
View File
@@ -260,6 +260,12 @@ CMUX_IOS_IROH_BROKER_BASE_URL_VALUE="$(cmux_ios_resolve_iroh_broker_base_url)"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
IOS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
IROH_RELAY_POLICY_BUILD_ARGS=()
if [[ "$PROD_AUTH" -eq 1 ]]; then
IROH_RELAY_POLICY_BUILD_ARGS=(
-xcconfig "$IOS_DIR/../config/IrohRelayPolicyProduction.xcconfig"
)
fi
# Shared tag/identity + attach helpers; sanitize_tag() above delegates here so the
# built bundle id matches the signed-launch bundle id. Sourced before any
# sanitize_tag call below.
@@ -574,6 +580,7 @@ reload_simulator() {
-configuration Debug \
-destination "$DESTINATION" \
-derivedDataPath "$DERIVED_DATA" \
${IROH_RELAY_POLICY_BUILD_ARGS[@]+"${IROH_RELAY_POLICY_BUILD_ARGS[@]}"} \
PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID" \
PRODUCT_DISPLAY_NAME="$DISPLAY_NAME" \
CMUX_GIT_SHA="$GIT_SHA" \
@@ -685,6 +692,7 @@ reload_device() {
-destination "$device_destination"
-derivedDataPath "$DERIVED_DATA"
)
build_args+=(${IROH_RELAY_POLICY_BUILD_ARGS[@]+"${IROH_RELAY_POLICY_BUILD_ARGS[@]}"})
if [[ "$ALLOW_PROVISIONING_UPDATES" -eq 1 ]]; then
build_args+=(-allowProvisioningUpdates)
+45
View File
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import {
chmodSync,
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
@@ -166,6 +167,50 @@ exit 73
assert.equal(mode, "700");
});
test("production release gate removes disposable tagged Iroh endpoint state", (t) => {
const directory = fixtureDirectory();
t.after(() => rmSync(directory, { recursive: true, force: true }));
const fakeBin = path.join(directory, "bin");
mkdirSync(fakeBin, { mode: 0o700 });
const fakeBun = path.join(fakeBin, "bun");
writeFileSync(fakeBun, "#!/usr/bin/env bash\nexit 73\n", { mode: 0o755 });
chmodSync(fakeBun, 0o755);
const stackEnvironment = path.join(directory, "stack.env");
writeFileSync(stackEnvironment, "unused=true\n", { mode: 0o600 });
chmodSync(stackEnvironment, 0o600);
const bundleID = "com.cmuxterm.app.debug.prodstate";
const endpointState = path.join(
directory,
"Library",
"Application Support",
"cmux",
"iroh-debug",
bundleID,
);
mkdirSync(endpointState, { recursive: true, mode: 0o700 });
writeFileSync(path.join(endpointState, "endpoint.key"), "disposable\n", {
mode: 0o600,
});
const result = run("bash", [
"scripts/run-iroh-release-gate.sh",
"--mode", "automatic",
"--tag", "prodstate",
"--production",
"--stack-env-file", stackEnvironment,
], {
HOME: directory,
PATH: `${fakeBin}:${process.env.PATH}`,
TMPDIR: `${directory}/`,
});
assert.equal(result.status, 73, result.stderr);
assert.equal(existsSync(endpointState), false);
});
test("Mac reload documents production auth without accepting secret values", () => {
const result = run("bash", ["scripts/reload.sh", "--help"]);
assert.equal(result.status, 0, result.stderr);
+3
View File
@@ -722,6 +722,9 @@ if [[ -z "$TAG" ]]; then
)
fi
XCODEBUILD_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER="$BUNDLE_ID")
if [[ "$PROD_AUTH" -eq 1 ]]; then
XCODEBUILD_ARGS+=(-xcconfig "$SCRIPT_DIR/../config/IrohRelayPolicyProduction.xcconfig")
fi
# Scope the sidebar ExtensionKit point per build tag so concurrent dev builds (and
# their tagged sample extensions) don't share one point. The host bundle declares
# the point under Contents/Extensions, and Info.plist carries the same identifier.
+61
View File
@@ -169,9 +169,12 @@ cleanup() {
pkill -f "cmux DEV ${SLUG}.app/Contents/MacOS/cmux DEV" 2>/dev/null || true
if [[ "$PRODUCTION" -eq 1 ]]; then
# Production uses a disposable account and must remove its local tokens.
# The endpoint key and verified-policy cache live outside the ordinary
# tagged app support directory, so clear that exact tagged identity too.
# Staging keeps its tagged state so a failed gate remains inspectable and a
# later --skip-build run can reuse the same authenticated build.
rm -rf "$HOME/Library/Application Support/cmux/$MAC_BUNDLE_ID"
rm -rf "$HOME/Library/Application Support/cmux/iroh-debug/$MAC_BUNDLE_ID"
security delete-generic-password -s "$MAC_BUNDLE_ID.auth" -a cmux-auth-access-token >/dev/null 2>&1 || true
security delete-generic-password -s "$MAC_BUNDLE_ID.auth" -a cmux-auth-refresh-token >/dev/null 2>&1 || true
fi
@@ -326,6 +329,64 @@ fi
[[ -d "$MAC_APP" ]] || { echo "error: tagged Mac app is missing: $MAC_APP" >&2; exit 1; }
if [[ "$PRODUCTION" -eq 1 ]]; then
PRODUCTION_RELAY_POLICY_XCCONFIG="$REPO_ROOT/config/IrohRelayPolicyProduction.xcconfig"
MAC_INFO_PLIST="$MAC_APP/Contents/Info.plist"
IOS_INFO_PLIST="$IOS_APP/Info.plist"
PRODUCTION_RELAY_POLICY_XCCONFIG="$PRODUCTION_RELAY_POLICY_XCCONFIG" \
MAC_INFO_PLIST="$MAC_INFO_PLIST" \
IOS_INFO_PLIST="$IOS_INFO_PLIST" \
/usr/bin/python3 <<'PY'
import os
import plistlib
setting_names = (
"CMUX_IROH_RELAY_POLICY_KEY_ID",
"CMUX_IROH_RELAY_POLICY_PUBLIC_KEY_BASE64",
"CMUX_IROH_RELAY_POLICY_NEXT_KEY_ID",
"CMUX_IROH_RELAY_POLICY_NEXT_PUBLIC_KEY_BASE64",
)
settings = {}
with open(os.environ["PRODUCTION_RELAY_POLICY_XCCONFIG"], encoding="utf-8") as handle:
for raw_line in handle:
line = raw_line.strip()
if not line or line.startswith("//") or "=" not in line:
continue
key, value = line.split("=", 1)
settings[key.strip()] = value.strip()
missing = [name for name in setting_names if not settings.get(name)]
if missing:
raise SystemExit("production relay-policy build profile is incomplete")
expected_trust = [
{
"keyID": settings["CMUX_IROH_RELAY_POLICY_KEY_ID"],
"publicKeyBase64": settings["CMUX_IROH_RELAY_POLICY_PUBLIC_KEY_BASE64"],
},
{
"keyID": settings["CMUX_IROH_RELAY_POLICY_NEXT_KEY_ID"],
"publicKeyBase64": settings["CMUX_IROH_RELAY_POLICY_NEXT_PUBLIC_KEY_BASE64"],
},
]
for label, environment_name in (
("Mac", "MAC_INFO_PLIST"),
("iOS", "IOS_INFO_PLIST"),
):
with open(os.environ[environment_name], "rb") as handle:
info = plistlib.load(handle)
if info.get("CMUXIrohRelayPolicyKeyID") != expected_trust[0]["keyID"]:
raise SystemExit(f"{label} production gate app has the wrong relay-policy key ID")
if info.get("CMUXIrohRelayPolicyPublicKeyBase64") != expected_trust[0]["publicKeyBase64"]:
raise SystemExit(f"{label} production gate app has the wrong relay-policy public key")
if info.get("CMUXIrohRelayPolicyTrustKeys") != expected_trust:
raise SystemExit(f"{label} production gate app has the wrong relay-policy trust set")
print("==> production relay-policy pins verified in Mac and iOS build artifacts")
PY
fi
# Both endpoints read the mode before constructing their Iroh endpoint. Write
# after installation so a fresh simulator app container cannot replace it.
defaults write "$MAC_BUNDLE_ID" cmux.iroh.debug.transport-mode -string "$RAW_MODE"