先看结论与判断条件

  • 紧急发布应是预先定义的受控通道,具备触发条件、决策人、最小门禁、证据格式和退出规则,不是事故发生后临时删除测试。
  • 候选摘要、源码和依赖、构建 provenance、签名证书、applicationId、versionCode 与加固配置必须绑定,换包后旧回执立即失效。
  • 缩减范围依据实际变更和影响路径:Native、依赖、Manifest、签名、协议或加固配置变化都会追加不同门禁,不能只看修改行数。
  • 安装、更新、启动、变更核心路径、加固目标、至少一个目标设备和停止推进方案属于底线;静态构建成功不能替代设备语义。
  • 延后测试必须记录 gate_id、原因、风险、approver、owner、expires_at 和 supplement_plan,逾期未补测时升级为发布阻断或事件复盘项。
  • Track API 返回成功只证明控制面接受状态,不证明审核、分发、兼容或质量通过;渠道回执、线上状态和业务监控要分别取证。

先把紧急通道设计成正式发布路径

紧急修复最容易出问题的时刻,是团队第一次在事故中讨论哪些测试可以不做。正确做法是在平时建立 emergency lane,写明触发条件、incident owner、security owner、release owner、允许的变更类型、不可删除门禁、可延后范围、最长债务期限和停止推进条件。事故发生时只需要套用规则并记录例外,不重新发明流程。

紧急不等于所有高优先级需求。触发条件应是明确业务或安全影响,需要在标准发布周期之前恢复服务、阻断持续损失或修复渠道拒绝。纯排期压力、营销日期或测试资源不足,不应自动获得紧急豁免。触发记录要包含 incident_id、影响、受影响版本、修复目标和授权决策人。

通道的目标是压缩等待与低相关测试,而不是降低候选真实性。并行准备构建证明、签名、测试数据和发布材料可以节省时间;提前维护核心回归、设备快照和回滚候选也可以缩短路径。删除候选核对、跳过设备验证或用旧包结果替代新包,只会把事故从服务端转移到客户端。

标准发布与紧急发布的差异边界
环节标准路径紧急路径不可改变
触发计划版本与完整排期符合预定义事件条件需要 owner 和审批记录
测试广度目标完整矩阵按变更影响选择最小矩阵变更核心路径和目标设备必须执行
测试顺序可串行等待构建、证据和设备准备并行每项回执仍绑定最终候选
延后项通常发布前完成允许有限风险债务有批准、期限、owner 和补测计划
发布范围按常规渠道策略从受控范围开始并设停止条件签名、版本和渠道身份正确
事后动作常规监控与回顾补测、债务关闭和事件复盘不能用线上无投诉替代补测

冻结事件、变更和最终候选身份

紧急修复开始后先冻结基线版本、修复提交和允许变更文件。每次构建记录 source_sha256、dependency_lock_sha256、variant、applicationId、versionCode、hardening_config_sha256、toolchain_id、signing_cert_sha256 和 artifact_sha256。候选一旦变化,就产生新的证据链;旧候选的设备结果不能通过相同版本名继续使用。

SLSA Provenance v1.1 可以把最终产物 subject 与 builder、buildType、externalParameters 和依赖材料绑定,适合证明紧急候选来自哪个提交、配置和构建者。in-toto Attestation Statement v1 可进一步把候选摘要与测试、签名和审批 predicate 绑定。两者都不证明运行时安全,声明签发者与内容仍需验证。

变更说明不能只列 Git diff。还要记录生成代码、依赖解析、资源与 Manifest 合并、R8 规则、Native 库、服务端协议和加固配置是否变化。某个 Java 方法只改一行,如果它改变授权协议或 JNI 调用,影响面可能很大;大段文案资源变化则可能与核心保护无关。门禁由影响机制决定,不按行数决定。

  • 事件、修复目标、基线版本和授权 owner 已冻结
  • 源码、依赖、变体、工具链和配置摘要入清单
  • 最终 artifact 摘要与 provenance subject 一致
  • 签名证书、applicationId 和 versionCode 可核对
  • 生成代码、资源、Manifest、R8 和 Native 变化已盘点
  • 任何换包都会废止旧候选未完成的门禁引用

按变化类型生成最小而完整的测试矩阵

变更分类至少覆盖 business_logic、dependency、native、manifest_resource、backend_protocol、signing_identity 和 hardening_config。每类变化对应不同风险域。业务逻辑影响目标断言和邻接路径,依赖影响初始化与传递行为,Native 影响 ABI 和加载,Manifest 与资源影响组件和权限,签名影响更新身份,加固配置影响保护范围和兼容。

多个类别同时出现时,门禁取并集而不是选择最轻的一类。例如一次修复既升级 Native SDK 又调整加固排除范围,需要执行 ABI 加载、目标业务、异常路径、保护覆盖和兼容回归。紧急流程可以减少设备数量或外围路径,但不能因为主要目标是修复业务缺陷,就忽略随包进入的依赖与配置变化。

变化分类还要考虑未变化证明。若声称签名、依赖或加固配置未变,应提供摘要比较,而不是口头确认。摘要相同只说明对应文件字节一致,仍要核对构建是否使用了它。对于无法证明的字段,最小矩阵按发生变化处理,宁可增加相关测试,也不要把未知状态当稳定状态。

紧急修复变化类型与追加门禁
变化类型主要风险必须追加不能仅靠
业务逻辑修复与邻接路径语义改变目标断言、反例和核心业务闭环单元测试或编译成功
依赖或工具链初始化、传递依赖和输出变化解析差异、启动、关键 SDK 与设备 smoke锁文件名称相同
NativeABI、加载、符号和线程边界变化目标 ABI 加载、JNI 核心路径和异常退出模拟器单一结果
Manifest 或资源组件、权限、链接和资源选择变化最终合并结果、入口和设备资源路径源码 diff
签名或应用身份安装更新和平台服务身份改变签名、覆盖更新、链接和后端配置签名命令 exit 0
加固配置保护范围与兼容语义变化目标资产、启动、异常和关键设备回归配置文件名相同

候选、签名、安装和核心路径属于不可删除门禁

任何紧急候选都要完成身份门禁:产物摘要、来源证明、applicationId、versionCode、签名证书和加固配置一致。How Android app updates work 说明覆盖更新需要保持 applicationId、兼容签名或轮换证明,并使用可接受的 versionCode。这些是平台更新条件,不等于业务防重放,但一旦错误,修复包可能根本无法覆盖用户现有版本。

设备门禁至少验证安装或覆盖更新、首次启动、修复目标、一个关键反例、加固目标和基本异常恢复。依赖真实 Android 运行时、组件和系统 API 的语义应通过 instrumented test 在设备端执行。单一设备通过不能代表完整矩阵,所以紧急结论必须标注设备、API、ABI 和未覆盖范围,并安排补测。

安全修复还要验证修复没有被加固配置排除或改变。如果修复位于高价值保护路径,保护检查和业务兼容必须针对最终签名候选重新执行;若临时降低保护范围,需要明确风险批准和恢复计划。不能以“先不上加固,后面再补”作为默认路径,因为这会改变发布候选的安全边界和后续差分。

紧急发布不可删除的底线门禁
门禁最小回执失败含义处置
候选身份artifact、source、config 与 subject 摘要测试对象无法确认阻断
签名更新证书、包名、版本和覆盖安装结果现有用户可能无法更新阻断
安装启动目标设备安装、启动和阶段码候选不可交付或不可进入阻断
修复目标正向断言与关键反例事件原因未关闭或引入误判阻断
加固目标目标资产与配置覆盖回执安全边界与审批不一致阻断或正式风险接受
停止推进负责人、触发条件和前向修复方案上线后无法受控处置阻断

延后项要成为有期限的风险债务

紧急路径可延后的是与当前变化关联较低、执行时间长且有替代监控的测试广度,例如外围功能全量回归、扩展设备矩阵、长时间稳定性或低风险地区渠道抽样。是否可延后由项目风险策略决定,不能建立永久豁免列表。涉及候选身份、签名、修复目标、加固目标和关键设备语义的门禁不得仅靠补测承诺。

每个 deferred gate 都要记录 gate_id、reason、risk_statement、approver、owner、expires_at、supplement_plan 和 blocking_if_overdue。审批人接受的是明确候选和时间窗内的残余风险,不能用群聊“先发”代替。到期前完成补测后,回执仍要绑定原紧急候选;若线上已切换到另一个版本,旧债务也应关闭或说明为何不再适用。

暂缓不应被写成通过。报告状态至少区分 passed、failed、deferred、inconclusive 和 not_applicable。deferred 有批准和补测,inconclusive 表示测试执行或证据不足,不能自动延期;not_applicable 要有范围理由。把未运行项目统一写绿,会让事件复盘无法知道实际承担了什么风险。

  • 只有低关联测试广度进入 deferred 候选
  • 每个延后项写明风险、审批人、owner 和到期时间
  • 补测计划包含候选、设备、用例和证据输出
  • 逾期处置明确为阻断、升级或事件复盘
  • deferred、inconclusive 与 not_applicable 分开记录
  • 线上无投诉不会自动把 deferred 改成 passed

受控发布、停止推进和前向修复要同时准备

紧急候选通过最小门禁后,应从受控范围开始发布,并预先定义继续、暂停和停止推进条件。条件可以来自安装失败、启动异常、修复目标失效、关键业务错误和渠道状态,但阈值必须由项目真实基线与风险策略决定。没有历史数据时不要虚构百分比,应使用可审计事件和人工审批。

Google Play Developer API track update 可以通过授权请求提交 Track 期望状态并返回 Track 资源。API 成功是控制面回执,不证明商店审核、用户分发、设备兼容或业务质量通过。发布系统应分别保存请求对象、返回资源、商店状态、真实安装和业务监控,避免把 HTTP 成功当上线成功。

回滚在移动发布中不一定等于安装旧版本。签名、versionCode、渠道和已安装用户状态会限制可用动作,因此紧急计划应优先准备合法的前向修复候选,并定义暂停继续分发、服务端降级或业务开关。任何修复候选仍需新摘要、新签名回执和最小门禁,不能复用紧急包结果。

  • 受控发布范围和继续、暂停、停止条件明确
  • Track 控制面、商店状态、安装和业务监控分别取证
  • 没有基线时不虚构告警百分比或通用阈值
  • 前向修复候选、签名和最小回归能够快速生成
  • 服务端降级和客户端发布动作边界已说明
  • 换候选后所有旧回执按摘要重新评估

用只读脚本生成并检查紧急最小门禁

下面的 Python 脚本读取脱敏紧急发布计划,根据 change_types 合并必须门禁,并检查候选摘要、provenance subject、gate 状态和 deferred 记录。基础门禁始终包含 candidate_identity、signing_update、install_launch、changed_path、hardening_scope、core_device、release_approval 和 stop_forward_fix。

变更类型会追加相关门禁,例如 native 追加 ABI 与 JNI,signing_identity 追加覆盖更新和平台服务,hardening_config 追加保护目标和兼容异常路径。required gate 不能标记 deferred;其他被延后项目必须具备 reason、risk_statement、approver、owner、expires_at 和 supplement_plan。脚本只输出 BLOCK 或 READY_FOR_EMERGENCY_REVIEW。

代码不会构建、签名、安装或调用发布接口,也不会判断业务严重性。它只验证计划结构和证据引用,真实 owner 仍需核对回执、设备和风险。日期字段要求非空但未用作通用时限判断,逾期规则由项目调度系统执行,避免在示例中虚构适用于所有事件的期限。

按紧急变更类型生成最小门禁并校验暂缓项
import json
import sys
from pathlib import Path

BASE_GATES = {
    "candidate_identity",
    "signing_update",
    "install_launch",
    "changed_path",
    "hardening_scope",
    "core_device",
    "release_approval",
    "stop_forward_fix",
}
CHANGE_GATES = {
    "business_logic": {"target_assertion", "negative_case"},
    "dependency": {"dependency_diff", "sdk_startup"},
    "native": {"abi_load", "jni_core_path"},
    "manifest_resource": {"merged_manifest", "resource_entry"},
    "backend_protocol": {"contract_case", "offline_error"},
    "signing_identity": {"upgrade_path", "platform_services"},
    "hardening_config": {"protection_target", "compatibility_exception"},
}
DEFER_FIELDS = {"reason", "risk_statement", "approver", "owner", "expires_at", "supplement_plan"}

def stop(message):
    raise SystemExit(f"invalid emergency plan: {message}")

def nonempty(value):
    return isinstance(value, str) and bool(value.strip())

if len(sys.argv) != 2:
    raise SystemExit("usage: python emergency_gate.py PLAN_JSON")
plan_path = Path(sys.argv[1]).resolve()
if not plan_path.is_file():
    stop(f"file not found: {plan_path}")
payload = json.loads(plan_path.read_text(encoding="utf-8"))
candidate = payload.get("candidate")
if not isinstance(candidate, dict):
    stop("candidate must be an object")
for field in ("artifact_sha256", "source_sha256", "hardening_config_sha256", "provenance_subject_sha256"):
    value = candidate.get(field)
    if not isinstance(value, str) or len(value) != 64:
        stop(f"candidate.{field} is not a SHA-256 string")
if candidate["artifact_sha256"] != candidate["provenance_subject_sha256"]:
    stop("provenance subject does not match the candidate")
change_types = payload.get("change_types")
if not isinstance(change_types, list) or not change_types:
    stop("change_types must be a non-empty list")
unknown = sorted(set(change_types).difference(CHANGE_GATES))
if unknown:
    stop(f"unknown change types: {unknown}")
required = set(BASE_GATES)
for change_type in change_types:
    required.update(CHANGE_GATES[change_type])
gates = payload.get("gates")
if not isinstance(gates, list) or not gates:
    stop("gates must be a non-empty list")
by_id = {}
for index, gate in enumerate(gates):
    if not isinstance(gate, dict) or not nonempty(gate.get("gate_id")) or gate.get("status") not in {"passed", "failed", "deferred", "inconclusive"}:
        stop(f"gate {index} is invalid")
    if gate["gate_id"] in by_id:
        stop(f"duplicate gate {gate['gate_id']}")
    by_id[gate["gate_id"]] = gate
missing = sorted(required.difference(by_id))
blocked = bool(missing)
for gate_id in required.intersection(by_id):
    if by_id[gate_id]["status"] != "passed" or not nonempty(by_id[gate_id].get("receipt_id")):
        blocked = True
deferred_review = []
for gate in gates:
    if gate["status"] != "deferred":
        continue
    absent = sorted(field for field in DEFER_FIELDS if not nonempty(gate.get(field)))
    if gate["gate_id"] in required or absent:
        blocked = True
        deferred_review.append({"gate_id": gate["gate_id"], "missing": absent, "required": gate["gate_id"] in required})
output = {
    "required_gates": sorted(required),
    "missing_gates": missing,
    "deferred_review": deferred_review,
    "gate": "BLOCK" if blocked else "READY_FOR_EMERGENCY_REVIEW",
}
print(json.dumps(output, ensure_ascii=False, indent=2, sort_keys=True))

发布后补测要关闭原候选债务并反哺常规流程

紧急版本上线后,第一项工作不是宣布事件结束,而是按 supplement_plan 完成延后矩阵。补测必须使用原紧急候选摘要和相同签名身份,才能关闭当时债务;若已经产生后续版本,要分别说明旧候选风险是否仍影响已安装用户,以及新候选需要哪些独立回归。

补测发现失败时,要依据预先定义的停止推进、服务端降级和前向修复动作处理,不能因版本已上线而降低严重性。事件复盘应比较标准流程与紧急流程,确认哪些并行化有效、哪些准备不足、哪些延后项实际高风险,并把结论更新到 emergency lane,而不是只追究执行速度。

NIST SP 800-218 SSDF 强调来源、构建、验证和变更证据,紧急发布也应保持同一原则。需要设计实际紧急加固通道时,可通过御盾中央平台提交脱敏事件类型、变更清单、候选身份、加固范围、核心设备、发布渠道和修复方案,先生成最小门禁,再由责任人批准风险债务。

  • 补测回执绑定原紧急候选和原 deferred gate
  • 后续版本不会覆盖旧候选已安装用户风险
  • 补测失败触发预定义停止或前向修复动作
  • 事件复盘更新变化分类、设备准备和并行流程
  • 风险债务关闭记录由 owner 与 approver 共同确认
  • 紧急通道定期演练并保持候选证据模板可用

事实依据与适用边界

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

本文判断事实或工程依据适用限制
安全发布应保留来源、构建、验证与变更证据,并把供应链风险纳入流程。NIST SP 800-218 SSDF 把来源追溯、构建完整性、验证、维护和供应链实践纳入安全开发。SSDF 是组织级实践框架,不定义 App 加固产品功能,也不规定紧急发布的具体测试数量。
构建证明应绑定产物主体、构建者、构建类型、外部参数与依赖材料。SLSA Provenance v1.1 定义 subject、builder、buildType、externalParameters 和 resolvedDependencies 等字段。provenance 只能证明记录的构建关系,不能单独证明紧急候选运行时安全或兼容。
供应链声明应把产物摘要与有类型的声明负载绑定。in-toto Attestation Statement v1 使用 subject 摘要、predicateType 和 predicate 组织证明。声明格式不保证签发者可信或内容真实,仍需验证签名、subject 和具体门禁。
Android 更新需要保持 applicationId、兼容签名或轮换证明,并使用可接受的 versionCode。How Android app updates work 说明应用身份、versionCode、签名证书和轮换证明对更新的要求。平台更新条件不等于业务防重放策略,也不证明紧急修复目标或加固范围正确。
发布系统可通过受授权的 Track 更新提交状态并获得返回资源。Google Play Developer API track update 描述 edits.tracks.update 的请求与 Track 返回对象。API 更新成功不证明商店审核、真实分发、设备兼容或业务质量门禁通过。
依赖真实 Android 运行时、组件和系统 API 的语义应通过设备端测试验证。Android instrumented tests 说明 instrumented test 在 Android 环境运行并可访问真实框架和组件。单一设备通过不能代表完整 API、ABI 和厂商矩阵,紧急流程仍需明确补测范围。
紧急发布可以缩减测试广度,但不能删除候选、签名、核心路径、加固范围和发布控制门禁。工程判断:这些门禁分别保证测试对象、更新身份、修复目标、安全边界和上线处置可验证,无法由其他成功状态替代。具体最小矩阵、设备选择和风险阈值必须依据项目变更、用户范围和历史基线确定。
延后测试应形成有批准、责任人、期限和补测计划的风险债务。工程判断:未运行测试若只标记通过,会掩盖真实风险并使上线后无法追踪原候选的未覆盖范围。没有项目候选和真实回执时,不能声明紧急版本已通过、风险已关闭或发布效果已验证。

工程常见问题

紧急修复是否可以先不加固,发布后再补?

不能作为默认方案。去除加固会改变安全边界和候选身份;若确需降低范围,必须正式批准风险并建立恢复与补测计划。

只修改一行代码,是否只测这个函数就够了?

不一定。要看它是否影响协议、依赖、JNI、签名、Manifest 或加固范围,门禁按影响机制而不是修改行数决定。

紧急发布最少要保留哪些测试?

至少保留候选来源、签名更新、安装启动、修复目标与反例、加固目标、目标设备和停止推进或前向修复证据。

未完成的设备矩阵可以全部标记 deferred 吗?

只有低关联范围可以延后,且每项需要风险批准、owner、期限和补测计划;关键设备与变更相关路径不能仅靠承诺。

Track API 返回成功后是否可以认为紧急版本上线成功?

不能。它只是控制面回执,还要核对商店状态、真实安装、业务监控和质量门禁,并保留停止推进条件。

准备紧急加固发布评审需要哪些材料?

准备事件与变更分类、候选摘要、provenance、签名、加固配置、最小设备矩阵、deferred 债务、审批和修复方案。

想用自己的 App 验证?

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

继续阅读: 御盾 VMP 接入前准备清单