先看结论与判断条件
- 先区分应用签名密钥、上传密钥、签名服务权限和 Apple 分发身份。
- 冻结只阻止新增风险,必须保留现有候选、证书摘要、日志和批准链。
- Android 轮换能力受平台版本、签名方案和分发渠道约束,要验证旧版本升级路径。
- Apple 证书、团队角色、profile 与 CI 机器身份分别处置,不能套用 Android 步骤。
- 每个渠道和在用版本都要有继续更新、受限维持或下架决定。
- 恢复发布前要绑定新身份、候选摘要、批准与真实安装升级回执。
先冻结签名与发布并保全证据
先把签名任务、发布队列、审计日志与现有候选写成可复核对象,而不是凭页面印象判断。冻结时间、执行人、任务状态、日志摘要和证据 owner应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,哪些动作立即停止、哪些只读调查可以继续才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
暂停队列而不删除材料,导出只读状态并建立事件编号。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
冻结动作删除了唯一日志或候选证据时先标记 blocked,并把剩余未知写进报告。冻结是风险控制,不等于泄露事实已确认。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 签名任务、发布队列、审计日志与现有候选身份 | 冻结时间、执行人、任务状态、日志摘要和证据 owner | 哪些动作立即停止、哪些只读调查可以继续 | 冻结动作删除了唯一日志或候选证据则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 暂停队列而不删除材料,导出只读状态并建立事件编号 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 冻结是风险控制,不等于泄露事实已确认 | 限制结论范围 | 不得扩大为全局结论 |
识别真正疑似泄露的身份对象
密钥、证书、服务账号、角色、令牌和机器身份要从状态变化而不是最终页面开始核对。把证书指纹、用途、托管位置、最近调用、权限范围和异常回执按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断事件涉及生产签名本体还是外围上传与访问身份发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
从签名服务和渠道反查真实身份,不凭文件名判断私钥。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。无法确认疑似泄露对象的用途与控制者说明链路仍缺少可归因证据;调查不回显私钥、令牌或可利用细节。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 密钥、证书、服务账号、角色、令牌和机器身份身份 | 证书指纹、用途、托管位置、最近调用、权限范围和异常回执 | 事件涉及生产签名本体还是外围上传与访问身份 | 无法确认疑似泄露对象的用途与控制者则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 从签名服务和渠道反查真实身份,不凭文件名判断私钥 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 调查不回显私钥、令牌或可利用细节 | 限制结论范围 | 不得扩大为全局结论 |
建立应用、证书、候选与渠道影响图
处理applicationId、证书摘要、versionCode、候选和轨道时,先固定应用身份、候选摘要、平台版本和可变条件。应用标识、旧新证书、版本区间、渠道、地区和候选摘要必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论哪些 App、版本和渠道需要冻结、轮换或继续观察,否则重试成功也只能说明环境变了,不能支持技术结论。
按证书和渠道双向查询,未知关系明确列为 blocked。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现某个证书关联的渠道或版本没有 owner,应回到上一份同源输入,比较首次分叉点并保留两侧回执。影响图缺失时只能标记未知,不能假设安全。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| applicationId、证书摘要、versionCode、候选和轨道身份 | 应用标识、旧新证书、版本区间、渠道、地区和候选摘要 | 哪些 App、版本和渠道需要冻结、轮换或继续观察 | 某个证书关联的渠道或版本没有 owner则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 按证书和渠道双向查询,未知关系明确列为 blocked | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 影响图缺失时只能标记未知,不能假设安全 | 限制结论范围 | 不得扩大为全局结论 |
按 Android 签名能力设计轮换路径
先把APK Signature Scheme、证明轮换和平台版本写成可复核对象,而不是凭页面印象判断。签名方案、proof-of-rotation、最低平台、渠道支持和验证结果应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,Android 设备是否接受设计的新签名身份和升级链才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
依据平台机制设计候选并在受控设备验证签名与升级。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
只验证新安装而没有验证旧版本升级时先标记 blocked,并把剩余未知写进报告。Android 轮换受系统和渠道能力限制,不能统一处理所有设备。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
单独处理上传密钥与托管签名角色
上传身份、Play App Signing、组织成员和 API 访问要从状态变化而不是最终页面开始核对。把上传证书、托管密钥角色、重置申请、成员和审计事件按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断是否只需重置上传身份而不改变应用签名身份发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
使用平台重置流程并轮换服务身份,保留前后回执。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。误把上传密钥问题当成应用签名密钥轮换说明链路仍缺少可归因证据;上传密钥与应用签名密钥职责不同,必须按真实托管模式判断。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 上传身份、Play App Signing、组织成员和 API 访问身份 | 上传证书、托管密钥角色、重置申请、成员和审计事件 | 是否只需重置上传身份而不改变应用签名身份 | 误把上传密钥问题当成应用签名密钥轮换则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 使用平台重置流程并轮换服务身份,保留前后回执 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 上传密钥与应用签名密钥职责不同,必须按真实托管模式判断 | 限制结论范围 | 不得扩大为全局结论 |
按 Apple 证书和团队机制处置
处理Apple 证书、profile、团队成员、CI 与分发账号时,先固定应用身份、候选摘要、平台版本和可变条件。证书序列、有效期、团队角色、profile、CI 身份和吊销状态必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论哪些 Apple 身份要吊销、替换或移除角色,否则重试成功也只能说明环境变了,不能支持技术结论。
按 Apple 团队与证书规则执行,分别验证 CI 和分发。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现移除人员账号后 CI 证书仍可用,应回到上一份同源输入,比较首次分叉点并保留两侧回执。Apple 与 Android 签名链不同,不能互相套用结论。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
验证旧版本到新身份的更新连续性
先把在用版本、安装来源、渠道和目标更新候选写成可复核对象,而不是凭页面印象判断。源版本、目标版本、安装器、签名验证、升级结果和回退应与最终候选摘要、版本、环境和操作时间绑定,记录值来自系统回执、测试日志或受控导出。只有这些输入同源,每个旧版本是否存在可验证的合法更新路径才有解释力;文件名相似、人员记忆或聊天截图都不能代替对象身份。
从最低仍受支持版本逐级或直接升级到新候选。执行前冻结会改变结果的开关、账号属性、时间窗和网络条件,执行中只改变一个变量,执行后保存输入、观察值与判定。每个通过项都要有正向路径和拒绝或失败路径;只记录“成功”会漏掉错误默认值、未授权访问、取消恢复和边界状态。
旧版本无法升级且没有业务处置决定时先标记 blocked,并把剩余未知写进报告。测试结果只覆盖列明源版本、设备和渠道。这项限制不能靠扩大权限、修改生产导出面或补写未经验证的数字绕过。若后续证据改变,只重开对应记录,不重复执行已经有稳定回执的无关步骤。
| 检查对象 | 输入证据 | 判定动作 | 失败处置 |
|---|---|---|---|
| 在用版本、安装来源、渠道和目标更新候选身份 | 源版本、目标版本、安装器、签名验证、升级结果和回退 | 每个旧版本是否存在可验证的合法更新路径 | 旧版本无法升级且没有业务处置决定则阻塞 |
| 同源基线 | 候选摘要与时间窗 | 锁定比较对象 | 来源不明则阻塞 |
| 执行记录 | 从最低仍受支持版本逐级或直接升级到新候选 | 保存系统回执 | 只有口头结论则补证 |
| 适用边界 | 测试结果只覆盖列明源版本、设备和渠道 | 限制结论范围 | 不得扩大为全局结论 |
用代码检查事件处置清单
脱敏处置记录中的证书、应用和更新路径要从状态变化而不是最终页面开始核对。把caseId、应用、证书、预期路径、观察路径、状态和证据按发生顺序保留,并为每一步写明触发者、前置条件、候选版本和回执来源。这样可以判断清单是否遗漏应用、证书、渠道或旧版本路径发生在读取、计算、传输、系统调度还是业务处理阶段,避免把多个故障压成一句“加固后不可用”。
把公开字段写成 JSON,让校验器拒绝路径不一致和空证据。清单至少包含对象标识、预期、观察、证据引用、责任人和复核状态。自动校验适合发现缺字段、重复记录与不一致结果,最终业务含义仍由熟悉应用的人判断。若依赖第三方平台,还要区分客户端实际状态、平台控制面显示和异步回执的时间差。
验收不能只看操作完成或命令退出码。预期更新路径与观察结果不一致却标记通过说明链路仍缺少可归因证据;代码不访问密钥、不执行吊销、签名或发布。结论应分成已确认事实、工程判断和适用限制,尤其不能把一次测试通过扩大成所有版本、设备、渠道或客户场景都通过。
import hashlib
import json
import sys
from pathlib import Path
REQUIRED = set([
'caseId',
'applicationId',
'certificateFingerprint',
'sourceVersion',
'expectedPath',
'observedPath',
'result',
'evidenceRef'
])
ALLOWED_RESULTS = set([
'pass',
'blocked',
'mismatch'
])
FORBIDDEN = {'token', 'password', 'privateKey', 'secret', 'customerData'}
def stop(message):
raise SystemExit(message)
def digest(path):
hasher = hashlib.sha256()
with path.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
hasher.update(chunk)
return hasher.hexdigest()
def text(record, field, index):
value = str(record[field]).strip()
if not value:
stop(f'record {index} has empty {field}')
return value
def validate(record, index):
if not isinstance(record, dict):
stop(f'record {index} is not an object')
if FORBIDDEN.intersection(record):
stop(f'record {index} contains forbidden fields')
missing = sorted(REQUIRED - record.keys())
if missing:
raise SystemExit(f'record {index} lacks fields: {missing}')
for field in REQUIRED:
if field != 'sourceVersion':
text(record, field, index)
result = text(record, 'result', index)
if result not in ALLOWED_RESULTS:
stop(f'record {index} has an unsupported result')
expected = text(record, 'expectedPath', index)
observed = text(record, 'observedPath', index)
evidence = text(record, 'evidenceRef', index)
if expected != observed and result == 'pass':
stop(f'record {index} passes despite a mismatch')
if expected != observed and not evidence:
stop(f'record {index} has a mismatch without evidence')
return text(record, 'caseId', index)
if len(sys.argv) != 2:
stop('usage: python validate_signing_response.py public-checks.json')
source = Path(sys.argv[1]).expanduser().resolve()
if not source.is_file():
stop('public check file is missing')
raw = source.read_bytes()
data = json.loads(raw.decode('utf-8'))
records = data.get('responseCases') if isinstance(data, dict) else None
if not isinstance(records, list) or not records:
raise SystemExit('responseCases must be a non-empty list')
identifiers = [validate(record, index) for index, record in enumerate(records)]
if len(identifiers) != len(set(identifiers)):
raise SystemExit('record identifiers are duplicated')
summary = {
'inputSha256': hashlib.sha256(raw).hexdigest(),
'records': len(records),
'results': {state: sum(1 for item in records if item['result'] == state) for state in sorted(ALLOWED_RESULTS)},
'status': 'pass',
}
print(json.dumps(summary, ensure_ascii=False, sort_keys=True))恢复发布前完成双人复核
处理新签名身份、恢复候选、批准人与验证回执时,先固定应用身份、候选摘要、平台版本和可变条件。候选 SHA-256、新证书、渠道、双人批准和安装回执必须来自同一轮执行,不能把不同设备或不同发布时间的结果拼在一起。证据链闭合后再讨论是否满足恢复签名和发布的最小条件,否则重试成功也只能说明环境变了,不能支持技术结论。
安全、发布和应用 owner 共同确认后才解除冻结。基线与最终候选使用相同设备、数据准备、账号权限和触发脚本,差异只保留必要变量。遇到失败先保存现场,再按最小范围补测;重装、清数据、切账号或换网络都会重置条件,必须作为新用例登记,不能覆盖原记录。
出现新身份未验证就恢复生产签名,应回到上一份同源输入,比较首次分叉点并保留两侧回执。加固不能补救泄露本身,恢复结论以平台和设备回执为准。临时绕行可以用于恢复业务,但必须记录批准、到期和回退条件,不能让临时措施替代根因验证或变成长期未审计配置。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android 使用应用签名建立更新身份,并由不同签名方案覆盖不同文件区域和平台版本。 | AOSP app signing:Android 使用应用签名建立更新身份,并由不同签名方案覆盖不同文件区域和平台版本。 | 签名有效只证明完整性与签名者身份,不证明业务代码安全。 |
| v3.1 描述签名轮换、最低轮换 SDK 和与 v4 的关联。 | APK Signature Scheme v3.1:v3.1 描述签名轮换、最低轮换 SDK 和与 v4 的关联。 | 轮换能力仍受分发渠道和历史安装基础约束。 |
| 发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。 | Sign your Android app:发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。 | 文档不能确认某个实际包使用了正确生产证书。 |
| Apple 平台通过代码签名、证书与运行验证建立 App 身份。 | Apple app code signing process:Apple 平台通过代码签名、证书与运行验证建立 App 身份。 | Apple 签名链与 Android APK 签名方案不能混为一套证据。 |
| 更新客户端应验证签名、版本、过期时间和一致快照,以识别回滚、冻结和混搭风险。 | The Update Framework specification:更新客户端应验证签名、版本、过期时间和一致快照,以识别回滚、冻结和混搭风险。 | TUF 是更新框架,不直接规定移动端模型或 APK 的业务授权策略。 |
| 安全发布应保留来源、构建、验证和变更证据,并把供应链风险纳入开发流程。 | NIST SP 800-218 SSDF:安全发布应保留来源、构建、验证和变更证据,并把供应链风险纳入开发流程。 | SSDF 是组织级实践框架,不定义某个 App 加固产品的具体功能。 |
| 签名事件必须按身份角色和分发平台拆分处置。 | 工程判断:应用签名、上传、托管服务和团队账号的权限与轮换能力不同。 | 具体步骤由平台、托管方式、系统版本和合同责任决定。 |
| 恢复发布前必须验证仍在用旧版本到新候选的更新路径。 | 工程判断:只验证全新安装无法证明既有用户可以合法升级并保持应用身份连续。 | 无法升级的历史版本需要业务和法务处置,文章不预设统一结论。 |
工程常见问题
加固能阻止泄露的签名密钥被滥用吗?
不能把加固当成密钥补救。应冻结、确认身份对象、按平台轮换或撤销,并验证渠道和旧版本更新路径。
上传密钥泄露等于应用签名密钥泄露吗?
不等于。启用托管签名时两者职责不同,要从平台角色、证书和审计记录确认实际影响。
为什么不能立即吊销所有证书?
盲目吊销可能切断合法更新和证据访问。先识别用途、依赖与恢复路径,再按授权顺序执行。
新签名 APK 能覆盖安装旧版本就算成功吗?
要覆盖支持范围内的源版本、设备、渠道和签名验证,保存真实安装回执;单一设备新安装不够。
Apple 和 Android 可以用同一处置清单吗?
可以共享事件字段,但证书、团队角色、轮换和运行验证机制必须分别执行和验收。
提交御盾响应评估准备什么?
准备应用与渠道清单、证书指纹、疑似身份角色、候选摘要、在用版本、审计日志、冻结回执、轮换方案和脱敏升级结果。