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:
@@ -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)
|
||||
}
|
||||
|
||||
+30
@@ -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=
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user