先看结论与判断条件
- 职责分离的目标不是增加审批层级,而是让修改候选、授予应用身份和触达用户三个高影响动作不能由同一主体悄然串联。
- 每次交接都要固定输入摘要、输出摘要、配置版本、执行身份和审批记录;只比较文件名或聊天截图无法证明候选没有被替换。
- 加固执行方可以处理未签名候选并提交技术回执,但生产私钥、签名授权和渠道账号不应因工具便利而默认交给同一方。
- 签名完成后不得继续改写 APK;需要调整保护配置时,应回到加固阶段形成新候选,再重新验证、签名和批准。
- 发布角色只接受已通过门禁且摘要匹配的签名候选,不能从临时目录任意挑包,也不能替代安全和测试负责人接受残余风险。
- 小团队允许人员兼任,但必须把冲突权限显式登记,采用独立复核、短时授权、受控凭据和到期撤销,不能把现实限制伪装成已经分离。
先把三个控制面拆开
加固阶段控制的是候选内容。规则选择、代码变换、资源处理、完整性配置和兼容排除都可能改变最终字节,因此执行者能够决定什么进入待签名包。这个角色需要工具和输入权限,却不因此获得生产签名权或商店发布权;否则一次未经批准的产物替换可能直接跨过后续身份和渠道门禁。
签名阶段控制的是应用身份。Android 依赖应用签名来建立安装、更新和签名者关系,Apple 平台也以代码签名、证书和运行验证连接应用与授权主体。签名成功只说明特定字节由相应身份签署,并不自动说明保护策略正确、业务兼容或测试已经完成,所以签名者必须核对被批准候选的摘要和签名前置条件。
发布阶段控制的是用户实际拿到哪一个版本。商店上传、企业分发、灰度范围、版本启停和回滚入口都属于渠道权限。发布人员不需要修改加固规则,也不应直接接触私钥材料;他们需要的是经过签名、验证和审批的唯一候选,以及能够证明候选身份的摘要、证书信息和门禁回执。
| 阶段 | 控制对象 | 必要输入 | 不应默认拥有 |
|---|---|---|---|
| 加固 | 候选字节和保护配置 | 已批准输入包与策略 | 生产私钥和发布账号 |
| 签名 | 应用身份与完整性声明 | 待签名摘要和审批回执 | 修改候选内容的权限 |
| 发布 | 渠道、范围和上线时点 | 已签名候选与门禁结果 | 加固配置和私钥读取权 |
| 安全审批 | 保护范围和残余风险 | 威胁、限制与验证证据 | 替代签名或发布执行 |
| 测试接受 | 业务与设备兼容 | 同一候选和测试范围 | 更换生产候选 |
| 审计复核 | 证据链和权限冲突 | 身份、时间、摘要与日志 | 代替业务风险批准 |
用不可变摘要完成每次候选交接
职责分离只有在交接对象可识别时才有意义。研发交给加固方的输入包、加固方交给签名服务的待签名包、签名服务交给发布方的最终包,都要计算强摘要并记录大小、格式、构建标识和来源。接收方先重新计算摘要,再检查它是否等于上一角色批准的值,校验失败就停止流程。
SLSA provenance 描述了产物主体、构建者、构建类型、外部参数和依赖材料之间的关系;in-toto Statement 则把产物摘要放进 subject,并用 predicateType 标明声明负载。工程上可借用这种结构,把每一步的事实写成机器可核对记录,而不是让报告只说某版本已经完成加固或已经走完签名。
摘要不能单独证明执行过程可信,但能暴露最常见的错配:审批针对 A 包,签名实际处理 B 包,发布人员又从共享目录取出 C 包。记录还要包含执行主体、开始和结束时间、工具版本、配置标识及上一步证明引用。若某个字段无法取得,应明确标记缺失并阻止高风险交接,而不是补写推测值。
| 字段 | 产生方 | 接收方检查 | 失败处置 |
|---|---|---|---|
| artifact_sha256 | 当前处理阶段 | 重新计算并精确比较 | 停止接收候选 |
| input_sha256 | 当前处理阶段 | 对应上一步输出 | 追查来源错配 |
| config_id | 加固或构建系统 | 匹配已批准配置 | 退回重新审批 |
| actor | 身份系统 | 确认主体与角色一致 | 拒绝匿名回执 |
| timestamp | 受控执行环境 | 检查流程顺序 | 标记时序异常 |
| evidence_ref | 门禁服务 | 打开并校验引用 | 视为证据缺失 |
加固执行者只控制保护处理所需权限
加固人员或服务需要读取已批准输入、应用保护配置并写出新候选,还可能读取公开符号信息、规则清单和测试用配置。权限应限定在项目、候选和执行窗口,输出写入只追加或版本化存储。执行者不能覆盖已经批准的旧候选,也不能把未经登记的本地文件直接送入签名队列。
保护配置的批准与执行要分开。安全负责人根据威胁、业务价值和兼容边界批准配置,执行方按批准版本运行;遇到崩溃、反射、序列化、JNI 或平台生成代码问题时,执行方可以提出排除建议,但不能自己降低范围后继续交付。任何变更都要生成新配置标识、新候选摘要和新的接受记录。
供应商参与并不意味着生产密钥也必须外发。更稳妥的边界是供应商接收可处理的未签名输入,交付带摘要的未签名输出和技术说明,客户在自己的受控签名系统内完成身份授予。若部署形态必须由外部环境触发签名,仍应使用不可导出的密钥、最小签名接口、候选白名单、双人批准和完整审计。
| 权限 | 默认决定 | 理由 | 可接受例外 |
|---|---|---|---|
| 读取批准输入 | 允许 | 完成保护处理 | 限制到项目与候选 |
| 写入新加固输出 | 允许 | 形成待验证候选 | 只追加并记录摘要 |
| 修改已批准策略 | 拒绝 | 执行不能替代风险批准 | 重新走策略审批 |
| 读取生产私钥 | 拒绝 | 超出保护处理需要 | 不以文件形式外发 |
| 直接发布生产渠道 | 拒绝 | 绕过签名和接受门禁 | 无默认例外 |
| 查看必要测试日志 | 受限允许 | 支持兼容定位 | 脱敏并按项目授权 |
生产签名服务只为已批准候选授予身份
签名服务的输入必须是明确摘要的待签名候选和独立审批记录,而不是一个允许调用者任意写入的文件路径。服务先核对应用标识、版本、证书策略、候选摘要与审批状态,再在受控环境执行签名。返回结果至少包含签名前后摘要、证书指纹、签名方案验证结果、执行身份和时间,不返回私钥。
AOSP 的应用签名说明表明,不同 APK 签名方案覆盖方式和平台支持不同;Android apksigner 文档还明确提醒,APK 签名后再修改会使签名失效。因此对加固结果的任何字节级修改都必须发生在签名前。签名后的对齐、压缩、资源替换或补丁操作不能被视为小修,应回到候选生成环节重新形成证据链。
签名验证是门禁的一部分,不是完整交付结论。校验证书和签名方案可以发现候选损坏、错误身份或流程顺序问题,却不能证明密钥由正确团队托管,也不能证明加固配置、业务路径和商店政策已经通过。发布方还要把验证结果绑定到最终上传文件,避免验证一个文件却发布另一个同名文件。
| 检查点 | 输入证据 | 通过条件 | 输出记录 |
|---|---|---|---|
| 候选身份 | 待签名 SHA-256 | 匹配批准摘要 | 签名前摘要 |
| 应用身份 | 包名或 Bundle ID | 匹配授权策略 | 应用标识 |
| 证书策略 | 允许证书指纹 | 签名者在白名单 | 实际证书指纹 |
| 审批状态 | 双人或独立批准 | 审批仍有效 | 审批引用 |
| 签名验证 | 签名后候选 | 工具验证成功 | 方案和证书结果 |
| 审计身份 | 工作负载或人员身份 | 主体可追溯 | 执行者与时间 |
Android 与 Apple 的签名证据不能混成一张截图
Android 的应用签名围绕 APK 或 AAB 的发布身份、证书和平台支持展开,Apple 平台则通过代码签名、证书、授权和运行验证建立信任。两者都要求保护私钥并绑定具体应用,但产物结构、验证工具和渠道责任并不相同。团队可以共用职责原则,不能共用一个模糊的签名成功字段。
Android 交接应记录原始 APK、加固后未签名 APK、最终签名 APK 的摘要,并保存 apksigner 对最终 APK 的方案与证书验证结果。若渠道采用上传密钥和应用签名密钥分工,还要写清哪个身份完成上传、哪个身份最终为用户分发包签名,避免把上传成功误写成最终签名已核对。
Apple 交接应记录归档、导出配置、签名身份、授权和最终分发产物之间的关系。加固工具若介入 Mach-O 或资源处理,处理必须位于最终代码签名前,签名后修改同样会破坏可信链。文章中的职责模型不声称替代 Apple 或 Android 的平台检查,实际项目仍需按目标渠道保存对应验证回执。
| 平台 | 候选标识 | 身份核对 | 边界 |
|---|---|---|---|
| Android APK | 签名前后摘要 | 证书与签名方案 | 验证不等于业务安全 |
| Android AAB | Bundle 摘要与渠道记录 | 上传和应用签名角色 | 本地包不等于用户分发包 |
| Apple Archive | 归档与导出产物标识 | 证书、授权与团队 | 归档通过不等于渠道接受 |
| Apple 分发包 | 最终产物摘要 | 代码签名验证 | 不能套用 APK 方案字段 |
| 共同要求 | 唯一候选引用 | 受控私钥与审计 | 不同平台分别留证 |
| 禁止做法 | 只记文件名 | 截图代替机器回执 | 混写为统一签名成功 |
发布权限只消费门禁结果,不重新制造候选
发布人员的工作是确认批准集合完整并把指定候选送入正确渠道。集合通常包括最终签名摘要、证书或签名身份、保护配置标识、兼容测试范围、风险例外、版本信息和回滚条件。发布账号不应具备覆盖加固输出或调用任意签名请求的能力,否则渠道控制与候选制造重新汇合。
上传前要从受控产物库按摘要取包,现场重新计算摘要,并与签名回执和发布审批精确比较。不能依赖下载文件名、修改时间或群聊中的最终版标签。若平台在上传后进行再签名或加工,还应保存平台返回的版本和身份信息,并区分本地上传候选与用户最终可获得产物。
灰度、暂停、扩大范围和回滚同样属于生产权限,应保留操作者、理由、目标版本和时间。事故中为了快速止损可以启动预先定义的紧急流程,但紧急流程不是取消审计;它需要限制动作、短时授权、事后复核和自动撤销。未经批准的临时账号不能成为长期发布入口。
| 门禁 | 接受条件 | 拒绝示例 | 责任角色 |
|---|---|---|---|
| 候选摘要 | 与签名回执一致 | 只凭文件名判断 | 发布执行者 |
| 签名身份 | 匹配应用和渠道策略 | 证书未知或过期 | 签名负责人 |
| 测试接受 | 范围和候选明确 | 报告未绑定摘要 | 测试负责人 |
| 风险例外 | 有范围、责任人和到期 | 口头接受 | 业务风险负责人 |
| 渠道配置 | 版本与灰度计划批准 | 临时改范围 | 发布负责人 |
| 回滚准备 | 入口和目标版本可识别 | 只有原则性描述 | 发布与运维 |
用独立审批、短时授权和紧急通道处理现实限制
职责分离不等于每个按钮都必须由不同部门点击。关键是高影响动作之间存在不可跳过的独立判断,并且任何单一主体无法同时改包、授予身份和触达用户。规模较小的团队可以让同一人承担多个职务,但签名或发布至少需要另一名有上下文的人核对摘要、审批范围和例外。
自动化服务账号也属于主体,不能因为没有人类姓名就绕过冲突检查。负责生成加固候选的工作负载不应同时拥有无约束的生产签名和渠道写入权限。更适合的做法是让流水线获取按候选签发、有效期短、用途受限的令牌,并由策略服务检查摘要、环境、应用和审批引用。
紧急通道要预先定义触发条件、可执行动作、批准者、到期时间和事后复盘。比如只允许暂停灰度或回滚到已知候选,不允许在紧急状态下上传未经测试的新包。break-glass 账号的使用应产生高优先级审计事件,并在窗口结束后撤销权限、轮换凭据和核对所有受影响候选。
用代码检查角色权限矩阵中的三权合一
权限矩阵适合在流水线变更时自动校验,因为冲突往往来自新增服务账号或临时授权,而不是制度文本缺失。下面的 Python 脚本读取调用者提供的 JSON 文件,检查主体名称、权限列表和关键权限覆盖;只要同一主体同时拥有替换加固候选、生产签名和生产发布权限,就返回可定位错误。
脚本还检查外部供应商是否获得生产签名权限,并验证 break_glass 记录是否包含批准人、工单和到期时间。它不会连接云平台、修改账号或读取密钥,只对导出的权限快照做静态判断。真实落地时应从身份系统和 CI/CD 平台取得快照,并将结果作为审批证据,而不是手工维护一份长期不更新的示例。
通过脚本不代表权限设计已经安全。它只覆盖预先定义的直接权限,角色继承、临时令牌、组织管理员、密钥服务代理和渠道平台超级账号仍需展开分析。团队还要验证拒绝路径,确认未获授权的加固账号确实无法签名,签名账号确实无法替换候选,发布账号也无法绕过受控产物库。
import json
import sys
from datetime import datetime, timezone
from pathlib import Path
CRITICAL = {
'replace-hardened-artifact',
'production-sign',
'production-release',
}
if len(sys.argv) != 2:
raise SystemExit('usage: validate_sod.py permissions.json')
path = Path(sys.argv[1])
if not path.is_file():
raise SystemExit('permissions file is missing')
data = json.loads(path.read_text(encoding='utf-8'))
subjects = data.get('subjects')
if not isinstance(subjects, list) or not subjects:
raise SystemExit('subjects array is missing')
seen = set()
coverage = {permission: set() for permission in CRITICAL}
for index, subject in enumerate(subjects):
name = str(subject.get('name', '')).strip()
kind = str(subject.get('kind', '')).strip()
permissions = subject.get('permissions')
if not name or name in seen:
raise SystemExit(f'invalid or duplicate subject at index {index}')
if kind not in {'person', 'service', 'vendor'}:
raise SystemExit(f'{name}: unsupported subject kind')
if not isinstance(permissions, list) or not permissions:
raise SystemExit(f'{name}: permissions must be a non-empty list')
permission_set = {str(item).strip() for item in permissions}
if '' in permission_set:
raise SystemExit(f'{name}: blank permission')
seen.add(name)
for permission in CRITICAL:
if permission in permission_set:
coverage[permission].add(name)
if CRITICAL <= permission_set:
raise SystemExit(f'{name}: can replace, sign and release one candidate')
if kind == 'vendor' and 'production-sign' in permission_set:
raise SystemExit(f'{name}: vendor has production signing permission')
for permission, owners in coverage.items():
if not owners:
raise SystemExit(f'missing owner for {permission}')
break_glass = data.get('break_glass')
if break_glass is not None:
required = {'approved_by', 'ticket', 'expires_at'}
if not isinstance(break_glass, dict) or not required <= break_glass.keys():
raise SystemExit('break_glass record is incomplete')
expires_at = datetime.fromisoformat(break_glass['expires_at'].replace('Z', '+00:00'))
if expires_at <= datetime.now(timezone.utc):
raise SystemExit('break_glass authorization has expired')
print(json.dumps({'subjects': len(subjects), 'coverage': sorted(coverage), 'status': 'pass'}))把证据、失败路径和复核责任写进日常流程
落地时先列出能够改变候选、调用生产签名、管理证书策略、写入发布渠道、扩大灰度和回滚的全部主体,再展开组继承、服务角色和紧急账号。矩阵中的每项权限都要对应真实系统控制和拒绝测试,纸面上写无权限而平台仍允许操作,只会制造虚假的安全感。
每次版本交付应形成连续证据:输入构建摘要、加固配置与输出摘要、独立测试接受、签名请求与验证、发布审批、渠道回执和实际版本标识。NIST SSDF 提供的是组织级安全开发和供应链实践方向,具体字段、审批强度与保存周期仍需结合团队风险和平台能力确定,不能借规范名称宣称项目已通过。
御盾项目评估可以从权限矩阵和一条真实候选链开始:列清所有主体,导出实际权限,固定候选摘要,再分别验证加固、签名和发布的允许与拒绝路径。需要申请加固或核对交付边界时,可通过御盾中央平台提交应用类型、目标渠道、当前签名方式和发布流程,由项目人员确认哪些职责必须保留在客户控制域。
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 安全开发和发布流程应保存来源、构建、验证与变更证据,并处理供应链风险。 | NIST SP 800-218 SSDF 提供组织级安全软件开发实践与供应链风险方向。 | 该框架不定义某个加固产品能力,也不证明具体项目已经执行这些实践。 |
| 构建证明可以把产物主体与构建者、构建类型、参数和材料关联。 | SLSA Provenance v1.1 定义 provenance 的模型与字段语义。 | provenance 记录不能单独证明运行安全、人员诚信或候选已经通过业务验收。 |
| 供应链声明应以摘要标识产物,并给声明负载明确类型。 | in-toto Attestation Statement v1 定义 subject、predicateType 和 predicate。 | 格式正确不保证声明真实,仍需可信签名者、策略与门禁核验。 |
| Android 应用签名用于建立应用身份、完整性和更新关系。 | AOSP app signing 说明 Android 应用签名和多种 APK 签名方案。 | 签名有效不证明业务代码安全、加固有效或真实设备兼容。 |
| Apple 平台以代码签名、证书和运行验证建立应用身份。 | Apple app code signing process 说明 Apple 平台的代码签名过程。 | Apple 证据链不能直接替代 Android APK 的签名方案和证书验证。 |
| APK 应在最终修改完成后签名,并可核验签名方案与证书信息。 | Android apksigner 说明签名、验证及签名后修改 APK 的限制。 | 工具验证成功不证明私钥治理、候选来源、渠道上传或测试已经正确。 |
| 修改候选、生产签名和生产发布不应由一个主体无复核地全部控制。 | 工程判断:三项权限合并后,单一主体可以把未经批准的候选直接变成用户可获取版本。 | 职责分离降低流程滥用与误操作风险,但不能证明账号未被攻破或产物没有漏洞。 |
| 小团队的角色兼任必须通过独立复核、短时授权和明确例外来约束。 | 工程判断:人数限制不消除冲突权限,需要用技术控制保存第二判断与撤销路径。 | 具体审批人数和权限模型取决于组织、平台与风险,文章不提供通用合规结论。 |
工程常见问题
加固供应商是否必须拿到生产签名私钥?
不必须。通常可由供应商处理未签名候选并交付摘要和回执,客户在自己的受控环境完成生产签名。特殊委托也应使用不可导出密钥、最小接口、审批和审计。
同一个人可以同时负责签名和发布吗?
小团队可能兼任,但要登记权限冲突,并由另一名有上下文的人员核对候选摘要、签名身份和发布范围。高影响操作还应采用短时授权和自动撤销。
为什么只看文件名不能完成候选交接?
文件名、目录和修改时间都可能重复或被覆盖,无法证明审批、签名和发布针对同一字节。应重新计算摘要,并把摘要写入每一步回执。
apksigner 验证通过是否代表加固项目验收通过?
不代表。它能核验 APK 的签名方案与证书等信息,但不能证明保护配置、真实业务兼容、私钥治理或渠道发布过程正确。
紧急发布能否跳过职责分离?
紧急流程可以缩短等待,但不应允许任意新候选绕过门禁。更合适的做法是限制为暂停、回滚等预定义动作,并保留短时授权、审计和事后复核。
申请御盾加固前应准备哪些职责信息?
准备应用平台、加固输入来源、生产签名方式、密钥控制方、测试接受人、发布渠道、账号主体和紧急流程。通过御盾中央平台提交后,再确认交接摘要与批准边界。