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):
- 只接受 HTTPS:唯一的例外是测试构建通过环境变量
SEARCH_FEED指向自己的测试 feed;正式版无论怎么启动都只认官网 feed - 同域校验:ZIP 与 DMG 的地址必须和 feed 本身在同一个主机上——即使 feed 主机被攻陷或 DNS 被劫持,它也无法把"去这里下载"的指引指到别的地址
- 缺校验值即拒绝:
build.sh发布时一定会写入 SHA256,所以一个缺少校验和的 feed 会被当作"不是我们的 feed"直接拒收
核心环节:Developer ID 签名校验
拿到 ZIP 后,Swap.install 会依次执行"下载 → 比对哈希 → 用ditto解压 → 签名校验 → 替换",任何一步失败都整体放弃。签名校验(verify)是整套机制的信任基石:
- 是同一个应用:新包的
CFBundleIdentifier必须与当前运行的 Search 完全一致 - 确实更新:新包的构建号(build number)必须严格大于当前版本,防止"回滚更新"
- 严格签名检查:用
SecStaticCodeCheckValidity执行kSecCSStrictValidate | kSecCSCheckAllArchitectures | kSecCSCheckNestedCode——对每个架构、包括所有嵌套代码做严格验证 - 证书链必须落在 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:
- 把当前的
Search.app改名为Search.app.old(旁路暂存) - 把新包 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),仅供参考