fix(computer-use): fail closed on unverified CuaDriver.app + background launch
Hardening on top of the TCC daemon-identity salvage: - _validate_cua_driver_app_signature: codesign -dv gate requiring EXACT Identifier=com.trycua.driver and the official team (4YEC26S9KF) before /usr/bin/open hands the bundle to LaunchServices — the identity fix must not double as a launcher for arbitrary/impostor bundles (suffixed identifiers and wrong teams rejected; unsigned dev builds only via computer_use.allow_unsigned_driver: true in config.yaml). - _resolve_cua_driver_app_path: derive the bundle ONLY from the resolved driver binary — the /Applications fallback could launch a DIFFERENT install than the manifest resolved. - open -n -g: don't activate/steal focus when launching the daemon. - 7 new tests incl. sabotage-verified exact-match assertions. Grafted from #76433's review direction (@Chadmc9889's original fail-closed validation requirement). Co-authored-by: Chadmc9889 <Chadmc9889@users.noreply.github.com>
This commit is contained in:
@@ -3646,6 +3646,12 @@ DEFAULT_CONFIG = {
|
||||
# flags when it launches the runtime. See
|
||||
# https://cua.ai/docs/reference/cua-driver/permission-modes
|
||||
"capability_manifest": "",
|
||||
# macOS only: allow launching an UNSIGNED (ad-hoc / TeamIdentifier
|
||||
# not set) CuaDriver.app for the private-session daemon. The default
|
||||
# (false) fails closed unless the bundle is signed with the official
|
||||
# cua-driver identity (com.trycua.driver / team 4YEC26S9KF). Enable
|
||||
# only when developing the driver locally from source.
|
||||
"allow_unsigned_driver": False,
|
||||
# Pre-authorize existing-profile browser attachment in standard mode
|
||||
# (cua-driver's trusted-launcher `--grant existing-profile`). When
|
||||
# true, the agent can attach to your already-running, signed-in
|
||||
|
||||
@@ -248,7 +248,9 @@ def test_schema_does_not_expose_approval_token():
|
||||
# ── bounded embedded daemon ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def test_macos_embedded_daemon_launches_through_cuadriver_app():
|
||||
def test_macos_embedded_daemon_launches_through_cuadriver_app(monkeypatch):
|
||||
validated = []
|
||||
monkeypatch.setattr(cb, "_validate_cua_driver_app_signature", lambda app: validated.append(app))
|
||||
command = cb._embedded_daemon_spawn_command(
|
||||
"/tmp/cua-driver",
|
||||
["serve", "--embedded", "--socket", "/tmp/private.sock"],
|
||||
@@ -256,9 +258,12 @@ def test_macos_embedded_daemon_launches_through_cuadriver_app():
|
||||
app_path="/Applications/CuaDriver.app",
|
||||
)
|
||||
|
||||
# Signature validation is mandatory before any launch command is built.
|
||||
assert validated == ["/Applications/CuaDriver.app"]
|
||||
assert command == [
|
||||
"/usr/bin/open",
|
||||
"-n",
|
||||
"-g",
|
||||
"-a",
|
||||
"/Applications/CuaDriver.app",
|
||||
"--args",
|
||||
@@ -269,6 +274,83 @@ def test_macos_embedded_daemon_launches_through_cuadriver_app():
|
||||
]
|
||||
|
||||
|
||||
def _codesign_proc(returncode=0, stderr=""):
|
||||
import subprocess as _sp
|
||||
|
||||
return _sp.CompletedProcess(["codesign"], returncode, stdout="", stderr=stderr)
|
||||
|
||||
|
||||
def _patch_codesign(monkeypatch, proc):
|
||||
monkeypatch.setattr(cb.shutil, "which", lambda name: "/usr/bin/codesign")
|
||||
monkeypatch.setattr(cb.subprocess, "run", lambda *a, **kw: proc)
|
||||
|
||||
|
||||
def test_driver_signature_valid_official_identity(monkeypatch):
|
||||
_patch_codesign(
|
||||
monkeypatch,
|
||||
_codesign_proc(stderr="Identifier=com.trycua.driver\nTeamIdentifier=4YEC26S9KF\n"),
|
||||
)
|
||||
cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app") # no raise
|
||||
|
||||
|
||||
def test_driver_signature_rejects_suffixed_identifier(monkeypatch):
|
||||
import pytest
|
||||
|
||||
_patch_codesign(
|
||||
monkeypatch,
|
||||
_codesign_proc(stderr="Identifier=com.trycua.driver.evil\nTeamIdentifier=4YEC26S9KF\n"),
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="identifier"):
|
||||
cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app")
|
||||
|
||||
|
||||
def test_driver_signature_rejects_wrong_team(monkeypatch):
|
||||
import pytest
|
||||
|
||||
_patch_codesign(
|
||||
monkeypatch,
|
||||
_codesign_proc(stderr="Identifier=com.trycua.driver\nTeamIdentifier=EVIL000000\n"),
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="team"):
|
||||
cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app")
|
||||
|
||||
|
||||
def test_driver_signature_unsigned_rejected_by_default(monkeypatch):
|
||||
import pytest
|
||||
|
||||
_patch_codesign(
|
||||
monkeypatch,
|
||||
_codesign_proc(stderr="Identifier=com.trycua.driver\nTeamIdentifier=not set\n"),
|
||||
)
|
||||
monkeypatch.setattr(cb, "_computer_use_cfg", lambda: {})
|
||||
with pytest.raises(RuntimeError, match="team"):
|
||||
cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app")
|
||||
|
||||
|
||||
def test_driver_signature_unsigned_allowed_by_config_opt_in(monkeypatch):
|
||||
_patch_codesign(
|
||||
monkeypatch,
|
||||
_codesign_proc(stderr="Identifier=com.trycua.driver\nTeamIdentifier=not set\n"),
|
||||
)
|
||||
monkeypatch.setattr(cb, "_computer_use_cfg", lambda: {"allow_unsigned_driver": True})
|
||||
cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app") # no raise
|
||||
|
||||
|
||||
def test_driver_signature_rejects_unsigned_bundle(monkeypatch):
|
||||
import pytest
|
||||
|
||||
_patch_codesign(monkeypatch, _codesign_proc(returncode=1, stderr="code object is not signed at all"))
|
||||
with pytest.raises(RuntimeError, match="not code-signed"):
|
||||
cb._validate_cua_driver_app_signature("/Applications/CuaDriver.app")
|
||||
|
||||
|
||||
def test_resolve_app_path_has_no_applications_fallback(tmp_path):
|
||||
# A driver binary OUTSIDE any .app bundle must resolve to None — the old
|
||||
# /Applications fallback could launch a DIFFERENT install than the
|
||||
# resolved driver.
|
||||
assert cb._resolve_cua_driver_app_path(str(tmp_path / "cua-driver")) is None
|
||||
|
||||
|
||||
def test_non_macos_embedded_daemon_keeps_direct_binary_launch():
|
||||
command = cb._embedded_daemon_spawn_command(
|
||||
"/tmp/cua-driver",
|
||||
|
||||
@@ -593,25 +593,90 @@ def _wsl_windows_path_to_posix(path: str) -> str:
|
||||
|
||||
|
||||
def _resolve_cua_driver_app_path(driver_cmd: str) -> Optional[str]:
|
||||
"""Return the installed CuaDriver.app carrying *driver_cmd*, if present."""
|
||||
"""Return the CuaDriver.app bundle that CARRIES *driver_cmd*, if any.
|
||||
|
||||
Deliberately derived from the resolved driver binary path only — no
|
||||
/Applications or ~/Applications fallback. A fallback candidate can be a
|
||||
DIFFERENT install than the driver the manifest resolved (stale copy,
|
||||
side-by-side version), and launching it would run code the resolution
|
||||
chain never validated. If the resolved driver does not live inside an
|
||||
app bundle, the caller fails closed with install guidance.
|
||||
"""
|
||||
marker = ".app/Contents/MacOS/"
|
||||
candidates: List[str] = []
|
||||
marker_index = driver_cmd.find(marker)
|
||||
if marker_index >= 0:
|
||||
candidates.append(driver_cmd[: marker_index + len(".app")])
|
||||
candidates.extend(
|
||||
[
|
||||
"/Applications/CuaDriver.app",
|
||||
os.path.expanduser("~/Applications/CuaDriver.app"),
|
||||
]
|
||||
)
|
||||
for candidate in candidates:
|
||||
executable = os.path.join(candidate, "Contents", "MacOS", "cua-driver")
|
||||
if os.path.isfile(executable) and os.access(executable, os.X_OK):
|
||||
return candidate
|
||||
if marker_index < 0:
|
||||
return None
|
||||
candidate = driver_cmd[: marker_index + len(".app")]
|
||||
executable = os.path.join(candidate, "Contents", "MacOS", "cua-driver")
|
||||
if os.path.isfile(executable) and os.access(executable, os.X_OK):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
# The only bundle identity the private daemon may launch through, and the
|
||||
# team that signs official cua-driver releases. Exact matches only: a
|
||||
# suffixed identifier ("com.trycua.driver.evil") or a different non-empty
|
||||
# team is an impostor bundle, not a variant.
|
||||
_CUA_DRIVER_BUNDLE_ID = "com.trycua.driver"
|
||||
_CUA_DRIVER_TEAM_ID = "4YEC26S9KF"
|
||||
|
||||
|
||||
def _validate_cua_driver_app_signature(app_path: str) -> None:
|
||||
"""Fail closed unless *app_path* is the genuinely-signed CuaDriver.app.
|
||||
|
||||
Launching via ``/usr/bin/open`` hands LaunchServices whatever bundle sits
|
||||
at the path, so the TCC-identity fix must not become a launcher for
|
||||
arbitrary apps: require ``codesign -dv`` to report EXACTLY
|
||||
``Identifier=com.trycua.driver`` and the expected TeamIdentifier.
|
||||
``TeamIdentifier=not set`` (unsigned/ad-hoc dev builds) is allowed only
|
||||
when ``computer_use.allow_unsigned_driver: true`` is set in config.yaml —
|
||||
the escape hatch for local driver development, never the default. Raises
|
||||
RuntimeError on any mismatch or when codesign is unavailable/fails.
|
||||
"""
|
||||
codesign = shutil.which("codesign")
|
||||
if not codesign:
|
||||
raise RuntimeError(
|
||||
"codesign is required to verify CuaDriver.app before launching it."
|
||||
)
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[codesign, "-dv", app_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=15,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
raise RuntimeError(f"could not verify CuaDriver.app signature: {exc}") from exc
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"CuaDriver.app at {app_path} is not code-signed; refusing to launch it "
|
||||
f"({(proc.stderr or '').strip()})"
|
||||
)
|
||||
# codesign -dv reports on stderr.
|
||||
fields = {}
|
||||
for line in (proc.stderr or "").splitlines():
|
||||
key, sep, value = line.partition("=")
|
||||
if sep:
|
||||
fields.setdefault(key.strip(), value.strip())
|
||||
identifier = fields.get("Identifier", "")
|
||||
team = fields.get("TeamIdentifier", "")
|
||||
if identifier != _CUA_DRIVER_BUNDLE_ID:
|
||||
raise RuntimeError(
|
||||
f"CuaDriver.app at {app_path} has identifier {identifier!r}, "
|
||||
f"expected {_CUA_DRIVER_BUNDLE_ID!r}; refusing to launch it."
|
||||
)
|
||||
if team == _CUA_DRIVER_TEAM_ID:
|
||||
return
|
||||
if team in ("", "not set") and _computer_use_cfg().get("allow_unsigned_driver") is True:
|
||||
return
|
||||
raise RuntimeError(
|
||||
f"CuaDriver.app at {app_path} is signed by team {team!r}, expected "
|
||||
f"{_CUA_DRIVER_TEAM_ID!r}; refusing to launch it. (Set "
|
||||
"computer_use.allow_unsigned_driver: true in config.yaml only for "
|
||||
"local unsigned driver builds.)"
|
||||
)
|
||||
|
||||
|
||||
def _embedded_daemon_spawn_command(
|
||||
driver_cmd: str,
|
||||
serve_args: List[str],
|
||||
@@ -628,9 +693,11 @@ def _embedded_daemon_spawn_command(
|
||||
"CuaDriver.app is required for private computer-use sessions on macOS. "
|
||||
"Run `hermes computer-use install` to restore it."
|
||||
)
|
||||
_validate_cua_driver_app_signature(resolved_app)
|
||||
return [
|
||||
"/usr/bin/open",
|
||||
"-n",
|
||||
"-g",
|
||||
"-a",
|
||||
resolved_app,
|
||||
"--args",
|
||||
|
||||
Reference in New Issue
Block a user