先看结论与判断条件

  • 先由构建系统枚举真实 variant,再建立一对一签名责任矩阵,不能只审查几个 Gradle 配置块。
  • 每个可发布变体都要固定 applicationId、渠道、build type、flavor、允许证书、产物类型和验证任务。
  • 签名策略以最终 APK 的全部证书摘要为准,signingConfig 名称、keystore 文件名和任务名只作诊断。
  • Play 上传包、直接分发 APK、内部测试包和派生 split 的签名身份不同,证据必须注明对象和责任。
  • 流水线应对变体清单中的每个实际 APK 运行 apksigner,并要求产物集合与策略集合完全一致。
  • 变体新增、重命名、禁用或签名配置改变都产生可审阅差异,任何未登记组合都不能自动进入发布。

先从真实变体集合建立签名矩阵

Android build variants 说明 build type、product flavor、source set、applicationId 与签名配置会组合成多个变体。项目配置中看起来只有 debug 和 release,加入 free、paid、region、customer 等 flavor 后,就可能出现大量组合。某个 flavor 还可以覆盖 signingConfig、applicationIdSuffix 或资源,使同一 release build type 产生不同包名和签名责任。

签名治理的第一步不是搜索 signingConfigs,而是让构建系统输出本次可构建、可发布的真实 variant 清单。每条记录包含稳定 variant 标识、applicationId、versionCode、build type、flavor 维度、产物类型、渠道和预期签名策略。清单与组织批准的发布矩阵比较,新增、消失和属性变化都作为审阅差异,不允许脚本默默忽略未知组合。

矩阵还要明确不发布的变体。benchmark、internal、staging 或 customerDebug 可能需要真实后端和测试证书,却不能进入公开渠道。工程判断是发布允许集合采用白名单,只有明确登记并绑定验证任务的变体可流入签名和上传阶段;名称中包含 release 不自动获得资格,名称中没有 debug 也不代表安全属性满足要求。

变体签名矩阵的最小字段
字段来源门禁用途异常处理
variantId构建系统枚举唯一连接策略和任务重复或未知拒绝
applicationId最终 manifest选择包名责任与证书与矩阵不符拒绝
buildType 与 flavorsvariant 元数据确认发布资格未批准组合拒绝
artifactType构建输出区分 APK、AAB 和 split类型错误拒绝
channel发布计划选择上传或分发责任未知渠道拒绝
allowedSignerDigests版本化证书策略精确比较全部签名者额外或缺失拒绝

Gradle 继承和覆盖会造成隐式串用

signingConfig 往往在 build type 上设置,flavor 又可能覆盖,CI 还可能通过环境或插件注入。最终变体采用哪个配置,取决于合并结果,不是某一段脚本的肉眼阅读。若 stagingRelease 继承正式证书,测试包可能获得生产升级身份;若 paidRelease 继承 debug 配置,正式付费包又会用调试证书生成。两类串用都需要从最终产物反查。

source set 合并也可能改变 applicationId、组件、资源和端点。同一签名证书如果被允许覆盖多个不同责任的包,会扩大错误签名影响。工程判断是签名矩阵以最终 applicationId 和渠道为主键,variantId 作为构建上下文,证书允许集合精确到责任边界;不能仅按 build type 给所有 flavor 共享一个宽泛策略。

配置评审应同时检查正向和反向关系。正向问题是每个发布 variant 应使用哪些证书,反向问题是每张证书可以签哪些 package 和渠道。若一张测试证书出现在正式变体策略,或正式证书被内部任意任务调用,都应提示边界扩大。本文只处理配置串用,不展开私钥托管和轮换,但门禁需要为后续密钥治理保留责任标识。

常见继承串用及其真实检查点
串用情形配置表象最终风险检查点
release 继承 debug未显式覆盖 signingConfig正式包使用调试证书最终 APK 证书摘要
staging 继承 production共用 release build type测试包获得正式签名身份包名加证书反向映射
flavor 覆盖包名applicationIdSuffix 隐藏在 flavor策略选择错包最终 manifest applicationId
CI 注入配置本地脚本看不到环境覆盖不同执行器结果不一致provenance 参数与回执
变体重命名旧策略仍引用旧名称新组合绕过验证任务实际集合与策略集合比较
任务通配符上传脚本选择最新文件另一个变体被误提交候选摘要与 variantId 绑定

应用签名、上传签名和渠道责任要分开

Sign your Android app 区分应用签名密钥、上传密钥、证书与 Play App Signing。提交 Play 的 AAB 或 APK 可能由上传密钥签署,用户最终安装的分发 APK 则由应用签名密钥负责。直接分发渠道没有这层托管转换,交付 APK 的签名身份就是用户侧升级身份。签名矩阵必须写明验证对象,不能把上传证书和应用签名证书统称为生产证书。

同一 variant 可能面向多个渠道,渠道又可能对产物进行派生或重新签名。本文不讨论渠道二次签名治理,但门禁至少要把本地上传候选和最终分发身份分开归档。上传阶段验证本地候选摘要与上传证书,发布完成后再通过渠道回执或可验证分发 APK 记录实际应用签名证书,二者不能共用一条回执。

证书主题、keystore 文件名和 Gradle signingConfig 名称都不是稳定身份。允许策略使用证书 SHA-256 或等价公钥身份,记录签名者数量与适用渠道。人工可读名称保留用于诊断,但放行只做精确摘要集合比较。这样 productionUpload.jks 被错误替换、配置名称没变时,最终 APK 仍会因摘要不符而失败。

不同产物的签名责任
产物本地验证身份用户侧身份证据边界
直接分发 APK应用签名证书同一交付 APK 证书本地候选与交付字节一致
Play 上传 AAB上传证书由 Play App Signing 决定上传回执不等于分发身份
Play 上传 APK上传或应用签名视配置平台处理后的发布身份必须注明实际流程
内部测试 APK测试证书仅测试环境接受不得进入正式渠道策略
bundletool 本地 APK set显式或调试签名仅本地测试身份不能冒充线上分发候选
渠道派生 split依渠道流程而定设备实际安装证书需要渠道或设备回执

AAB 与派生 APK 需要两层验证

Build and test Android App Bundles 说明 bundletool 和测试轨道可生成、安装并验证从 AAB 派生的 APK 集。AAB 是发布容器,目标设备运行的是按配置选择的 APK。变体签名矩阵要分别记录上传 AAB 身份和派生 APK 身份,不能因为 AAB 使用某上传证书,就推断设备 APK 使用同一证书或具有相同文件摘要。

本地使用 bundletool 时,如果没有显式提供签名信息,生成物可能采用调试签名。这样的 APK set 可以检查模块拆分和基本运行,却不应进入正式分发或证书门禁的生产通过记录。工程判断是把本地派生测试标为支持性证据,输出设备规格、bundletool 版本、输入 AAB 摘要、APK set 摘要和实际签名身份。

测试轨道可以更接近真实平台处理,但仍要保存目标应用、变体、轨道和时间回执。不同渠道或不同应用包名的结果不能互借。若同一源代码生成多个 applicationId,平台上的 Play App Signing 配置、上传证书和应用签名证书可能完全不同,因此每个变体都要拥有独立渠道记录。

AAB 到设备 APK 的签名检查层
层级要固定的身份适合验证不能外推
构建 variant包名、版本、flavor、build type配置和发布资格最终证书
上传 AAB文件摘要与上传证书提交候选身份设备 APK 字节
本地 APK set输入 AAB、设备规格与本地签名拆分和基本运行线上渠道身份
测试轨道平台应用、轨道和处理回执接近线上分发行为其他渠道和全部设备
设备安装集split 摘要与应用证书真实安装身份未覆盖设备组合
发布归档每层证据连接关系独立复核与事故反查未记录的渠道变换

每个变体都要有独立的最终产物验证任务

Android apksigner 可以验证 APK 签名方案和证书,并要求签名后不再修改 APK。流水线对变体清单中的每个最终 APK 运行 verify,保存工具退出状态、签名方案、全部签名者证书摘要和 APK 摘要。验证任务名称包含 variantId,但任务真正读取的路径必须来自构建输出元数据,不能用目录通配符猜测。

策略集合与产物集合必须完全相等。矩阵有三个发布变体而构建只生成两个,应失败;构建多出一个未登记变体,也应失败。每个产物必须恰好匹配一个策略节点,避免相同包名或别名让脚本选择第一条。证书集合、build type、channel 和 applicationId 都精确比较,任何未知值默认拒绝。

apksigner 通过不代表变体配置全部正确。它只验证签名结构和输出证书,仍要把 applicationId、versionCode、variant 元数据和来源证明纳入同一记录。签名后若发生资源注入、渠道修改或重签名,APK 摘要变化,整个验证任务失效并重新执行。

  • 每个发布 variant 生成唯一候选记录
  • 产物集合与策略集合完全相等
  • apksigner 对最终 APK 而非中间文件执行
  • 全部签名者证书摘要精确比较
  • applicationId、channel 和 build type 同时匹配
  • 签名后任何字节变化都重新验证

用清单驱动脚本逐个验证变体证书

变体产物清单应由构建系统自动生成,至少包含 variantId、applicationId、channel、buildType、APK 相对路径和预期 SHA-256。策略文件保存每个 variant 的允许证书集合。验证器先检查清单中没有重复 variant、路径没有越过产物根目录、文件摘要等于清单,再对每个 APK 调用 apksigner verify --print-certs 并收集全部证书摘要。

下面 Python 示例使用参数数组调用 apksigner,不经过 shell,不读取 keystore,也不签名任何文件。它只处理策略明确列出的 APK,并要求实际证书集合与 allowlist 精确相等。为了保持示例聚焦,代码没有解析 applicationId 和 versionCode,真实流水线应从最终 APK 提取这些字段并与清单比较;也要固定 apksigner 版本并归档完整回执。

执行器失败、输出缺失证书、摘要不符或未知 variant 都返回非零。日志打印 variant 和错误类别,不打印私钥、口令或内部 keystore 路径。工程判断是这类检查适合作为签名后、上传前的不可绕过门禁,不能放在可选报告任务里,也不能允许发布角色通过环境变量关闭。

  • 变体清单来自真实构建输出
  • 产物相对路径不能越过根目录
  • 调用 apksigner 不经过 shell
  • APK 摘要在验签前重新计算
  • 实际与允许证书集合精确相等
  • 所有失败聚合后阻断发布
逐个验证变体 APK 的证书允许列表
from pathlib import Path
import hashlib
import json
import re
import subprocess
import sys

if len(sys.argv) != 4:
    raise SystemExit("usage: verify_variants.py artifacts.json policy.json output-root")

manifest = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
policy = json.loads(Path(sys.argv[2]).read_text(encoding="utf-8"))
output_root = Path(sys.argv[3]).resolve()
artifacts = manifest.get("artifacts")
policies = policy.get("variants")

if not isinstance(artifacts, list) or not isinstance(policies, list):
    raise SystemExit("artifacts and variant policies are required")

policy_by_id = {item.get("variantId"): item for item in policies}
artifact_ids = [item.get("variantId") for item in artifacts]
if len(policy_by_id) != len(policies) or len(set(artifact_ids)) != len(artifact_ids):
    raise SystemExit("duplicate variant identifiers are forbidden")
if set(artifact_ids) != set(policy_by_id):
    raise SystemExit("artifact and policy variant sets differ")

cert_pattern = re.compile(r"certificate SHA-256 digest:\s*([0-9A-Fa-f:]+)")
failures = []
for artifact in artifacts:
    variant_id = artifact["variantId"]
    relative = Path(artifact.get("apk", ""))
    if relative.is_absolute() or ".." in relative.parts:
        failures.append(f"{variant_id}: unsafe APK path")
        continue
    apk_path = (output_root / relative).resolve()
    if output_root not in apk_path.parents or not apk_path.is_file():
        failures.append(f"{variant_id}: APK is missing")
        continue
    actual_hash = hashlib.sha256(apk_path.read_bytes()).hexdigest()
    if actual_hash != artifact.get("sha256"):
        failures.append(f"{variant_id}: APK digest mismatch")
        continue
    result = subprocess.run(
        ["apksigner", "verify", "--verbose", "--print-certs", str(apk_path)],
        capture_output=True,
        text=True,
        check=False,
    )
    if result.returncode != 0:
        failures.append(f"{variant_id}: apksigner verification failed")
        continue
    observed = {value.replace(":", "").lower() for value in cert_pattern.findall(result.stdout)}
    allowed = {value.replace(":", "").lower() for value in policy_by_id[variant_id].get("allowedDigests", [])}
    if not observed or observed != allowed:
        failures.append(f"{variant_id}: signer set differs from policy")

if failures:
    print("variant signing gate failed:")
    for failure in failures:
        print(f"- {failure}")
    raise SystemExit(2)

print(f"verified {len(artifacts)} variant APK files")

provenance 把正确变体连接到正确构建

SLSA Provenance v1.1 将产物主体与构建者、构建类型、外部参数和依赖材料关联。多变体项目中,externalParameters 应表达不含秘密的 variant、flavor、build type 和渠道选择,subject 摘要绑定最终候选或明确的中间产物。这样可以区分同一任务名在不同参数下产生的结果。

证书允许列表通过仍可能签错文件,例如正式证书签了 staging 代码或旧 APK。provenance 门禁检查 builder、buildType、参数和材料是否匹配签名矩阵,并确认 subject 摘要属于当前候选。若 provenance 只覆盖签名前文件,必须有可验证的签名变换记录把中间摘要连接到最终 APK,不能直接沿用。

provenance 不是运行时或密钥保管证明。它帮助确认文件从哪条构建路径产生,无法说明签名私钥授权、业务代码安全和设备兼容。工程判断是证书、来源、变体和设备验证都使用同一候选索引,各自给出窄结论,最终发布判断再组合。

变体矩阵与 provenance 的连接点
矩阵字段provenance 对应检查目的边界
variantIdexternalParameters确认构建选择参数可能被错误授权
build typebuildType 或参数确认批准流程不证明最终证书
flavorsexternalParameters确认组合未串用不证明资源运行正确
源码与依赖resolvedDependencies追溯材料身份不证明依赖无漏洞
APK 摘要subject.digest绑定当前产物不证明设备兼容
builderrunDetails.builder限定构建执行者不替代签名者验证

发布与变更记录要能回答谁串用了什么

NIST SP 800-218 SSDF 将来源、构建、验证、变更和供应链风险纳入安全开发实践。每次多变体发布应归档实际变体集合、策略集合、APK 摘要、包名、版本、渠道、全部证书摘要、apksigner 回执、provenance、上传回执和设备验证。记录必须可按 variant、证书或渠道反查,不能只按版本号放在同一目录。

新增 flavor、调整继承、改变 applicationId、切换渠道或修改签名配置都应触发矩阵差异。审阅者确认责任边界后更新策略,再由流水线验证真实产物。若发现串用,停止对应 variant 发布,固定错误 APK 摘要和证书,检查平台处理状态,并从受控构建重新生成新候选;不能只在归档里改标签或替换策略。

最终结论应写成指定变体集合的实际 APK 与包名、渠道、证书和来源策略一致,不能推导私钥管理正确、所有派生 APK 已上线或运行时完全安全。准备评估时,可通过御盾中央平台提交变体矩阵、最终 APK、证书摘要、apksigner 回执和 provenance,先固定候选身份与责任边界。

  • 实际变体集合和批准策略集合都有摘要
  • 每个 APK 拥有独立签名与来源回执
  • Play 上传身份与分发身份分别记录
  • 矩阵变更必须经过责任人审阅
  • 串用事故固定错误候选而非改名掩盖
  • 报告明确未验证的渠道与设备范围

事实依据与适用边界

以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。

本文判断事实或工程依据适用限制
build type、product flavor、source set、applicationId 和签名配置会组合成不同构建变体。Android build variants 描述变体组合、source set、包名和签名配置机制。同一仓库或相同 release 名称不保证所有变体具有相同证书、资源和运行行为。
Android 发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。Sign your Android app 描述应用签名、上传密钥和 Play App Signing 流程。官方文档不能确认某个实际变体使用正确证书或平台交付同一文件。
apksigner 可以验证 APK 签名方案和证书,并要求签名后不再修改 APK。Android apksigner 描述签名、verify、证书输出和签名后修改边界。工具通过不证明变体策略、keystore 管理、渠道流程或候选归档正确。
构建证明可以把产物主体与构建者、构建类型、外部参数和依赖材料关联。SLSA Provenance v1.1 描述 provenance 的 subject、builder、buildType、参数和材料字段。provenance 不证明签名密钥授权、运行时安全或设备兼容。
安全发布应保留来源、构建、验证和变更证据并管理供应链风险。NIST SP 800-218 SSDF 提供组织级安全软件开发与发布实践框架。SSDF 不定义具体变体矩阵,也不能确认某个 APK 已通过门禁。
AAB 可以通过 bundletool 与测试轨道生成和验证设备相关的派生 APK 行为。Build and test Android App Bundles 描述本地生成、设备安装和测试 App Bundle 的路径。本地派生不能完全替代 Play 线上回执,也不能把上传证书外推为分发证书。
每个发布变体都应拥有唯一包名、渠道、证书允许集合和验证任务。工程判断:一对一矩阵可以阻止继承、覆盖和任务通配导致的配置串用。实际 variant 集合、渠道和允许证书必须由项目构建输出与发布责任确认。
产物集合与策略集合必须完全一致,未知或缺失变体都应失败关闭。工程判断:集合相等检查同时阻止新变体绕过门禁和计划变体遗漏验证。集合通过后仍需独立验证包名、版本、来源、签名方案和设备行为。

工程常见问题

所有 release 变体可以共用同一 signingConfig 吗?

只有责任边界确实相同且策略明确时才可以。仍需按最终包名和渠道逐个验证证书摘要,不能因 Gradle 配置名称相同就自动放行。

为什么只检查 signingConfig 名称不够?

名称可以保留而 keystore、CI 注入或继承结果改变。最终 APK 中的证书摘要才是实际签名身份,应与变体策略精确比较。

Play 上传 AAB 与用户安装 APK 应使用同一证书策略吗?

不能默认相同。上传产物可能使用上传证书,分发 APK 由 Play App Signing 的应用签名证书负责,两层对象和回执要分开。

bundletool 本地生成的 APK set 能作为正式签名证据吗?

取决于显式签名配置。未提供发布签名时可能使用调试身份;即使本地签名正确,也不能替代平台线上派生和分发回执。

变体证书全部正确是否意味着可以发布?

不能。还要验证 applicationId、版本、产物来源、签名方案、加固结果、渠道处理、设备兼容和业务路径,并绑定同一候选摘要。

申请多变体签名配置评估前需要准备什么?

准备真实变体清单、签名矩阵、最终 APK、证书摘要、apksigner 回执、渠道说明和 provenance,再通过御盾中央平台提交申请。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: APK、DEX 与签名完整性如何一起验收