先看结论与判断条件
- 先明确停止上新、停止支持、停止服务和彻底退役的日期与范围;同一个产品在不同渠道可能处于不同状态。
- 应用下架通常只改变后续分发,不能假定已安装副本自动消失,因此服务端、更新提示和用户迁移仍需单独设计。
- 最后可支持候选必须用文件摘要、版本、签名身份、渠道和加固配置识别,不能只保存一个最终版文件名。
- Android 与 Apple 的签名身份、证书和更新链分别留证;停止发布不等于可以立即销毁签名控制或撤销全部恢复能力。
- 构建来源、provenance、attestation、加固配置、mapping、符号和测试回执要有接收人和访问边界,但具体保留期限属于独立政策。
- 关闭流水线、账号、存储和供应商访问之前,先验证用户迁移、紧急修复、数据导出、事件调查和合规冻结没有继续依赖。
先定义退役状态,避免一句下架掩盖四种决定
停止上新表示不再向新用户分发,已安装用户仍可能继续运行;停止更新表示不再发布新客户端,但服务端可能继续;停止支持表示不再承诺常规修复;彻底退役才涉及关闭服务、终止更新路径、回收权限和按政策处置材料。四种状态的日期和责任不能混写。
同一 App 可能同时存在 Google Play、其他 Android 渠道、企业分发、Apple App Store 和历史侧载包,各渠道的可见性与用户覆盖不同。收尾清单逐渠道记录当前状态、最后版本、操作人、回执和仍可获取的用户范围,不能用主商店已下架推断全部分发结束。
工程判断是把退役作为有依赖的变更,而不是行政关闭工单。产品、研发、安全、发布、客服、法务和供应商分别批准自己控制的对象;最终 Accountable 角色确认所有未决依赖有去向。文章提供技术清单,不给出法律结论或御盾的固定服务承诺。
| 状态 | 仍可能发生 | 必须保留的能力 | 不能直接推断 |
|---|---|---|---|
| 停止上新 | 旧用户继续运行 | 服务与支持判断 | 安装副本消失 |
| 停止更新 | 现有版本继续访问 | 身份和事件处置 | 无需安全响应 |
| 停止支持 | 用户仍持有客户端 | 通知与风险决定 | 可以立即删证据 |
| 停止服务 | 离线功能继续 | 数据导出与迁移 | 所有渠道已关闭 |
| 彻底退役 | 历史争议或调查 | 授权的证据与责任人 | 材料无任何价值 |
| 恢复发布 | 紧急修复或过渡版 | 签名、构建和门禁 | 旧包可直接重用 |
建立渠道和已安装版本的真实清单
渠道清单至少包含应用标识、商店或分发系统、国家或组织范围、当前状态、最后 versionCode 或构建号、签名身份、下架操作回执和负责人。企业内测、MDM、下载页与合作方镜像也要纳入,否则公开商店关闭后仍可能出现新的安装。 对仍可下载的历史安装器、合作伙伴缓存和企业设备策略,要写明发现方式、通知对象和关闭验证;无法控制的副本保留为风险缺口,不能用主渠道回执覆盖。
已安装版本无法只靠商店后台得出完整数字。团队可以结合服务端版本分布、发布后台和支持记录形成有边界的估计,并明确数据缺口。没有真实数据时写未知,不编造活跃量、下架完成率或剩余用户数;每项迁移动作标明适用版本。
下架不应破坏用户的必要数据出口。若 App 承担账号、凭据、离线内容或设备控制,产品负责人先确定替代入口、导出格式、通知方式和最后服务条件。客户端加固不能替代服务端迁移,已安装包也不应被当作可远程强制消失的对象。
| 字段 | 来源 | 核对动作 | 缺失处置 |
|---|---|---|---|
| 应用标识 | 构建与商店 | 精确匹配 | 停止合并记录 |
| 渠道状态 | 渠道回执 | 读取真实结果 | 标记未知 |
| 最后版本 | 发布记录 | 绑定候选摘要 | 追查历史包 |
| 签名身份 | 签名回执 | 核对证书或团队 | 不得猜测 |
| 用户范围 | 服务与发布数据 | 说明口径 | 保留数据缺口 |
| 负责人 | 组织授权 | 确认仍在职可响应 | 重新指定角色 |
冻结最后可支持候选,而不是保存一个最终版文件名
最后可支持候选是退役后调查、用户恢复或过渡发布的锚点。记录文件 SHA-256、applicationId 或 Bundle ID、版本、平台、渠道、签名身份、来源构建、加固配置和测试回执。文件名、网盘时间或聊天中的最终版不能建立唯一身份。
若不同渠道对包做资源处理、重新签名或派生安装包,每个最终产物分别登记,并指向共同来源。provenance 可以把主体、构建者、构建类型、参数与材料关联;in-toto Statement 可用 subject 摘要绑定有类型的声明,但格式正确仍需要可信签名者和门禁核验。
冻结候选不等于宣称它永远安全或兼容。记录明确最后验证的设备、系统、业务范围、已知限制和未通过项。未来因平台变化或安全公告需要恢复发布时,应从保存的来源重新建立新候选和新门禁,不把历史通过直接继承。 恢复实验还要证明依赖仓库、构建镜像、许可证与工具入口在授权范围内可用;若只能读取二进制而无法重建,应把这种限制写进最终支持边界。
| 对象 | 唯一标识 | 责任人 | 边界 |
|---|---|---|---|
| 二进制 | 文件摘要 | 制品负责人 | 不代表安全强度 |
| 构建来源 | 提交与 provenance | 研发负责人 | 记录不保证可重现 |
| 加固配置 | 配置摘要与工具版本 | 安全负责人 | 不公开敏感规则 |
| 签名身份 | 证书或团队标识 | 发布负责人 | 不保存私钥内容 |
| 测试证据 | 候选与范围 | 测试负责人 | 仅适用已测条件 |
| 已知限制 | 例外和失败事实 | 风险负责人 | 不得改写为通过 |
签名和更新能力要在退役窗口内保持可控
Android 应用签名建立更新身份,不同签名方案覆盖的文件区域和平台支持有所不同。退役清单要分清应用签名密钥、上传身份、证书和渠道角色,确认谁仍能为合法过渡版本执行签名。签名有效只说明身份和完整性,不说明业务安全。
Apple 平台通过代码签名、证书和运行验证建立 App 身份,证书、团队和分发方式与 Android 证据不能合并成一个签名已保存字段。分别记录最后候选、签名责任、撤销或到期影响和必要的恢复授权,不在普通清单中保存私钥。
签名控制何时关闭取决于是否仍有安全修复、用户迁移、法定通知或服务端切换需要。停止日常发布后可收紧权限、改为双人批准或离线保管,但立即销毁可能切断更新能力。具体处置由密钥政策批准,文章不规定通用期限。
| 控制对象 | 退役前确认 | 关闭条件 | 禁止做法 |
|---|---|---|---|
| Android 应用身份 | 最后版本与更新关系 | 无剩余更新责任 | 只记包名 |
| 上传身份 | 渠道角色与替补 | 渠道操作完成 | 与应用密钥混写 |
| Apple 签名身份 | 团队、证书和分发 | 迁移责任结束 | 套用 Android 字段 |
| 签名服务账号 | 权限和审计 | 紧急窗口关闭 | 保留长期共享账号 |
| 证书回执 | 与候选绑定 | 按政策归档 | 只留截图 |
| 紧急更新能力 | 触发和批准角色 | 书面终止决定 | 无人负责地保留 |
更新、回滚和冻结风险需要书面终态
The Update Framework specification 讨论更新元数据中的签名、版本、过期与一致快照,用于识别回滚、冻结和混搭风险。移动 App 不一定直接采用 TUF,但退役设计可以借用其问题意识:谁授权最后版本、旧版本是否还能被重新分发、元数据何时过期、恢复发布如何防止错包。
如果服务端仍允许旧客户端连接,要明确最低版本、功能限制、认证和下线响应。若计划发布过渡版,指定来源候选、签名、渠道、用户提示和回滚条件;若决定永不再更新,则由有权承担风险的负责人批准,并说明已安装用户的支持边界。
不能用远程开关代替所有退役控制。开关可能无法到达离线设备,客户端默认值也可能在服务关闭后继续工作。测试至少覆盖在线、离线、服务端拒绝、过渡版本和旧版本恢复,记录可观察结果,不宣称一个开关能撤销全部安装副本。
| 情形 | 客户端动作 | 服务端动作 | 批准证据 |
|---|---|---|---|
| 继续有限支持 | 显示明确状态 | 保留必要接口 | 范围与终止条件 |
| 发布过渡版 | 迁移和提示 | 支持新旧切换 | 候选与门禁 |
| 停止更新 | 维持最后功能边界 | 拒绝新增能力 | 风险接受 |
| 强制升级不可行 | 提供替代入口 | 保护数据出口 | 用户迁移计划 |
| 发现高危问题 | 限制受影响功能 | 触发事件流程 | 紧急批准 |
| 彻底终止 | 离线状态可解释 | 关闭并留审计 | 最终收尾签字 |
构建、加固和诊断材料要完成责任移交
退役包至少能回答最后候选从哪里来、由什么工具和配置处理、谁批准、如何签名、覆盖哪些测试。NIST SSDF 支持保留来源、构建、验证与变更证据并处理供应链风险;它是组织级实践,不定义某个加固产品的功能或保留期限。
源码、依赖锁定、构建脚本、provenance、attestation、加固配置摘要、mapping、Native 符号和测试回执的访问级别不同。清单为每类材料指定保管人、恢复方法和销毁冻结条件;具体保存多久、如何校验与销毁由已有证据保留政策负责,本文不提供第二套期限。
供应商交付物完成接收后,客户要验证自己能够打开索引、按摘要找到材料并识别限制。只收到网盘链接、邮件附件或报告截图不算控制权转移。若某材料由供应商专有系统托管,收尾记录写明可导出范围、终止后的可访问性和替代证据。 接收方应在供应商访问关闭前完成一次按摘要检索和恢复演练,记录缺失索引、损坏文件和受限格式;演练失败时,材料状态保持 blocked,不能用已下载文件数量代替可恢复性。
| 材料 | 收尾动作 | 继续用途 | 不得包含 |
|---|---|---|---|
| 来源构建 | 固定提交与依赖 | 重建和调查 | 未授权源码副本 |
| provenance | 绑定主体与材料 | 追溯来源 | 虚构构建事实 |
| 加固配置 | 保存摘要与边界 | 解释最后候选 | 公开敏感规则 |
| mapping 与符号 | 绑定候选 | 诊断历史问题 | 无归属文件 |
| 测试回执 | 保存范围和失败 | 支持边界判断 | 改写原始结论 |
| 例外记录 | 保留责任与终态 | 解释风险决定 | 无限期默认继承 |
先完成用户与服务端迁移,再回收项目访问
客户端退役通常伴随 API、账号、推送、配置、存储和客服入口变化。逐项确认替代服务、数据导出、账号关闭、设备解绑、离线数据解释和投诉入口。服务端拒绝旧客户端时返回可识别状态,不让用户陷入无限重试或无说明空白页。 对每个接口还要定义最后允许的客户端范围、拒绝响应、数据出口、客服说明和监控终止时间;依赖未关闭前,相关服务账号和审计日志不能先行撤销。
访问回收覆盖开发者账号、签名服务、CI/CD、制品库、供应商门户、样本存储、诊断平台和工单系统。每个主体记录权限、最后用途、撤销时间、执行人和复核人;共享账号无法证明谁完成了最后操作,应先拆分身份再关闭。
删除动作必须等待依赖解除。若存在安全事件、合同争议、法务冻结、用户数据请求、紧急更新或尚未验收的迁移,清单把对象标为 hold 并指定解除批准人。hold 不代表永久保存,解除后仍按已批准政策执行处置并保存回执。
用代码检查退役清单中的未闭合依赖
下面的 Python 脚本读取公开安全的退役清单 JSON,不连接商店、不删除文件、不撤销账号。它要求渠道、最后候选、签名控制、更新路径、用户迁移、证据移交、访问回收和最终批准八类工作都有负责人、状态和证据引用。
脚本把完成状态与依赖分开检查。一个步骤标记 completed 时,所有 dependsOn 必须已经完成;要删除材料时,activeHolds 必须为空且 deletionApprovedBy 不能缺失。这样可以阻止先关签名账号、后发现还要发过渡版的顺序错误。
结构检查只证明清单字段闭合,不证明渠道实际下架、用户已经迁移或材料已经销毁。真实结论仍需打开 evidenceRef 核对回执。示例拒绝私钥、token 和用户数据字段,避免把收尾台账变成敏感材料副本。
import json
import sys
from pathlib import Path
REQUIRED_STEPS = {
'channels',
'final_candidate',
'signing_control',
'update_path',
'user_migration',
'evidence_handoff',
'access_offboarding',
'final_approval',
}
SENSITIVE_FIELDS = {'privateKey', 'token', 'userData', 'password'}
ALLOWED_STATUS = {'planned', 'blocked', 'hold', 'completed'}
if len(sys.argv) != 2:
raise SystemExit('usage: validate_decommission.py closeout.json')
input_path = Path(sys.argv[1])
if not input_path.is_file():
raise SystemExit('closeout file is missing')
data = json.loads(input_path.read_text(encoding='utf-8'))
steps = data.get('steps')
if not isinstance(steps, list) or not steps:
raise SystemExit('steps array is missing')
records = {}
for index, step in enumerate(steps):
if not isinstance(step, dict):
raise SystemExit(f'step {index} is not an object')
leaked = SENSITIVE_FIELDS.intersection(step)
if leaked:
raise SystemExit(f'step {index} contains sensitive fields: {sorted(leaked)}')
required = {'id', 'owner', 'status', 'evidenceRef', 'dependsOn'}
if not required.issubset(step):
raise SystemExit(f'step {index} is missing required fields')
step_id = str(step['id']).strip()
if not step_id or step_id in records:
raise SystemExit(f'step {index} has an invalid or duplicate id')
if step['status'] not in ALLOWED_STATUS:
raise SystemExit(f'step {step_id} has an invalid status')
if not str(step['owner']).strip():
raise SystemExit(f'step {step_id} has no owner')
if not isinstance(step['dependsOn'], list):
raise SystemExit(f'step {step_id} has invalid dependencies')
records[step_id] = step
missing = sorted(REQUIRED_STEPS - records.keys())
if missing:
raise SystemExit(f'closeout lacks required steps: {missing}')
for step_id, step in records.items():
unknown = sorted(set(step['dependsOn']) - records.keys())
if unknown:
raise SystemExit(f'step {step_id} has unknown dependencies: {unknown}')
if step['status'] == 'completed':
incomplete = [item for item in step['dependsOn'] if records[item]['status'] != 'completed']
if incomplete:
raise SystemExit(f'step {step_id} completed before dependencies: {incomplete}')
if step['status'] == 'completed' and not str(step['evidenceRef']).strip():
raise SystemExit(f'step {step_id} completed without evidence')
delete_request = data.get('deleteRequest')
if delete_request is not None:
if not isinstance(delete_request, dict):
raise SystemExit('deleteRequest is not an object')
if delete_request.get('activeHolds'):
raise SystemExit('deletion is blocked by active holds')
if not str(delete_request.get('deletionApprovedBy', '')).strip():
raise SystemExit('deletion has no approver')
print(json.dumps({'steps': len(records), 'requiredComplete': True, 'status': 'pass'}))最终批准只关闭已核实的工作,不抹掉未知项
最终评审逐项打开渠道回执、最后候选、签名控制、更新决定、用户迁移、证据移交和撤权记录。完成表示动作与回执都已核实;blocked 表示有明确障碍和责任人;unknown 不能被默认通过。无法形成证据的事项保留为缺口。
收尾报告写清仍可恢复的能力、已经不可逆关闭的能力、剩余风险和未来联系人。若日后需要安全修复或监管响应,团队可以从最后候选和责任索引恢复判断,而不是重新猜测签名、配置与渠道历史。退役完成也不代表历史版本从所有设备消失。
需要制定材料保留和销毁细则时,可继续阅读本站的软件加固证据保留政策,再携带应用清单、渠道、最后候选和迁移计划申请御盾项目收尾评估。申请、登录与控制台动作由御盾中央平台承接;本文不虚构御盾已完成任何客户退役项目。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 更新系统需要关注签名、版本、过期时间与一致快照,以识别回滚、冻结和混搭风险。 | The Update Framework specification 定义更新元数据和客户端验证模型。 | TUF 不直接规定移动 App 的业务授权、商店下架或御盾项目收尾流程。 |
| 安全发布应保留来源、构建、验证、变更和供应链风险证据。 | NIST SP 800-218 SSDF 提供组织级安全软件开发实践。 | SSDF 不定义某个加固产品功能,也不规定所有组织统一的保留期限。 |
| 构建证明可以把产物主体与构建者、构建类型、参数和材料关联。 | SLSA Provenance v1.1 定义 provenance 的模型和字段语义。 | provenance 记录不能单独证明运行安全、渠道下架或用户迁移已经完成。 |
| 供应链声明应使用摘要标识产物,并给声明负载明确类型。 | in-toto Attestation Statement v1 定义 subject、predicateType 与 predicate。 | 格式正确不保证声明真实,仍需可信签名者、策略和门禁核验。 |
| Android 应用签名建立应用身份、完整性和更新关系。 | AOSP app signing 说明 Android 应用签名和 APK 签名方案。 | 签名有效不证明业务安全、加固有效或退役动作完成。 |
| Apple 平台通过代码签名、证书和运行验证建立 App 身份。 | Apple app code signing process 说明 Apple 平台代码签名过程。 | Apple 签名链不能直接替代 Android 签名证据,也不证明商店下架完成。 |
| 退役收尾应区分停止分发、停止更新、停止支持、停止服务与彻底退役。 | 工程判断:这些状态影响已安装用户、签名能力、服务端和证据责任的范围不同。 | 具体日期、通知和法务义务由业务与适用规则决定,文章不提供法律结论。 |
| 销毁材料前必须确认更新、迁移、事件、争议和冻结依赖均有终态。 | 工程判断:过早删除可能切断紧急修复、用户恢复或调查所需证据。 | 这不是无限保存建议;解除 hold 后仍按批准的保留与销毁政策执行。 |
工程常见问题
App 从应用商店下架后,已安装版本会自动失效吗?
不能这样假定。下架通常影响后续分发,已安装副本、离线功能和服务端访问要分别设计迁移、限制与通知。
退役后可以立即销毁生产签名密钥吗?
不应默认立即销毁。先确认没有过渡版、安全修复、用户迁移或事件响应责任,再由密钥政策规定的负责人批准收紧、保管或销毁。
只保存最后 APK 或 IPA 文件是否足够?
不够。还要绑定文件摘要、版本、签名身份、来源构建、加固配置、渠道、测试范围和已知限制,否则无法证明文件身份。
退役收尾与加固证据保留政策有什么区别?
收尾清单决定哪些渠道、能力、责任和依赖已经关闭;证据保留政策负责材料的访问、校验、期限、hold 与销毁细则。
服务端已经关闭,还需要保留旧客户端的支持边界吗?
需要说明已安装版本会看到什么、数据如何导出、离线功能如何表现、投诉入口在哪里,以及是否存在过渡或恢复路径。
申请御盾协助做 App 退役收尾需要准备什么?
准备应用与渠道清单、最后候选摘要、签名控制、更新决定、用户迁移、证据索引、访问清单和 hold 状态,再通过御盾中央平台提交。