- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
导读
本文基于 Operit 仓库的 APK V3 签名轮换实施计划,完整讲解 Android 应用在旧发布证书泄露的背景下,如何借助 APK Signature Scheme v3 的证明轮换(proof-of-rotation)机制,在不影响 Android 8 / 8.1 存量用户更新的前提下,把 Android 9 及以上设备平滑迁移到新证书。读完本文,你将掌握:Release 与 Nightly 保持同一正式包名的"旧 V2 + 新 V3 双签"完整配置与命令、lineage 文件的作用与生成、Debug 独立包名与测试密钥的隔离方案,以及整套方案的验收方法与源码级原理。
背景与问题定义
Operit 线上发布的 APK 此前仅使用旧发布证书的 V2 签名。该旧证书已经泄露,属于必须立即处置的安全事件。然而直接切换签名证书会带来一个致命问题:Android 对 APK 签名有严格的继承约束——新签名的 APK 必须能被当前已安装版本识别为"同源更新",否则用户无法直接升级,只能卸载重装,导致数据丢失。
Android 系统对签名方案的选取策略如下:
- Android 8(API 26)与 8.1(API 27):只理解 V1 / V2 签名,不理解 V3 轮换链;
- Android 9(API 28)及以上:是第一个能够选择 V3 签名块并理解 proof-of-rotation(轮换证明)的平台。
因此迁移策略必须是分层的:低版本设备继续沿用旧证书的更新链,高版本设备通过 V3 轮换链过渡到新证书。这正是本计划的核心思路:
Release 与 Nightly 保持同一正式包名
com.ai.assistance.operit,执行旧 V2、新 V3 双签;Debug 使用独立包名与测试密钥,与正式版并行安装。
计划分为两个实施步骤,均已标记为 [DONE]:
- Release 与 Nightly 双签
- Debug 独立包名与测试密钥
总体方案架构
整套轮换方案在 app/build.gradle.kts 中落地,涉及三块核心资产:
| 角色 | 密钥资产 | 用途 |
|---|---|---|
| 旧发布证书 | RELEASE_STORE_FILE指向的 keystore | 为 V2 签名提供旧签名者(legacy signer),服务 Android 8 / 8.1 |
| 新发布证书 | APK_ROTATION_NEW_STORE_FILE指向的.p12 | 为 V3 签名提供新签名者,服务 Android 9+ |
| lineage 文件 | APK_ROTATION_LINEAGE_FILE指向的.lineage | 记录从旧证书指向新证书的轮换链,是 V3 proof-of-rotation 的依据 |
双签后的 APK 同时携带 V2 与 V3 两块签名:低版本系统读取 V2 块中的旧签名者,高版本系统读取 V3 块并沿 lineage 验证"旧 → 新"的合法轮换。
步骤一:Release 与 Nightly 双签
旧实现 vs 新实现
旧实现(改造前):
tools/hotbuild/nightly_auto.py构建 Release 或 Nightly APK 后直接使用 Gradle 产物,不做任何二次签名;- Release 使用当前发布密钥;Nightly 使用 debug 密钥;
- 没有任何 APK Signature Scheme v3 轮换链。
新实现(改造后):
- 脚本构建 Release 或 Nightly 后,以旧发布密钥写入 V2 签名;
- 以新发布密钥与已生成 lineage 写入 V3 签名;
- Android 8 与 8.1 保持旧证书更新链;Android 9 及以上迁移到新证书更新链;
- Debug 不进入正式 APK 的双签任务。
修改作用域
已修改:
app/build.gradle.kts—— 新增轮换签名配置读取、signApkWithRotation函数与两个签名任务;local.properties.example—— 新增新证书与 lineage 的配置模板;.gitignore—— 新增对 keystore、p12、lineage 等敏感文件的忽略规则。
不修改:App 业务代码、Gradle build type 定义、tools/hotbuild/nightly_auto.py(它继续调用原有的 Gradle assemble 任务)、Debug 签名策略(由步骤二处理)、GitHub Actions 发布流程。
密钥与 lineage 配置
构建机本地配置位于 local.properties.example(实际使用时复制为local.properties,该文件已被 .gitignore 忽略):
# 旧发布签名者:用于 V2,服务 Android 8/8.1 更新链 RELEASE_STORE_FILE=C:/secure/operit/release.keystore RELEASE_STORE_PASSWORD= RELEASE_KEY_ALIAS= RELEASE_KEY_PASSWORD= # 新签名者与 lineage:用于 V3,服务 Android 9+ 更新链 APK_ROTATION_NEW_STORE_FILE=C:/secure/operit/operit-release-2026.p12 APK_ROTATION_NEW_STORE_PASSWORD= APK_ROTATION_NEW_KEY_ALIAS=operit-release-2026 APK_ROTATION_NEW_KEY_PASSWORD= APK_ROTATION_LINEAGE_FILE=C:/secure/operit/operit-release-2026.lineage注意:新证书使用PKCS12格式(.p12),这是 apksigner 轮换命令中的--ks-type PKCS12所要求的。
对应的忽略规则(.gitignore):
*.keystore *.jks *.p12 *.lineage /local.properties local.properties所有密钥文件与轮换链文件均不入库,防止泄露事故再次发生。
配置读取与强校验
app/build.gradle.kts 定义了ApkRotationSigningConfig数据类与loadApkRotationSigningConfig()加载函数,对配置做了层层强校验:
requiredLocalProperty(name):要求local.properties必须定义对应属性,缺失直接抛出require异常,报错文案为local.properties must define ... for Release/Nightly APK rotation signing;configuredFileProperty(name):解析路径(相对路径基于仓库根目录解析),并强制校验指向的文件真实存在;sdk.dir必须指向合法目录,且build-tools/35.0.0下的apksigner(Windows 下为apksigner.bat)必须存在。
这一层校验确保双签任务只在"密钥与工具链齐备"的机器上运行,避免半成品签名被误发布。
核心:apksigner 轮换签名命令
signApkWithRotation()(app/build.gradle.kts)是整条链路的执行核心。它以 Gradle 产出的 APK 为输入,先输出到同级临时文件.${name}.rotation-signing(若已存在则拒绝覆盖,防止误毁产物),签名完成后用apksigner verify --verbose --print-certs自我校验,最后才Files.move覆盖回原文件。
实际执行的命令等价于:
apksigner sign \ --in app-release.apk \ --out .app-release.apk.rotation-signing \ --min-sdk-version 26 \ --v1-signing-enabled false \ --v2-signing-enabled true \ --v3-signing-enabled true \ --v4-signing-enabled false \ --lineage operit-release-2026.lineage \ --rotation-min-sdk-version 28 \ --ks release.keystore --ks-type PKCS12 --ks-key-alias <旧别名> \ --ks-pass env:OPERIT_OLD_STORE_PASSWORD --key-pass env:OPERIT_OLD_KEY_PASSWORD \ --next-signer \ --ks operit-release-2026.p12 --ks-type PKCS12 --ks-key-alias operit-release-2026 \ --ks-pass env:OPERIT_NEW_STORE_PASSWORD --key-pass env:OPERIT_NEW_KEY_PASSWORD关键参数逐一拆解:
| 参数 | 取值 | 含义 |
|---|---|---|
--min-sdk-version 26 | 26 | 与项目minSdk = 26对齐,V2 签名覆盖 Android 8+ 全版本 |
--v1-signing-enabled false | false | 关闭 V1(JAR)签名,仅 V2/V3 |
--v2-signing-enabled true | true | 旧签名者以 V2 块落盘,服务 API 26/27 |
--v3-signing-enabled true | true | 新签名者以 V3 块落盘,服务 API 28+ |
--v4-signing-enabled false | false | 不生成 V4 增量签名 |
--lineage | lineage 文件 | 提供旧 → 新的轮换证明 |
--rotation-min-sdk-version 28 | 28 | 轮换生效的最低平台版本,见下方原理 |
--next-signer | 分隔符 | 宣告从旧签名者切换到新签名者 |
--ks-pass/--key-pass | env:OPERIT_* | 密码通过环境变量注入,避免出现在进程列表中 |
签名者顺序为"旧 → 新":第一个--ks是旧证书(写 V2),--next-signer之后是新证书(写 V3)。
为什么轮换门槛是 API 28
代码中的注释揭示了关键设计(app/build.gradle.kts):
API 28 is the first platform that selects V3 and understands proof-of-rotation; API 26/27 therefore continue to select the old signer from the V2 block.
即:Android 9(API 28)是首个能识别 V3 块并理解轮换证明的平台。通过--rotation-min-sdk-version 28,我们显式声明"轮换链只对 API 28+ 生效";API 26/27 设备因不理解 V3,仍会回退到 V2 块中的旧签名者继续验证,从而保住 8.0/8.1 用户的升级路径。这正是整个方案能在"证书已泄露"前提下不丢存量用户的根基。
Gradle 任务接入
在signApkWithRotation之上,app/build.gradle.kts 注册了两个分布任务并挂接到 assemble 链上:
val signRotatedReleaseApk by tasks.registering { description = "Signs the Release APK with the legacy V2 signer and rotated V3 signer." group = "distribution" dependsOn("packageRelease") doLast { signApkWithRotation(build/outputs/apk/release/app-release.apk) } } val signRotatedNightlyApk by tasks.registering { description = "Signs the Nightly APK with the legacy V2 signer and rotated V3 signer." group = "distribution" dependsOn("packageNightly") doLast { signApkWithRotation(build/outputs/apk/nightly/app-nightly.apk) } } tasks.matching { it.name == "assembleRelease" }.configureEach { finalizedBy(signRotatedReleaseApk) } tasks.matching { it.name == "assembleNightly" }.configureEach { finalizedBy(signRotatedNightlyApk) }即开发者/CI 执行./gradlew assembleRelease或assembleNightly后,自动触发双签任务。Nightly 变体在 buildTypes 中定义(app-nightly.apk输出名),并使用 debug 证书作为 Gradle 阶段的初始签名,随后由双签任务统一替换为"旧 V2 + 新 V3"。Release 变体若在local.properties中配置了发布密钥,则使用发布签名配置作为初始签名。
nightly_auto.py 的配合
tools/hotbuild/nightly_auto.py 是热构建发布脚本,本方案明确不改动它,它继续走原有的 Gradle 调用链:
- 从 app/build.gradle.kts 的
versionName解析目标版本(支持1.12.1+6这类补丁号格式); - 无补丁号 → 走 Release 线,执行
:app:assembleRelease,产物app-release.apk; - 有补丁号 → 走 Nightly 线,执行
:app:assembleNightly,产物app-nightly.apk; - 调用时跳过
lintVitalReport*任务,仅取 APK 产物,并生成from.apk/to.apk供 tools/hotbuild/build_patch.py 做增量补丁。
由于双签通过finalizedBy挂在 assemble 任务之后,nightly_auto.py 在不知情的情况下即可拿到"已双签"的正式产物,发布流程零侵入。
验收标准(步骤一)
- Release 与 Nightly 产物均报告V2、V3 签名有效;
- V2 使用当前发布证书(旧证书);
- V3 lineage 从当前发布证书指向新发布证书;
- Debug 产物不进入双签流程。
手工验证命令(apksigner 位于$ANDROID_HOME/build-tools/35.0.0/):
apksigner verify --verbose --print-certs app-release.apk输出中应能同时看到 V2 块的旧证书指纹与 V3 块携带 lineage 的新证书指纹。
步骤二:Debug 独立包名与测试密钥
旧实现的问题
改造前 Debug 构建存在三个痛点:
- Debug 使用正式
applicationId,与正式版同包名; - 配置到本地正式密钥时,Debug 直接用正式密钥签名;
- 因此Debug 与正式版不能并行安装,且动态快捷方式固定指向正式包名,调试时容易误触正式版入口。
意图修正
旧 Debug APK 虽已发布,但发布规范不完整——协作者直接安装正式版覆盖旧 Debug,不保留旧 Debug 的更新或数据迁移路径。本步骤就是要把 Debug 彻底从正式发布链中剥离出来。
新实现
- Debug 的 application ID 为
com.ai.assistance.operit.debug; - Debug 使用Android 默认 debug keystore(
signingConfigs.debug,与发布证书完全不同); - Debug 启动器名称为
Operit Debug; - Debug 快捷方式只打开 Debug 包;
- 正式版、Nightly 与 Debug 可以并行安装。
对应实现位于 app/build.gradle.kts:
debug { applicationIdSuffix = ".debug" signingConfig = signingConfigs.getByName("debug") resValue("string", "app_name", "Operit Debug") }applicationIdSuffix = ".debug"使正式包名com.ai.assistance.operit变为com.ai.assistance.operit.debug,与正式版共存;resValue覆盖应用名称为Operit Debug,桌面上即可区分。顺带一提,仓库还定义了clone变体(applicationIdSuffix = ".clone",应用名Operit Clone),用于并行安装的测试场景。
快捷方式指向修正
app/src/main/res/xml/shortcuts.xml 中的三个动态快捷方式原本全部指向正式包名(com.ai.assistance.operit的MainActivity/OperitAssistActivity/DataRecoveryActivity)。改造后,Debug 资源目录新增独立的 app/src/debug/res/xml/shortcuts.xml,把三个快捷方式的targetPackage全部改为com.ai.assistance.operit.debug,其余 action 与 targetClass 保持不变。这样 Debug 构建中快捷方式只会打开 Debug 包,不会误启动正式版。
修改作用域
已修改:
app/build.gradle.ktsapp/src/debug/res/xml/shortcuts.xml
不修改:正式版与 Nightly 的 application ID、正式版与 Nightly 的轮换签名、旧 Debug APK 的更新链和用户数据(旧 Debug 已不再维护升级路径,属于有意放弃)。
验收标准(步骤二)
- Debug APK 的 package 为
com.ai.assistance.operit.debug; - Debug 签名证书不同于正式发布证书;
- Debug 与正式版可同时安装(并行共存);
- Debug 快捷方式指向
com.ai.assistance.operit.debug。
验证命令:
# 查看 Debug APK 包名 aapt dump badging app-debug.apk | grep package # 查看 Debug 签名证书(应与正式证书指纹不同) apksigner verify --print-certs app-debug.apk安全设计要点小结
- 密码永不落盘:双签命令通过
env:OPERIT_OLD_STORE_PASSWORD、env:OPERIT_NEW_STORE_PASSWORD等环境变量注入(app/build.gradle.kts),避免密码出现在命令行参数与进程列表中; - 密钥文件全部 gitignore:
.keystore、.jks、.p12、.lineage、local.properties均不入库(.gitignore),从源头防止再次泄露; - 产物防覆盖:轮换输出先写临时文件,存在即拒绝执行,避免重复构建误毁有效签名;
- 签名后自校验:
apksigner verify --verbose --print-certs内嵌在执行流中,签名结果不经过人工目检即可得到强校验; - 分层迁移:
--rotation-min-sdk-version 28把轮换限制在 API 28+,API 26/27 保持旧证书 V2 链,兼顾安全与存量用户。
结语
Operit 的 APK V3 签名轮换方案,以local.properties承载双证书与 lineage 配置、以apksigner的--next-signer完成"旧 V2 + 新 V3"双签、以finalizedBy零侵入接入 Release/Nightly assemble 链,最后通过 Debug 独立包名 + 测试密钥 + 独立快捷方式实现构建隔离。这套"证书泄露紧急处置 + 多构建渠道隔离"的组合,是 Android 分发安全治理中可复用的完整样板:既保住了 Android 8/8.1 存量用户的升级路径,又把 Android 9+ 用户平滑迁移到新证书,同时让日常调试不再污染正式发布环境。两个实施步骤的完整记录与验收条目可在 01_NightlyAndReleaseDualSigning.md 与 02_DebugPackageAndTestKey.md 中查阅。
- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
相关推荐
GPT-2泰语大语言模型未来路线图:泰语AI发展的五大趋势
GPT 2泰语大语言模型未来路线图:泰语AI发展的五大趋势 在人工智能技术飞速发展的今天, 泰语大语言模型 正成为东南亚地区AI应用的重要基石。gpt2 bas
GoReleaser 升级 Cosign v3 实战指南:用 `--bundle` 取代分离的证书与签名文件
GoReleaser 升级 Cosign v3 实战指南:用 bundle 取代分离的证书与签名文件 GoReleaser 的签名管线( signs / bin
开发工具CI/CD构建工具electron-builder 密钥轮换实战:Ed25519 更新清单签名与 Windows/macOS/Linux 代码签名证书的平滑过渡
electron builder 密钥轮换实战:Ed25519 更新清单签名与 Windows/macOS/Linux 代码签名证书的平滑过渡 导读 本文是一份
构建工具桌面应用开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考