先看结论与判断条件
- 退出能力应在采购和首次接入时建立,至少保留企业可独立重建的原始发布基线、配置目标、签名控制、验证用例和责任人。
- 可迁移的是业务资产、威胁、控制目标、构建输入、测试断言和证据索引;供应商专有变换、运行时协议和配置名称不能假设一一对应。
- Android 应用签名与 Apple 代码签名采用不同身份链和发布流程,必须分别证明企业可持续更新,不能用同一证书字段混为一套回执。
- 旧供应商的保护包不能替代未保护基线;生产迁移应从企业控制的源码、依赖、工具链和签名前产物重新构建新候选。
- 新供应商候选需要重新完成保护验证、兼容矩阵、签名身份、更新路径和渠道交付,旧供应商报告只能作为需求与历史证据。
- 退出关闭包含数据清单、删除范围、删除回执、账号与令牌撤销、构建访问复核、残余副本说明和责任人签收,不是停止续费即结束。
把可退出能力做成接入条件,而不是临时补救
供应商退出并不只发生在服务到期。价格、服务质量、地域合规、产品路线、企业并购、构建平台变化或关键人员调整,都可能触发迁移。真正的可退出能力,是企业在没有旧供应商继续配合的情况下,仍能从受控源码构建合法发布候选、维持应用身份、执行回归并安全更新用户,而不是合同里只有一句“支持迁移”。
退出计划应在首次接入时确定 owner、触发条件、通知期、交付物、数据处理、过渡支持和验收标准。技术 owner 管理基线与配置目标,安全 owner 管理证据与残余风险,密钥 owner 管理签名,发布 owner 管理渠道和更新身份,法务或采购 owner 管理数据删除与合同义务。没有责任人的清单在真正退出时很难执行。
企业还要定义不可接受的锁定。例如只能由供应商云端读取的配置、无法导出的保护映射、供应商代持且企业无法恢复的发布身份、只在对方环境可运行的测试,以及没有源码基线的二进制交付。并非所有专有实现都必须开放,但企业要知道哪些能力不能迁移,并为这些差异准备重建和重验预算。
| 要素 | 企业必须控制 | 供应商可协助 | 退出验收 |
|---|---|---|---|
| 源码与依赖 | 仓库、提交、锁文件和构建说明 | 兼容建议和适配补丁 | 无供应商环境可构建基线 |
| 签名身份 | 密钥策略、证书、恢复和轮换责任 | 按最小权限完成签名接口 | 企业可持续合法更新 |
| 加固目标 | 资产、威胁、控制目标和范围 | 专有能力配置与实现 | 新方案可重新映射并重验 |
| 验证用例 | 业务断言、设备矩阵和接受标准 | 供应商专用诊断工具 | 不依赖旧平台也能执行关键回归 |
| 构建证据 | 产物摘要、provenance 和审批索引 | 生成阶段日志和供应商声明 | 每份证据绑定明确候选 |
| 数据与访问 | 数据清单、授权、撤销和保留策略 | 删除回执与过渡支持 | 访问失效、数据处置有证据 |
区分可迁移资产与供应商专有实现
退出盘点应把资产分为企业原始资产、通用安全目标、供应商生成资产和供应商运营数据。源码、资源、签名前产物、业务测试、威胁模型和签名身份属于企业必须控制的基线;“保护离线授权决策”“检测关键资源异常”属于可重新映射的控制目标;虚拟化指令、运行时协议、专用映射和内部规则通常属于专有实现。
配置迁移不应试图把旧供应商的每个开关翻译成新供应商同名开关。名称相同也可能改变作用域、兼容限制和运行时行为,名称不同也可能服务同一控制目标。更稳妥的中间格式是 asset_id、threat_id、control_objective、protected_scope、compatibility_boundary 和 verification_case,新方案根据这些目标设计配置,再用候选回执验证。
供应商运营数据包括上传的 APK、AAB、IPA、符号、映射、诊断日志、设备回执、配置快照、账号信息和支持工单附件。退出清单要写明每类数据的所有者、存储位置、处理依据、保留期、子处理方和删除方式。不能因为门户里看不到文件就认为副本已删除,备份和支持系统中的残余范围也应由回执说明。
- 企业源码、资源、依赖和签名前基线位置明确
- 控制目标使用资产与威胁语言而非专有开关名
- 供应商生成包、映射、日志和配置分别登记
- 专有实现无法迁移的范围与替代成本已知
- 所有供应商数据标注存储、保留和删除责任
- 子处理方与备份副本进入退出数据清单
Android 与 Apple 签名身份必须分别保全
Android 使用应用签名建立安装和更新身份,不同签名方案覆盖的文件区域与平台版本有所不同。企业需要保存应用签名或商店托管责任、上传密钥、证书摘要、轮换与恢复流程,并明确供应商是否曾接触签名材料。旧供应商生成的 APK 若使用临时证书,不能作为生产更新基线;签名有效也只证明完整性和签名者身份。
Apple 平台通过代码签名、证书与运行验证建立 App 身份,相关流程与 Android APK 签名不能合并成一个 certificate 字段。退出计划应分别记录 Apple 团队责任、证书和配置文件管理、构建与签名阶段、App 标识以及发布渠道。供应商若代执行签名,企业仍需能在自有授权链中重建并验证最终候选。
迁移中最危险的动作是为了适配新工具而重签现有生产包、重新包装旧保护包或替换内部文件。任何字节变化都会产生新候选,并可能影响签名与测试关系。正确流程是从企业控制的原始源码和可发布基线重新构建,再由当前签名链签发;旧保护包仅用于历史对照,不能逆向恢复成可信基线。
| 平台对象 | 企业控制项 | 迁移验证 | 禁止替代 |
|---|---|---|---|
| Android 应用身份 | applicationId、应用签名证书和更新责任 | 签名验证、安装更新和渠道身份 | 供应商临时证书 |
| Android 上传身份 | 上传密钥、轮换与商店托管关系 | 上传授权和返回渠道回执 | 把上传密钥当应用签名密钥 |
| Android 签名方案 | 目标平台和签名参数 | 最终 APK 的方案与证书回执 | 只检查文件扩展名 |
| Apple 应用身份 | 团队、App 标识、证书与发布授权 | 最终归档、签名和渠道验证 | 沿用 Android 证书字段 |
| Apple 配置与能力 | entitlement、profile 和服务配置责任 | 目标能力、安装和运行回归 | 只看签名命令成功 |
| 共同证据 | 候选摘要、签发者、时间和审批 | 与测试和发布回执绑定 | 用旧包报告证明新包身份 |
保留可独立重建的原始发布基线
可退出基线不是旧供应商处理后的最终包,而是企业能独立重建的原始发布输入:源码提交、依赖锁、构建工具、变体、资源、平台 SDK、签名前产物生成步骤和预期摘要。若只有保护包,没有可构建源码和依赖,迁移只能从不完整二进制猜测,既不可靠,也难以证明业务语义与发布身份。
SLSA Provenance v1.1 可以记录产物 subject、builder、buildType、externalParameters 与 resolvedDependencies,适合说明基线和新候选如何产生。加固供应商、配置摘要与工具版本可以作为构建参数或材料进入证明。provenance 只能证明记录的构建关系,不证明运行时安全,也不能弥补缺失的源码、密钥或兼容回执。
in-toto Attestation Statement v1 可把产物摘要与有类型的声明负载绑定。退出时可以用它索引基线、供应商输入输出、测试和删除回执,避免把一份报告贴到另一个候选。声明格式本身不保证内容真实,企业仍需验证签发者、签名、subject 和 predicate,并确保停止合作后仍能读取必要证据。
- 源码提交、依赖锁、工具链和变体能够独立复现
- 签名前基线与最终候选分别计算摘要
- 供应商输入、输出和配置摘要进入 provenance
- 测试、签名和发布声明绑定同一 subject
- 退出后证据索引仍可由企业读取和验证
- 旧保护包不会被当成原始源码或可发布基线
用验证用例迁移安全目标,不迁移旧结论
供应商迁移真正可复用的是验证问题。例如高价值授权路径是否仍由服务端最终决定,目标结构在静态检查中暴露到什么程度,完整性异常如何处置,关键 Native 路径在目标 ABI 是否保持业务语义。这些用例与业务资产绑定,可以交给新方案重跑;旧供应商“已通过”的结论则只属于旧候选。
新候选至少需要保护验证与兼容验证两组回执。保护验证检查新方案是否覆盖目标资产和威胁,兼容验证检查安装、启动、登录、支付、数据、后台任务、异常路径和设备矩阵。即使新供应商声称功能等价,配置作用域、代码变换和运行时依赖仍会变化,不能承诺无损替换或复用旧设备结果。
迁移还要保留原始对照与旧供应商最终稳定候选。原始对照帮助区分应用本身问题,旧稳定候选帮助观察行为差异,新供应商候选用于验收目标控制。三者都要绑定来源、签名和摘要,不能重命名文件后比较。若旧候选无法在当前渠道重新发布,也要明确它仅是诊断对照而非可回滚包。
| 材料 | 可迁移部分 | 必须重建部分 | 主要原因 |
|---|---|---|---|
| 威胁模型 | 资产、威胁、业务影响和控制目标 | 新控制映射与残余风险 | 实现与边界变化 |
| 配置 | 保护范围、兼容约束和验证目标 | 新供应商具体参数 | 开关语义不等价 |
| 静态验证 | 检查方法、样本和接受标准 | 新候选观察与回执 | 二进制结构不同 |
| 设备回归 | 用例、数据、矩阵选择原则 | 新候选每台设备结果 | 运行时和产物变化 |
| 供应商报告 | 历史问题、范围和证据索引 | 当前保护与兼容结论 | 报告只绑定旧候选 |
| 回滚材料 | 决策条件和渠道流程 | 合法可更新候选与签名回执 | 旧包未必可重新发布 |
分阶段切换构建、发布与数据责任
执行迁移可以分为冻结、复现、映射、PoC、生产接入、受控发布和关闭七个状态。冻结旧稳定候选与证据;复现企业原始基线;把控制目标映射到新方案;对新候选做 PoC;接入生产签名与 CI;在批准范围内发布;最后完成数据删除与权限撤销。每个状态都应有 owner、输入、退出条件和不可变回执。
更新系统还要防止回滚、冻结和混搭风险。The Update Framework specification 通过签名角色、版本、过期时间和一致快照处理更新元数据风险,可以作为企业自管更新服务的参考,但它不直接规定 APK、IPA 或业务授权。应用商店和企业分发各有身份与版本规则,迁移方案必须按实际渠道验证,不能简单套用 TUF 名称。
受控发布前要定义停止推进、恢复业务开关和发布修复候选的条件。旧候选能否回滚取决于签名、版本、渠道和已安装用户状态,不能默认随时降级。更可靠的做法是准备经过验证的前向修复路径,并保留旧供应商配置与新配置的差异记录。任何紧急包也要进入合法签名链和最小门禁。
- 冻结、复现、映射、PoC、生产、发布和关闭状态明确
- 每个状态具有 owner、输入、门禁和回执
- 更新元数据验证与移动应用发布规则分别处理
- 停止推进、业务降级和前向修复条件预先定义
- 旧候选是否可回滚经过签名与渠道核对
- 紧急候选仍使用合法身份和最低验证门禁
用只读脚本检查退出清单是否具备闭环
下面的 Python 脚本读取脱敏退出清单,逐个平台检查责任人、签名控制、原始基线、provenance subject、验证用例、供应商数据清单、删除回执、访问撤销和回滚说明。它不接触密钥、不删除数据、不撤销账号,只把缺失字段列入 BLOCK,适合在退出评审前做结构门禁。
签名记录只保存证书摘要、custodian 和 recovery_process,不应包含私钥。baseline 要同时保存 artifact_sha256、source_sha256、dependency_lock_sha256 和 provenance_subject_sha256,脚本要求证明 subject 与基线产物一致。删除回执需要 receipt_id、scope、issuer 和 owner,访问撤销需要 receipt_id 与 owner,避免用口头确认关闭。
清单完整不证明供应商真的删除了所有副本,也不证明新候选可发布。法务、采购、安全、密钥 owner 和发布 owner 仍需核对合同、子处理方、备份残余、签名权限和真实渠道回执。脚本的目的只是阻止明显遗漏,让每个退出结论有责任人和可追踪证据引用。
import json
import sys
from pathlib import Path
REQUIRED_PLATFORM = {
"platform",
"owner",
"signing_control",
"baseline",
"verification_cases",
"vendor_data_inventory",
"deletion_receipt",
"access_revocation",
"rollback_plan",
}
HASH_FIELDS = {"artifact_sha256", "source_sha256", "dependency_lock_sha256", "provenance_subject_sha256"}
def stop(message):
raise SystemExit(f"invalid exit plan: {message}")
def nonempty(value):
return isinstance(value, str) and bool(value.strip())
def require_receipt(value, name):
if not isinstance(value, dict):
stop(f"{name} must be an object")
for field in ("receipt_id", "owner", "scope"):
if not nonempty(value.get(field)):
stop(f"{name}.{field} is required")
if len(sys.argv) != 2:
raise SystemExit("usage: python exit_plan_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"))
platforms = payload.get("platforms")
if not isinstance(platforms, list) or not platforms:
stop("platforms must be a non-empty list")
review = []
seen = set()
for index, item in enumerate(platforms):
if not isinstance(item, dict):
stop(f"platform {index} must be an object")
missing = sorted(REQUIRED_PLATFORM.difference(item))
if missing:
stop(f"platform {index} missing {missing}")
platform = item["platform"]
if not nonempty(platform) or platform in seen or not nonempty(item["owner"]):
stop(f"platform {index} has invalid identity or owner")
seen.add(platform)
signing = item["signing_control"]
if not isinstance(signing, dict) or not nonempty(signing.get("certificate_sha256")) or not nonempty(signing.get("custodian")) or not nonempty(signing.get("recovery_process")):
stop(f"platform {platform} lacks signing control")
baseline = item["baseline"]
if not isinstance(baseline, dict):
stop(f"platform {platform} lacks baseline")
for field in HASH_FIELDS:
value = baseline.get(field)
if not isinstance(value, str) or len(value) != 64:
stop(f"platform {platform} has invalid {field}")
if baseline["artifact_sha256"] != baseline["provenance_subject_sha256"]:
stop(f"platform {platform} provenance subject mismatch")
cases = item["verification_cases"]
inventory = item["vendor_data_inventory"]
if not isinstance(cases, list) or not cases or not all(nonempty(case) for case in cases):
review.append({"platform": platform, "gap": "verification_cases"})
if not isinstance(inventory, list) or not inventory or not all(nonempty(entry) for entry in inventory):
review.append({"platform": platform, "gap": "vendor_data_inventory"})
require_receipt(item["deletion_receipt"], f"{platform}.deletion_receipt")
require_receipt(item["access_revocation"], f"{platform}.access_revocation")
if not isinstance(item["rollback_plan"], dict) or not nonempty(item["rollback_plan"].get("owner")) or not nonempty(item["rollback_plan"].get("scope")):
review.append({"platform": platform, "gap": "rollback_plan"})
output = {
"plan_id": payload.get("plan_id"),
"platform_count": len(platforms),
"review": review,
"gate": "BLOCK" if review else "READY_FOR_EXIT_REVIEW",
}
print(json.dumps(output, ensure_ascii=False, indent=2, sort_keys=True))退出验收要同时关闭技术、数据和合同责任
退出完成的技术证据包括企业可独立构建的基线、新供应商最终候选、正确签名身份、保护与兼容回执、更新渠道验证、差异清单和前向修复方案。旧供应商稳定候选与报告可以保留为历史证据,但要明确其可用期限、读取方式和是否仍依赖供应商服务。
数据与访问关闭包括所有账号、API 凭据、CI 访问、仓库权限、上传入口、支持门户和远程诊断通道的撤销回执,以及 APK、IPA、AAB、符号、映射、日志和工单附件的删除或合法保留说明。删除回执只能证明声明的范围,备份、法规保留和子处理方残余应单独写明。
NIST SP 800-218 SSDF 强调持续维护、变更和供应链风险管理,退出方案也应进入日常演练,而不是只在争议时打开。需要评估实际迁移时,可通过御盾中央平台提交脱敏基线摘要、签名责任、控制目标、验证矩阵、供应商数据清单和渠道约束,先识别不可迁移项,再安排新候选和退出门禁。
- 企业在退出后仍可独立构建、签名、测试和发布
- 新候选取得保护、兼容、更新与渠道回执
- 旧供应商数据删除范围和残余副本已说明
- 所有账号、令牌、仓库与 CI 访问均有撤销回执
- 专有资产、历史证据和支持期限进入合同关闭
- 退出方案定期演练并随平台与供应链变化更新
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 更新系统应验证签名、版本、过期时间和一致快照,以识别回滚、冻结与混搭风险。 | The Update Framework specification 定义角色签名、版本、过期和一致快照等更新元数据机制。 | TUF 是更新框架,不直接规定 Android APK、Apple App 或业务授权策略,实际渠道仍需单独验证。 |
| 构建证明应绑定产物主体、构建者、构建类型、外部参数和依赖材料。 | SLSA Provenance v1.1 定义 subject、builder、buildType、externalParameters 和 resolvedDependencies 等字段。 | provenance 只能证明记录的构建关系,不能单独证明保护强度、运行时安全或迁移兼容。 |
| 供应链声明应把产物摘要与有类型的声明负载绑定。 | in-toto Attestation Statement v1 使用 subject 摘要、predicateType 和 predicate 组织证明。 | 声明格式不保证签发者可信或内容真实,仍需验证签名、subject 和具体门禁。 |
| Android 使用应用签名建立更新身份,不同签名方案覆盖不同文件区域与平台版本。 | AOSP app signing 说明 APK 签名用途、验证关系及各签名方案的适用范围。 | 签名有效只证明完整性与签名者身份,不证明业务代码安全或新供应商候选兼容。 |
| Apple 平台通过代码签名、证书与运行验证建立 App 身份。 | Apple app code signing process 说明 Apple 平台应用代码签名和运行验证关系。 | Apple 签名链与 Android APK 签名不能混为同一证据,文档也不证明实际候选签名正确。 |
| 安全发布应保留来源、构建、验证和变更证据,并管理供应链风险。 | NIST SP 800-218 SSDF 把来源追溯、构建完整性、验证、维护和供应链实践纳入安全开发。 | SSDF 是组织级框架,不定义某个 App 加固产品能力或供应商迁移的具体技术方案。 |
| 供应商迁移应复用业务资产、控制目标和验证用例,而不是假设专有配置无损转换。 | 工程判断:不同供应商的变换、运行时协议、默认值和兼容边界可能不同,同名开关不代表等价。 | 没有新候选和项目实测时,不能声明保护范围、强度、兼容性或迁移成本与旧方案相同。 |
| 退出关闭需要数据删除、访问撤销、基线控制和责任人回执共同完成。 | 工程判断:停止续费不会自动删除供应商持有的产物和日志,也不会撤销仓库、CI、签名或支持访问。 | 删除回执只覆盖声明范围,备份、法规保留、子处理方和合同义务仍需法务与安全核对。 |
工程常见问题
旧供应商的加固包可以直接交给新供应商继续处理吗?
不应把旧保护包当原始基线。应从企业控制的源码、依赖和可发布构建重新生成新候选,并重新完成签名与验证。
两家供应商都有同名功能,配置能直接一一转换吗?
不能假设等价。应迁移资产、威胁、控制目标和验证用例,由新方案重新映射配置并用候选回执验收。
更换加固供应商会影响应用签名吗?
供应商变化不应改变企业发布身份,但工具接入和签名阶段可能变化。Android 与 Apple 签名责任需分别核对和回归。
保留旧供应商测试报告能否减少新方案回归?
报告可复用范围、问题和用例,但结论只绑定旧候选。新二进制仍需重新取得保护、兼容、签名和渠道回执。
收到数据删除邮件是否就完成供应商退出?
不够。还要核对删除范围、备份与子处理方残余,并取得账号、令牌、仓库、CI、签名和支持通道的撤销回执。
准备供应商迁移评审需要哪些材料?
准备可构建基线、签名责任、控制目标、旧新候选摘要、验证矩阵、数据清单、访问清单、删除回执和发布约束。