先看结论与判断条件
- 仅凭 apksigner verify 成功不能确认签名者是组织发布证书,必须将提取的证书 SHA-256 指纹与独立可信来源的预期摘要做比对。
- 工程判断:当前发布流程可要求 APK 至少通过 v2 验证;需要兼容旧系统时同时核对 v1,但不能把 v1 通过作为唯一接受条件。
- 签名有效的 APK 仍可能来源于非授权构建环境,引入 SLSA 构建证明或 in-toto 声明并将产物摘要绑定到签名者身份,可以弥补单一签名验证的不足。
- 所有比对条件(指纹、方案、来源证明)形成整体门禁,任一条件不满足流水线应立即失败,不允许手动跳过。
- apksigner 和构建证明的验证边界清晰:它们不能保护密钥材料保管、不能证明代码无恶意行为,也不能替代商店托管签名流程。
- 脚本必须采用只读模式,通过环境变量传入预期值,任何格式差异或匹配失败均需返回非零退出码以触发流水线阻断。
CI 中缺失签名者身份校验的供应链风险
持续集成环境中,若签名后的 APK 未经严格验证就直接归档或发布,极易因证书错配、构建环境泄露或供应链攻击而引入签名身份错误。攻击者可能篡改产物并用另一套证书重新签名,重打包后的 APK 签名有效性仍然可以通过常规校验,但证书已不属于发行组织,导致用户设备拒绝更新或无法识别应用身份。这种风险在未实施证书指纹强制核对的流水线里难以在常规自动化检查中发现。
传统流程往往依赖发布前人工检查,或等上传商店后再看报告,导致 CI 阶段无法自动拦截。若 CI 只测安装启动,不核对证书,则即使用了错误测试密钥,构建结果也会显示通过。等到应用上线才发现签名错配,只能重新构建发版,造成合规与时间损失,且无法追溯具体是哪个环节出现了密钥混用。
把证书摘要和签名方案作为硬门禁引入 CI,能将原本依赖人工的审核点前移至每次构建。门禁不应仅检查签名算法是否正确,还必须严格比对证书指纹,并验证该指纹与密钥管理系统中记录的发布证书摘要完全相同。同时,门禁需核查签名方案是否满足预定的版本基线,防止攻击者利用旧方案漏洞保留一个有效但不可信的签名身份。这种自动化的证书指纹核对机制才是供应链中可信赖的签名身份校验。
| 风险场景 | 常规校验表现 | 实际隐患 | 业务影响 |
|---|---|---|---|
| 测试密钥误用于生产构建 | apksigner verify 返回成功 | 证书指纹与发布记录不符 | 用户无法覆盖安装,更新链断裂 |
| 构建机被入侵并重签名 | 签名数学有效性验证通过 | 签名者身份变为攻击者控制 | 恶意代码以合法应用身份分发 |
| 仅使用 v1 签名方案 | 旧设备可正常安装运行 | ZIP 结构可被追加文件而不报错 | 应用包体被植入广告或后门 |
| 多套证书混用且未隔离 | 构建日志无明显报错 | 不同版本应用签名不一致 | 应用商店审核拒绝或下架处理 |
- 确认 CI 流水线中是否存在证书指纹自动比对步骤
- 检查构建日志是否明确输出了签名证书的 SHA-256 摘要
- 验证预期证书摘要是否来自独立的密钥管理服务而非硬编码
- 评估当前流程是否允许人工跳过签名校验环节
Android 签名方案的作用域与验证边界
Android 使用多个签名方案来保护 APK 的不同区域。v1 方案基于 JAR 签名,通过 META-INF 内的摘要文件验证 ZIP 条目完整性,但允许添加新文件而不破坏现有签名。v2 方案从 Android 7.0 开始引入,覆盖整个 APK 文件的内容签名,使任何 ZIP 结构变动都会导致校验失败。v3 方案增加了密钥轮换能力,让应用可以在更新时变更签名密钥。这些方案并存意味着 APK 可能混合使用多个版本,CI 门禁必须了解其覆盖范围和遗留缺陷。
根据 AOSP app signing 的说明,签名方案建立应用更新时的身份关系。v2 和 v3 覆盖的文件区域更完整,但签名数学上正确只说明某个证书签署了当前 APK,并不能证明该证书持有者已获授权发布。CI 因此要把工具结果与独立维护的证书指纹基线一起核对。
对签名方案的校验还存在平台兼容性约束。v2 方案需要至少 API 24,v3 需要 API 30 及以上,而部分旧设备仍只能验证 v1 签名。CI 门禁在设计最低方案要求时,不能脱离目标 API level 和发布渠道要求。工程判断是:除非面向极旧系统,否则应将 v2 设为底线,因为 v1 允许追加文件且不影响签名有效性,容易被恶意利用植入广告或代码。门禁脚本可通过 apksigner --verbose 输出中是否出现特定方案字样来做决策,如果缺失则标记失败。
| 签名方案 | 引入 API | 防护范围 | CI 门禁推荐策略 |
|---|---|---|---|
| v1 (JAR) | API 1 | 仅保护 META-INF 内声明的文件摘要,允许追加文件而不破坏签名 | 不满足最低安全要求,门禁应拒绝仅含 v1 签名的 APK |
| v2 (APK Signing Block) | API 24 | 覆盖整个 APK 文件完整性,任何 ZIP 改动都会导致校验失败 | 作为必选基线,门禁强制要求至少包含 v2 |
| v3 (APK Signing Block 带密钥轮换) | API 30 | 在 v2 基础上引入密钥轮换证明,支持应用安全地更换签名证书 | 需要轮换的场景下必须额外检查 v3 存在,否则可根据发布策略豁免 |
| v4 (增量安装签名) | API 30+ | 支持增量安装但需辅助文件,不单独作为身份凭证 | 不单独作为门禁条件,若有可校验其与 v2/v3 签名者一致 |
- 读取 APK 的 minSdkVersion,避免强制要求未支持的方案
- 检查 apksigner --verbose 输出中所有已验证的方案列表
- 若仅出现 v1 或缺少预期方案,立即失败并记录缺失的方案名称
- 确认构建配置中是否显式启用了 v2SigningEnabled 和 v3SigningEnabled
通过 apksigner 获取并比对证书摘要的步骤
apksigner verify --print-certs 能够安全地从给定 APK 中提取签名证书的 MD5、SHA-1 和 SHA-256 指纹,而不需要访问任何私钥材料。命令解析 APK 的签名块,找出签名者证书链,并在标准输出中打印。对于 CI 门禁,我们只关注签署 APK 的实体证书指纹,即签名者证书,而不是整个证书链中的中间证书。通常一个 APK 只有一个签名者,若出现多个,门禁应判定异常并将所有指纹输出以供审计。
比对前,将预期证书 SHA-256 摘要放在 CI 的受保护变量或只读密钥配置中。脚本不能依据 APK 自身内容反推预期指纹,否则换证后的恶意包也可能生成自己的“基线”。提取值先统一为小写、无冒号的十六进制字符串,再做精确比较;格式化只作用于表现形式,不能接受部分匹配。
若 apksigner --print-certs 输出多个签名证书摘要,脚本应提取全部指纹并与预期集合比较。产品只允许单一签名者时,明确读取第一个签名者并拒绝额外项;允许多个签名者时,则校验完整集合。任一差异都要输出实际值与预期项,并返回非零状态。
- 在脚本开始检查 apksigner 是否可用,若未找到则失败
- 调用 apksigner verify 确保 APK 未被篡改且签名数学有效
- 提取 SHA-256 摘要后与预期的认证指纹逐字符比对
- 发现任何格式不统一(如多余空格或冒号)先标准化再比较
签名方案版本硬门禁的设计与策略
签名方案版本的硬门禁不应仅作为可选检查项。工程上可以为当前渠道规定至少通过 v2 验证,并把这一要求写入版本化配置;需要兼容旧系统的变体再额外验证 v1。本文引用的来源没有直接证明任意 v1 包都可安全追加文件,因此验收结论只依据工具的逐方案输出和既定发布策略。
获取 APK 实际使用的签名方案,最可靠的方式是解析 apksigner verify --verbose 的输出。该命令会逐条列出每个被验证的方案。脚本可以查找包含指定方案标志的行,若不存在则标记缺少。注意某些旧版本 apksigner 可能对 v3 等新方案只显示一句话,需要检查具体版本的行为,避免因工具差异产生误判。工程团队应定期更新构建镜像中的签名工具以保持解析能力。
签名方案的选取必须兼顾应用的 minSdkVersion。支持 API 23 及更早设备时,不能只生成 v2;CI 可要求现代方案通过,同时对需要旧系统兼容的变体核对 v1。每个构建变体的允许方案写入版本化配置,不要在脚本中散落互相矛盾的硬编码。
| 签名方案缺失情况 | 潜在风险 | 门禁行为 | 后续处理 |
|---|---|---|---|
| 仅包含 v1 签名 | 允许追加文件且不被察觉 | 立即阻断并返回错误码 10 | 检查签名步骤是否使用了过时 JDK 签名工具,迁移到 apksigner |
| 缺少 v2 但存在 v1 和 v3 | 中间版本设备无法校验完整性 | 阻断,错误码 11 | 确认签名命令中是否同时启用了 v2 与 v3,通常需同时包含 |
| 签名方案均为 v2/v3,但意外带有多个签名者 | 身份混淆、匿名注入 | 阻断,错误码 12 | 用 apksigner --print-certs 列出所有证书并通知安全团队排查 |
| v4 签名文件存在但签名者与 v2 不一致 | 增量安装验证失败 | 可配置阻断或告警,错误码 13 | 检查 v4 签名生成过程,确保采用相同的密钥 |
- 定义明确的最低签名方案基线(如 v2)
- 编写正则表达式准确匹配 apksigner verbose 输出中的方案标识
- 为不同类型的方案缺失配置不同的退出码以便快速定位
- 在构建文档中记录例外情况的审批流程
将构建来源证明纳入签名者信任链
仅靠证书指纹比对和签名方案校验,仍不能确保 APK 是由授权的构建环境所产出。如果构建服务器被攻破,攻击者可能窃取签名材料并使用合法证书签名任意恶意 APK。在这种情况下,所有数字签名校验都会通过,但实际内容不可信。SLSA Provenance v1.1 和 in-toto Attestation Statement v1 提供了解决方案:它们把构建产物的摘要、构建者身份、外部参数和材料依赖绑定在一条可验证的证明记录中,门禁可以一并校验这些证明。
引入构建来源证明的硬门禁意味着在 CI 流程中,除了对 APK 签名做 apksigner 检查,还需要验证与 APK 对应的 in-toto 证明文件。证明主体中声明的 subject digest 必须与 APK 文件的 SHA-256 完全一致,同时证明的签名者应关联到组织的构建系统身份。这样,即使攻击者拿走了签名密钥,没有可信构建证明的 APK 也将被门禁拒绝。需要注意,证明的签名者校验必须独立于 APK 签名,不能仅凭证明文件自述信任。
先验签名再验证明;任一失败即退出码非零并将差异写入 stderr。可在 CI 阶段将 APK、签名后摘要和 in-toto 证明一并生成或从构建系统拉取,然后由门禁脚本依次校验。校验顺序很重要:先通过 apksigner 验证 APK 未被修改且证书正确,再验证构建证明中声明的主体摘要匹配,并核实证明的签名者是否在允许列表中。这种顺序确保了基础完整性优先于来源可信度。
- 确保 APK 的 SHA-256 与 in-toto subject 中 digest 完全匹配
- 验证证明签名者身份密钥与构建系统绑定,且在信任列表中
- 检查证明中的构建类型和外部参数是否与预期流水线一致
- 防止在缺少证明时仅凭 APK 签名予以放行
CI 脚本实现:参数设计、退出码与错误诊断
实现证书摘要、签名方案和构建来源硬门禁的脚本应采用无状态只读模式:仅读取传入的 APK 文件、可选的构建证明和环境变量,不修改文件系统上的任何产物。脚本至少需要两个输入参数:待校验 APK 的路径和预期证书 SHA-256 指纹。可选参数包括要求的最低签名方案列表和构建证明文件路径。所有外部判据应通过环境变量或命令行参数传入,绝不可硬编码在脚本体内。
退出码设计需要为不同的失败原因分配唯一错误码,帮助 CI 平台快速分类问题。例如,证书指纹不匹配返回 4,签名方案缺失返回 5,构建证明主体摘要不一致返回 6,apksigner 基础校验失败返回 2。错误码 1 保留给脚本自身调用异常,0 表示全部检查通过。脚本在失败时必须向 stderr 输出包含检查项、预期值和实际值的消息,便于开发者在构建日志中定位根因。
在 CI 环境中调用该脚本通常无需完整的 Android SDK,只需确保 apksigner 工具的路径在 PATH 中或通过环境变量指定。可以将 apksigner 二进制随构建镜像分发。脚本内应首先验证 apksigner 是否存在且可执行,然后执行 verify 命令,再按顺序进行证书摘要比对、签名方案检查和构建证明校验,任意步骤失败立即退出。确保脚本在任意步骤失败时立即退出,避免人工干预导致异常产物流入下游。
| 配置项 | 来源 | 示例值 | 缺失时行为 |
|---|---|---|---|
| EXPECTED_CERT_SHA256 | CI 安全变量或密钥管理服务 | a1b2c3d4... | 立即失败,不可默认 |
| REQUIRED_SIGNING_SCHEMES | 流水线配置文件 | v2,v3 | 若不提供,默认至少要求 v2 |
| ATTESTATION_FILE | 构建产物或远程拉取 | /ci/attestations/build.it.json | 若整个文件缺失且门禁开启,则阻断 |
| APKSIGNER_PATH | 构建工具路径 | /usr/local/bin/apksigner | 尝试从 PATH 查找,若均失败则退出 1 |
#!/bin/bash
set -euo pipefail
# 输入参数校验
if [ $# -lt 2 ]; then
echo "Usage: $0 <apk_path> <expected_sha256>" >&2
exit 1
fi
APK="$1"
EXPECTED_SHA256="$2"
# 环境变量默认值设置
: "${REQUIRED_SIGNING_SCHEMES:=v2,v3}"
: "${ATTESTATION_FILE:=}"
: "${APKSIGNER_PATH:=apksigner}"
# 检查 apksigner 工具可用性
if ! command -v "$APKSIGNER_PATH" &>/dev/null; then
echo "ERROR: apksigner not found at $APKSIGNER_PATH" >&2
exit 1
fi
# 第一步:验证 APK 完整性
echo "--- Verifying APK integrity ---"
if ! "$APKSIGNER_PATH" verify --verbose "$APK" > /tmp/apk_verify.log 2>&1; then
echo "ERROR: apksigner verify failed" >&2
cat /tmp/apk_verify.log >&2
exit 2
fi
# 第二步:提取签名证书 SHA-256
echo "--- Extracting signing certificate SHA-256 ---"
CERT_DIGEST=$("$APKSIGNER_PATH" verify --print-certs "$APK" 2>/dev/null | grep -E '^Signer #1 certificate SHA-256 digest:' | awk '{print $NF}')
if [ -z "$CERT_DIGEST" ]; then
echo "ERROR: could not extract certificate SHA-256 digest" >&2
exit 3
fi
# 第三步:标准化并比对证书指纹
echo "--- Comparing certificate digest ---"
EXPECTED_NORM=$(echo "$EXPECTED_SHA256" | tr '[:upper:]' '[:lower:]' | tr -d ':')
ACTUAL_NORM=$(echo "$CERT_DIGEST" | tr '[:upper:]' '[:lower:]' | tr -d ':')
if [ "$ACTUAL_NORM" != "$EXPECTED_NORM" ]; then
echo "ERROR: certificate SHA-256 mismatch" >&2
echo "Expected: $EXPECTED_NORM" >&2
echo "Got: $ACTUAL_NORM" >&2
exit 4
fi
# 第四步:验证签名方案
echo "--- Verifying signing schemes ---"
SCHEME_LINES=$(grep -oP 'Verified using v\d+ scheme' /tmp/apk_verify.log || true)
if [ -z "$SCHEME_LINES" ]; then
echo "ERROR: no signing scheme detected" >&2
exit 5
fi
IFS=',' read -ra SCHEMES_REQ <<< "$REQUIRED_SIGNING_SCHEMES"
for scheme in "${SCHEMES_REQ[@]}"; do
# 去除可能的 'v' 前缀进行匹配
scheme_num="${scheme#v}"
if ! echo "$SCHEME_LINES" | grep -q "v${scheme_num}"; then
echo "ERROR: required signing scheme v${scheme_num} missing" >&2
exit 6
fi
done
# 第五步:可选的构建证明校验
if [ -n "$ATTESTATION_FILE" ]; then
echo "--- Validating build provenance ---"
if [ ! -f "$ATTESTATION_FILE" ]; then
echo "ERROR: attestation file not found: $ATTESTATION_FILE" >&2
exit 7
fi
APK_SHA256=$(sha256sum "$APK" | awk '{print $1}')
SUBJECT_DIGEST=$(jq -r '.subject[0].digest."sha256"' "$ATTESTATION_FILE" 2>/dev/null || true)
if [ -z "$SUBJECT_DIGEST" ] || [ "$SUBJECT_DIGEST" != "$APK_SHA256" ]; then
echo "ERROR: attestation subject digest does not match APK" >&2
echo "APK SHA256: $APK_SHA256" >&2
echo "Attestation SHA256: $SUBJECT_DIGEST" >&2
exit 8
fi
echo "Provenance subject digest matches APK."
fi
echo "All signature identity checks passed."
exit 0门禁失败分类与故障排除手册
当 CI 门禁因证书指纹不匹配而失败时,最常见的原因是构建流水线中签名步骤使用了错误密钥文件或密钥别名。这种情况往往发生在同一构建机存在多套签名材料的环境,而脚本未正确选择发布密钥。故障排除应首先检查 CI 日志中提取到的实际证书指纹,并与密钥管理服务中记录的各个发布证书对比,锁定实际签名的证书。若发现指纹属于测试证书,需修正密钥引用。
签名方案缺失的错误通常源于打包工具链问题。例如,某些旧版本的 Gradle Android Plugin 在签名时可能默认不启用 v2 签名,或构建脚本中显式调用了 jarsigner 而未使用 apksigner 签名。诊断时先检查 apksigner --verbose 输出中声明的方案列表,再回溯构建日志寻找签名命令。解决方法是在构建配置中确保使用 apksigner 或 Android Gradle Plugin 的签名配置开启 v2SigningEnabled true 及 v3SigningEnabled true。
证明校验失败通常意味着 APK 与证明不匹配,或证明签名者不受信。应确保证明与 APK 在同一构建步骤内生成,防止中间篡改;证明验证密钥应独立于构建机配置。证明文件中的 subject digest 若与 APK 不同,需检查构建制品是否在证明生成后被二次处理。证明签名者白名单应依托独立于构建环境的密钥发现机制,不能来自构建机自身的配置。
| 失败退出码 | 现象描述 | 直接原因排查 | 根本修复方向 |
|---|---|---|---|
| 2 | apksigner verify 返回非零 | APK 被修改、损坏或签名块缺失 | 重新构建并确保签名后无任何 zip 操作 |
| 4 | 证书 SHA-256 不匹配 | 签名用了非发布密钥 | 切换至正确的 key store 和 alias |
| 6 | 缺少必需签名方案(如 v2) | 构建工具未启用新签名方案 | 启用 v2SigningEnabled 或直接调用 apksigner |
| 8 | 构建证明主体摘要不匹配 | 产物在证明生成后被更换 | 保证产物与证明在同一原子步骤中生成且不可变 |
- 收集失败时的完整 stderr 输出
- 核对 EXPECTED_CERT_SHA256 环境变量值是否正确更新
- 检查构建机时间同步状态,避免证书有效期判断误差
- 确认 jq 等依赖工具在 CI 环境中已安装且版本兼容
本文门禁不覆盖的领域与补充建议
本文明确只处理 CI 流水线中对最终 APK 签名者身份与证书摘要的自动化校验,不扩展至密钥材料的生成、交接与保管。如何安全地创建密钥对、如何将私钥分发到签名节点、如何实施硬件安全模块保护签名材料,以及如何规划证书轮换周期,这些领域需要独立的密钥管理流程与安全策略,不属于 CI 签名门禁的职责范围。项目证据尚未接入具体的 HSM 集成案例。
同样,商店托管的签名流程(如 Google Play App Signing 中上传密钥与应用签名密钥的区分)不在本门禁的覆盖之内。商店托管签名改变了证书责任边界,CI 阶段校验的对象是上传密钥签名的 APK,但最终用户在设备端接收到的是由商店密钥重签的产物。对此类场景,本文所述门禁仍适用,但需要额外步骤去验证上传后的签名转换是否满足发布方要求,这超出了构建阶段签名校验的能力。
证书指纹和签名方案检查不能替代应用层安全测试;仍需结合静态分析、动态测试与运行时防护。本文只处理 CI 签名检查,其余控制措施按项目风险模型选择。NIST SSDF 提供过程层建议,但不提供具体产品能力;签名有效也不证明业务代码没有恶意逻辑。
- 审查密钥管理策略是否独立于 CI 脚本逻辑
- 确认是否有针对商店托管签名的特殊校验流程
- 评估是否需要引入额外的静态代码分析工具
- 定期检查 NIST SSDF 框架的最新更新以调整策略
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| apksigner 能够在不读取签名私钥的情况下验证 APK 的完整性并确认签名证书。 | Android apksigner | 工具验证成功不证明 keystore 管理、渠道流程或候选包归档正确。 |
| apksigner 的 --print-certs 选项可安全提取签名证书的 SHA-256 和 SHA-1 指纹。 | Android apksigner | 提取的指纹仅来自 APK 内嵌证书,不能独立证明该证书就是授权发布证书。 |
| Android 发布应用需要明确区分应用签名密钥、上传密钥以及 Play App Signing 的责任边界。 | Sign your Android app | 文档无法确认任一实际 APK 是否使用了正确的生产证书。 |
| 应用签名是 Android 建立更新身份的基础,不同签名方案覆盖不同的文件区域和平台版本。 | AOSP app signing | 签名有效只证明完整性和签名者身份,不证明业务代码安全。 |
| apksigner 要求签名完成后不得再对 APK 进行修改,否则校验应失败。 | Android apksigner | 该要求仅检测签名后改动,无法发现签名前已注入的恶意内容。 |
| SLSA 构建证明应将产物主体、构建者、构建类型、外部参数和依赖材料绑定在一起。 | SLSA Provenance v1.1 | provenance 只能证明被记录的构建过程,无法单独证明运行时安全性。 |
| in-toto 证明通过 Statement 结构将产物摘要与有类型的声明负载绑定,避免报告与候选包错配。 | in-toto Attestation Statement v1 | 声明格式本身不保证声明内容真实,仍需可信签名者和门禁核验。 |
| 安全发布实践应保留来源、构建、验证和变更的证据,并把供应链风险纳入开发流程。 | NIST SP 800-218 SSDF | SSDF 是组织级实践框架,不规定特定 App 加固产品的功能。 |
工程常见问题
apksigner verify 已经成功,为什么还需要额外核对证书指纹?
apksigner verify 只验证签名数据在密码学上的有效性和 APK 完整性,不验证证书是否属于发布组织。因此必须额外核对证书指纹,以确认签名者身份,防止攻击者使用任意有效证书重签名并通过校验。
CI 环境没有 Android SDK 时如何获得 apksigner 工具?
apksigner 是 Android SDK 构建工具中的独立 JAR 包装脚本或原生可执行文件,可以从 SDK 目录中直接复制出来,或通过预置的 Docker 镜像分发到 CI 运行环境,无需安装完整 SDK。
是否必须要求 APK 包含 v3 签名方案?
v3 签名主要服务于密钥轮换,并非所有设备都支持。CI 门禁的最低要求通常设为 v2。如果产品有轮换需求,可以额外检查 v3 的存在,但应根据目标 API level 和发布策略做出判断,而不是一刀切强制。
预期证书 SHA-256 摘要应该如何安全地提供给 CI 脚本?
应使用 CI 系统的掩码安全变量、密钥管理服务或只读配置文件注入,禁止硬编码在代码仓库中。摘要值的生成需来源于发布证书的一次性提取并由可信人员确认,本文不涉及私钥的获取与存储。
如何将构建证明与 APK 签名门禁集成到一个检查步骤?
在同一个 CI 阶段中,先验证 APK 签名者身份,再读取 in-toto 证明文件,校验声明中的 subject digest 与 APK 的 sha256 是否一致,并验证证明本身的签名者是否在信任列表中。两个检查任意失败都应阻断流水线。
如果证书指纹比对失败,但签名有效,会有什么实际后果?
可能出现测试证书或外部证书签名的 APK 被投放,导致用户设备因签名不一致而无法覆盖旧版应用,或者被 Google Play 等商店拒绝。这直接引发更新链断裂和信任危机,因此必须在 CI 阶段拦截。