☰
Search 浏览器如何静默自我更新?带 Developer ID 签名校验的更新器全解析
2026/9/26 22:00:56 网站建设 项目流程

Search 浏览器如何静默自我更新?带 Developer ID 签名校验的更新器全解析

【免费下载链接】SearchA small, fast WebKit browser for macOS, by Office Commun.项目地址: https://gitcode.com/gh_mirrors/search59/Search

Search 是一款运行在 macOS 上的轻量 WebKit 浏览器(约 3 MB,由设计工作室 Office Commun 打造)。它内置了一个"静默自我更新"机制:每天在后台检查一次新版本,静默下载更新包,并通过 SHA256 哈希与 Developer ID 签名校验确认来源可信后,悄悄替换到原安装位置——下次启动即生效,全程不打断你正在浏览的页面。本文带你完整拆解这套带 Developer ID 签名校验的更新器是如何工作的。

没有框架、没有守护进程:一个 JSON 文件搞定一切

传统 macOS 应用的自动更新往往依赖第三方更新框架或常驻后台的守护进程。Search 的更新器反其道而行,源码注释把立场写得很直白(Updater.swift):

  • ❌ 不用任何更新框架,不运行任何后台守护进程
  • ✅ 只在下载页旁边放一个小小的appcast.json文件,记录最新版本号、ZIP 包地址、SHA256 校验值
  • ✅ 每天读一次、用户手动点"检查"时再读一次;若文件里提到的构建比当前更新,就把对应的 ZIP静默下载、检查、放进应用所在目录,下次打开时就是新版本
  • ✅ 永远不会自己重启应用——"你正在读的页面,不会因为浏览器想变新而被中断"

这是"Chrome 的思路,没有 Chrome 的机器"。

检查节奏:一天一次,开着就每小时看一次

自动检查由 checkIfDue 控制,规则很克制:

场景行为
每次启动距上次检查超过20 小时才真正去读 feed,否则跳过
应用常驻运行挂一个每小时触发一次的计时器(容忍 5 分钟),防止"开着浏览器一周就永远不检查"
手动触发设置 › 关于 里有"Check now"按钮,点一下立即检查

![说明:] 每次检查完都会记录时间,在设置页显示"X 前检查过——每天自动一次"(Settings.swift)。

下载前的三道安全闸门 🔒

在真正下载 ZIP 之前,更新器会先对 feed 里的地址做严格把关(link):

  1. 只接受 HTTPS:唯一的例外是测试构建通过环境变量SEARCH_FEED指向自己的测试 feed;正式版无论怎么启动都只认官网 feed
  2. 同域校验:ZIP 与 DMG 的地址必须和 feed 本身在同一个主机上——即使 feed 主机被攻陷或 DNS 被劫持,它也无法把"去这里下载"的指引指到别的地址
  3. 缺校验值即拒绝:build.sh发布时一定会写入 SHA256,所以一个缺少校验和的 feed 会被当作"不是我们的 feed"直接拒收

核心环节:Developer ID 签名校验

拿到 ZIP 后,Swap.install 会依次执行"下载 → 比对哈希 → 用ditto解压 → 签名校验 → 替换",任何一步失败都整体放弃。签名校验(verify)是整套机制的信任基石:

  1. 是同一个应用:新包的CFBundleIdentifier必须与当前运行的 Search 完全一致
  2. 确实更新:新包的构建号(build number)必须严格大于当前版本,防止"回滚更新"
  3. 严格签名检查:用SecStaticCodeCheckValidity执行kSecCSStrictValidate | kSecCSCheckAllArchitectures | kSecCSCheckNestedCode——对每个架构、包括所有嵌套代码做严格验证
  4. 证书链必须落在 Apple:要求串(developerID)要求证书链以Apple 根证书为锚点、经过 Apple 的Developer ID 中间机构、叶子为 Developer ID Application 证书,且subject.OU必须等于正在运行的这个应用的 Team ID

源码里有一句很关键的话:"从签名里读出的 Team ID 只是证书自己声称的东西,而任何人都能造一张那样声称的证书"——所以必须走 Apple 锚点的完整链校验,而不是只读 Team ID。

💡自编译版本永远不会被自动更新:自己swift build出来的构建是 ad-hoc 签名,没有 Team ID,更新器会直接以unsignedHere拒绝——一个无法与任何别的东西区分开的应用,绝不接受"换壳"。

原子替换:两次重命名,且绝不自动重启 🔄

通过全部检查后,替换(swap)只靠同一磁盘卷上的两次 rename:

  1. 把当前的Search.app改名为Search.app.old(旁路暂存)
  2. 把新包 rename 到正式位置;若第二步失败,立即撤销第一步,应用原封不动

几个精妙之处:

  • rename 而非复制,同卷操作几乎是瞬时且原子的
  • 正在运行的进程不受影响——内核会跟着 rename 走,旧版本继续从.old运行,且它的签名身份没变,钥匙串项对它依然有效
  • 更新器只删除自己产生的东西:临时目录和那个.old包。.old的清理(sweep)只在退出时、以及下次启动时(应对上次非正常退出)进行,且会先确认它确实是本应用才动手

你的数据,一个字都不会碰

更新器有意"放过"了一切用户数据(Updater.swift 注释):

  • ~/Library/Application Support/Search里的会话、历史、书签、隐藏元素 →不读、不搬、不重写
  • com.officecommun.search的默认设置、钥匙串 →原封不动
  • 新旧包保持相同的 bundle id 和签名身份,因此旧构建存进钥匙串的密码,新构建开箱即读
  • 换用新版本的时机完全由你决定:要么下次自然启动生效,要么在设置里点"Relaunch now"(实现上用一个 shell 等待本进程退出后再open应用,relaunch)

失败兜底与用户控制权

情况更新器的应对
安装目录不可写(如只读挂载)切换为offered状态:在应用内提示"新版本已发布",附 DMG 下载按钮——和第一次安装一样
设置中关闭"Install updates on its own"(Settings.swift)默认开启;关掉后更新器只告知(Search x.x 已发布——在设置里),等你手动点 Install
feed 声明的最低系统版本高于本机该版本对你"不算更新",直接不展示

发布侧:三个文件如何严丝合缝配对

更新能"静默"成立,靠的是发布流水线的配合:

  • build.sh:产出Search.dmg(给人)+Search.zip(给更新器),并同时生成appcast.json——版本、构建号、两个下载地址、ZIP 的 SHA256、更新说明、最低系统版本,一个字段不少
  • publish.sh:把这三个文件原样拷到官网目录;文件名永不改变,所以站点链接、更新器的 feed 地址都永远有效
  • 公证策略:DMG 会被公证并 staple(首次打开即通过 Gatekeeper);ZIP 则"交给更新器自己把关"——哈希 + 签名校验就是它的 Gatekeeper(build.sh 注释)

小结

Search 的更新器是"小而自洽"的典范,值得每个做 macOS 应用的人参考:

环节做法换来什么
检查一天一次 + 常驻每小时一次不打扰、不漏查
传输单文件 JSON feed,HTTPS 同域无劫持面
信任SHA256 + Developer ID 全链严格签名 + Team 匹配换壳攻击不成立
替换同卷两次原子 rename,失败回滚应用永不损坏
数据只动 bundle,不碰设置与钥匙串密码、历史无缝继承
控制权从不自动重启,关自动安装则只提示用户始终在方向盘后

想深入了解更新相关的安全边界(比如更新通道本身属于漏洞报告范围),可以阅读 SECURITY.md;整体架构与设计取舍则见 README.md。

【免费下载链接】SearchA small, fast WebKit browser for macOS, by Office Commun.项目地址: https://gitcode.com/gh_mirrors/search59/Search

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询