【Bug已解决】macOS detects Codex Computer Use.app as malware and deletes it! 解决方案
【Bug已解决】macOS detects Codex Computer Use.app as malware and deletes it! 解决方案
原始报错:macOS detects Codex Computer Use.app as malware and deletes it! 场景:用户下载并打开
Codex Computer Use.app,macOS 的 Gatekeeper / XProtect 把它判定为恶意软件,直接删除(或 quarantine 后阻止运行并提示"已移动至废纸篓")。应用根本起不来。 关键词:macOS 应用包、Gatekeeper、XProtect、代码签名、公证(notarization)、隔离属性(quarantine)、应用包结构。
一、现象长什么样
用户操作:
- 从网页/渠道拿到
Codex Computer Use.app(一个.appbundle); - 双击打开,macOS 弹出"无法打开,因为 Apple 无法检查其是否包含恶意软件",或直接由 XProtect 删掉;
- 应用消失在废纸篓,无法运行;
- 用
codesign --verify检查发现签名无效或不完整,或根本没签名。
对最终用户来说,这就是"我的 Mac 把应用当病毒删了"。对开发者来说,根因几乎都在签名/公证链条不完整或隔离属性没清掉。
二、背景:macOS 怎么判定一个 .app 是否安全
macOS 对从网络下载的应用有多道防线,针对的是.appbundle 整体:
- 代码签名(Code Signing):
.app/Contents/MacOS/里的可执行文件、框架、资源都要用开发者证书签名,且签名要覆盖整个 bundle(含Contents/_CodeSignature/CodeResources清单)。 - 公证(Notarization):开发者把 app 上传 Apple 扫描,拿到"ticket",用
xcrun stapler staple把 ticket 钉进 app。用户打开时 Gatekeeper 校验 ticket。 - 隔离属性(quarantine):从浏览器下载的 app 会被打上
com.apple.quarantine扩展属性,首次打开必经 Gatekeeper 检查。 - XProtect:系统级恶意软件特征库,命中即删。
.appbundle 比单个 CLI 二进制复杂:它是一个目录树,签名必须递归覆盖每一层(可执行文件、dylib、嵌套 framework、资源),任何一层漏签或哈希不符,整个 bundle 的校验就失败。
三、根因:签名/公证链有缺口
根因拆解(针对 app bundle):
- 整体未签名或部分签名:只签了主可执行文件,没签嵌套的 dylib / framework,bundle 校验失败。
- 未公证:有签名但没上传 Apple 拿 ticket,Gatekeeper 在较新 macOS 上直接拦。
- quarantine 未清:用户用
curl/浏览器下载后没清com.apple.quarantine,打开即触发检查。 - entitlements 不匹配:app 需要的权限(如屏幕录制、辅助功能,Computer Use 类应用常需)在签名时没声明,运行时被拒或特征异常引 XProtect 误判。
- 签名在打包后破损:签名后又改了 bundle 内文件(改 Info.plist、替换资源),签名失效。
下面用最小模型复现"带 quarantine 且签名无效导致校验失败",再给修复。
四、最小可运行复现(校验逻辑模拟)
用 Python 模拟"校验一个 app bundle":检查签名清单是否完整、是否带 quarantine。
import hashlib, os, json class AppBundleVerifier: def __init__(self, bundle_root): self.root = bundle_root def has_quarantine(self) -> bool: # 模拟读取 com.apple.quarantine 扩展属性 qp = os.path.join(self.root, ".quarantine") return os.path.exists(qp) def signature_intact(self, code_resources: dict, actual_files: list) -> bool: # code_resources 是签名时记录的哈希清单 for rel in actual_files: recorded = code_resources.get(rel) if recorded is None: return False # 清单没覆盖该文件 -> 签名不完整 live = hashlib.sha256( open(os.path.join(self.root, rel), "rb").read()).hexdigest()[:8] if live != recorded: return False # 哈希不符 -> 签名破损 return True if __name__ == "__main__": # 假设某文件不在签名清单里 verifier = AppBundleVerifier("/tmp/Codex.app") code_resources = {"Contents/MacOS/Codex": "abc123"} # 漏了嵌套 framework actual = ["Contents/MacOS/Codex", "Contents/Frameworks/Helper.framework/Helper"] print("签名完整?", verifier.signature_intact(code_resources, actual)) # False运行输出False——嵌套 framework 没进签名清单,bundle 校验失败,这正是 Gatekeeper 拒绝/删除的根因之一。
五、方案:递归签名整个 app bundle
第一层:签名必须覆盖 bundle 内每一层。用codesign --deep --force --sign递归签所有可执行组件(真实命令,注释说明,不在 Python 内编造):
import subprocess, shlex def sign_app_bundle(bundle_path: str, identity: str) -> int: """递归签名整个 .app bundle。真实调用 codesign。""" cmd = [ "codesign", "--deep", "--force", "--sign", identity, bundle_path, ] print("执行:", shlex.join(cmd)) # 真实环境运行: return subprocess.call(cmd) def verify_app(bundle_path: str) -> int: """校验签名。真实调用 codesign --verify --verbose。""" cmd = ["codesign", "--verify", "--verbose=2", bundle_path] print("执行:", shlex.join(cmd)) return subprocess.call(cmd) if __name__ == "__main__": # 演示调用(不会真跑,需真实证书与 app) print("签名命令示例已生成;实际需有效的 Developer ID Application 证书") sign_app_bundle("/Applications/Codex Computer Use.app", "Developer ID Application: Acme") verify_app("/Applications/Codex Computer Use.app")--deep保证嵌套的 framework / dylib 都被签,消除"清单漏项"。
六、方案:公证并 staple ticket
第二层:签名后上传 Apple 公证,拿到 ticket 钉进 app,让用户离线也能过 Gatekeeper:
def notarize_and_staple(bundle_path: str, keychain_profile: str) -> int: # 1) 上传公证 submit = [ "xcrun", "notarytool", "submit", bundle_path, "--keychain-profile", keychain_profile, "--wait", ] rc = subprocess.call(submit) if rc != 0: return rc # 2) 把 ticket 钉进 app(stapler) staple = ["xcrun", "stapler", "staple", bundle_path] return subprocess.call(staple) if __name__ == "__main__": print("公证流程:notarytool submit -> stapler staple") notarize_and_staple("/Applications/Codex Computer Use.app", "my-profile")stapler 把 ticket 嵌入 app 内部,用户首次打开无需联网查 Apple,Gatekeeper 直接放行。
七、方案:清除隔离属性 + 校验 entitlements
第三层:分发时清掉 quarantine(或指导用户用xattr -dr清),并确保 entitlements 覆盖应用所需权限(Computer Use 类常需屏幕录制等):
def remove_quarantine(bundle_path: str) -> int: """清除 com.apple.quarantine 扩展属性。""" cmd = ["xattr", "-dr", "com.apple.quarantine", bundle_path] print("执行:", shlex.join(cmd)) return subprocess.call(cmd) def check_entitlements(bundle_path: str, required: list) -> list: """检查 app 的 entitlements 是否声明了所需权限。""" out = subprocess.run( ["codesign", "-d", "--entitlements", ":-", bundle_path], capture_output=True, text=True, ) missing = [e for e in required if e not in out.stdout] return missing if __name__ == "__main__": remove_quarantine("/Applications/Codex Computer Use.app") needed = [ "com.apple.security.cs.allow-jit", "com.apple.security.screen-capture", # 屏幕类应用常需 ] missing = check_entitlements("/Applications/Codex Computer Use.app", needed) print("缺失的 entitlements:", missing)清 quarantine 让"首次打开"不再触发 Gatekeeper 拦;entitlements 检查确保运行时权限与签名一致,避免异常行为招来 XProtect 误判。
八、验证:本地校验脚本(交付前自检)
def self_check(bundle_path: str) -> dict: import os report = {} # 1. quarantine 应已清 report["quarantine_cleared"] = not os.path.exists( os.path.join(bundle_path, ".quarantine")) # 2. 签名可校验 report["signature_ok"] = (verify_app(bundle_path) == 0) # 3. entitlements 覆盖所需 report["entitlements_missing"] = check_entitlements( bundle_path, ["com.apple.security.screen-capture"]) return report if __name__ == "__main__": rep = self_check("/Applications/Codex Computer Use.app") print("交付前自检:", rep) # 全绿才发布:签名 ok + quarantine 清 + entitlements 齐把self_check放进发布流水线,签名/公证/entitlements 任一缺失都不发布,从源头杜绝"用户下载即被删"。
九、排查清单(".app 被当恶意软件删除"按顺序查)
- 签名完整性:bundle 内每一层(可执行、dylib、framework、资源)是否都签了?
codesign -vvv是否通过? - 公证:是否上传 Apple 拿到 ticket 并
stapler staple?新 macOS 无 ticket 会被拦。 - quarantine:从网络下载后
com.apple.quarantine是否清除? - entitlements:应用所需权限(屏幕录制/辅助功能等)是否在签名时声明?
- 打包后改动:签名后是否又改过 bundle 内文件?改动会废掉签名。
- XProtect 误判:是否因行为异常(如缺权限导致古怪行为)触发特征库?补全 entitlements 通常缓解。
- 证书有效:用的 Developer ID 证书是否仍在有效期内、未被吊销?
十、小结
"macOS 把 .app 当恶意软件删除"是签名/公证/隔离属性链条有缺口导致的 Gatekeeper/XProtect 拦截。区别于单文件二进制,.app是目录树,签名必须递归覆盖每一层。修复三层:
- 递归签名:
codesign --deep覆盖可执行、dylib、framework、资源,清单不漏项; - 公证 + staple:上传 Apple 拿 ticket 并钉进 app,离线可过 Gatekeeper;
- 清 quarantine + 齐 entitlements:分发时清隔离属性,签名时声明应用所需权限。
核心原则:.app的安全校验看的是整个 bundle 的完整性,不是主可执行文件一个。递归签名、公证、正确的 entitlements,三者齐备,用户下载打开才不会被系统当成恶意软件删掉。
