先看结论与判断条件
- 签名责任必须拆成身份所有者、签名执行者、候选批准者和分发上传者,四个角色可以由同一组织承担,但证据不能合并成一句口头承诺。
- 加固前候选、加固后未签名候选和最终已签名候选是三个不同对象,必须分别计算摘要并记录转换关系,文件名和版本号不能代替身份。
- 代码签名证明代码与签名身份的绑定和完整性校验条件,不证明业务逻辑安全、加固效果、隐私合规或服务端授权已经成立。
- App Attest 属于运行期服务端验证边界,不能替代发布签名;发布签名有效也不能省略 challenge、断言验证、重放控制和风险处置。
- 隐私清单与第三方 SDK 责任必须在加固前确定,因为依赖、资源和声明的变化可能改变审核材料,签名通过不会自动修正这些声明。
- 交付前应运行可重复的清单校验,检查候选摘要、标识符、渠道、责任人和必要材料是否齐全;校验通过只是进入签名和分发流程的条件。
先把签名责任拆成可验收的角色,而不是一句“由客户签名”
加固接入会跨过研发、移动安全、证书管理和发布运营四类工作。若需求单只写“签名由客户负责”,发生问题时无法判断是签名身份选错、权限配置漂移、加固后候选被替换,还是上传人员拿了另一份 IPA。可执行的做法是给每个动作指定角色、输入、输出、批准条件和回执,并在工作开始前由双方确认。
责任矩阵至少要区分签名身份所有者、签名执行者、加固候选批准者、最终候选批准者、分发上传者、隐私声明维护者、第三方 SDK 负责人和平台证明服务端负责人。同一人可以承担多个角色,但每个角色仍要有独立动作记录。这样做不是增加流程,而是让失败能够落到一个具体输入和一个可修复责任点。
边界确认还要写明加固服务能够接触什么。通常可以提交候选文件、公开构建元数据和经过最小化处理的诊断材料;私钥、证书口令、Apple 账号会话、一次性验证码和无关业务数据不应因为“方便签名”进入普通工单或内容系统。若最终签名必须由资产所有者控制,就应把签名动作留在其受控环境,只交付摘要和结果回执。
| 动作 | 责任角色 | 必要输入 | 可验收输出 | 拒绝条件 |
|---|---|---|---|---|
| 批准加固前候选 | 候选批准者 | 文件摘要与构建标识 | 批准记录 | 摘要缺失或文件已变化 |
| 执行最终签名 | 签名执行者 | 加固后未签名候选与授权身份 | 已签名候选摘要 | 身份、渠道或权限不匹配 |
| 批准最终候选 | 发布负责人 | 签名检查与回归回执 | 候选批准记录 | 检查对象不是同一摘要 |
| 上传分发平台 | 分发上传者 | 已批准的最终候选 | 平台接收回执 | 上传文件摘要无法对应 |
| 维护隐私与 SDK 声明 | 隐私和依赖负责人 | 依赖清单与数据行为说明 | 复核记录 | 声明来源不清或版本未锁定 |
用三个候选身份固定加固前、加固后和最终签名阶段
一个可审计交付至少存在三个候选:进入加固流程的输入候选、加固完成但尚未执行最终发布签名的输出候选,以及准备上传的最终已签名候选。签名和重新打包都会改变文件字节,因此三个对象不能共享同一个 SHA-256。清单应记录每个对象的摘要、大小、生成时间、Bundle ID、版本、构建号和产生该对象的责任步骤。
Bundle ID、版本和构建号用于说明业务身份,却不足以唯一定位文件。同一组标识可以对应多次构建、不同编译器输入或不同签名结果;相同文件名也可能被复制覆盖。验收时应以实际文件重新计算的 SHA-256 为主键,再把 Team ID、计划分发渠道、签名证书公开指纹、权限清单摘要和来源证明作为关联属性,避免凭截图或文件名放行。
签名材料的记录要遵守最小披露。清单可保存证书公开信息、签名身份标签、Team ID、权限摘要和签名动作回执,不应保存私钥原文、导出密码或账号凭据。若签名通过硬件密钥、受控构建节点或专用签名服务完成,应记录执行环境标识、审批号与结果摘要,而不是把密钥复制到加固服务或发布人员电脑。
| 候选阶段 | 身份主键 | 关联字段 | 谁批准 | 不可替代证据 |
|---|---|---|---|---|
| 加固前输入 | 输入文件 SHA-256 | Bundle ID、版本、构建号 | 研发与安全 | 实际提交文件的重算摘要 |
| 加固后未签名输出 | 输出文件 SHA-256 | 工具版本、配置摘要、输入摘要 | 安全与发布 | 输入到输出的转换回执 |
| 最终已签名候选 | 最终文件 SHA-256 | Team ID、渠道、权限摘要 | 发布负责人 | 签名检查和同摘要回归 |
| 平台接收对象 | 上传文件 SHA-256 | 平台记录号与时间 | 分发上传者 | 上传前后的身份对应记录 |
正确理解 Apple 代码签名能证明什么,以及不能证明什么
Apple app code signing process 说明,平台使用代码签名、证书和运行时验证建立 App 代码与获准开发者身份之间的关系。工程上应把签名验证作为候选身份和完整性门禁:签名执行前确认目标身份与渠道,执行后从最终候选读取签名信息,并把结果绑定到该文件摘要。任何重新打包、资源变更或再签名都产生新的验收对象。
权限声明是签名边界中的高风险字段。加固前应导出原候选所需的能力和权限,说明哪些必须保留、哪些由渠道或账号配置决定;加固后再从最终候选读取实际结果,逐项比较。仅凭安装成功不能证明权限完全一致,因为未覆盖的系统能力、推送、关联域或密钥链访问可能只在特定路径和账号状态下暴露问题。
有效签名不等于安全结论。它不能证明加固策略阻断了逆向分析,不能证明第三方 SDK 没有收集数据,也不能证明服务端接受的请求来自可信业务会话。文章中的签名清单只用于控制身份、责任和交付一致性;保护强度、兼容范围和攻击面变化仍需要针对同一最终候选的独立测试证据。
| 检查对象 | 签名边界内的结论 | 签名边界外的结论 | 需要补充的证据 |
|---|---|---|---|
| 证书与身份 | 候选关联到指定签名身份 | 身份持有人一定批准业务行为 | 审批记录与账号治理 |
| 代码完整性 | 签名后代码变化可被验证机制识别 | 代码不存在业务漏洞 | 代码审计和安全测试 |
| 权限声明 | 最终候选携带实际签名权限 | 所有运行场景兼容 | 同摘要设备与业务回归 |
| 加固效果 | 无直接结论 | 不能由签名有效推导保护强度 | 独立逆向与运行验证 |
把分发渠道写成签名输入,不要到上传前才决定
分发渠道不是发布团队最后填写的备注,而是签名责任的输入。项目应在加固前写明计划渠道、负责账号、最终签名发生位置、允许使用的签名身份、需要保留的能力,以及平台拒绝时由谁收集回执。渠道尚未确定时可以保留候选,但不能把该候选标记为可发布,更不能提前声称签名方案已验收。
交接时应采用单向清单:加固输出先由候选批准者确认摘要,再进入受控签名环境;签名执行者输出最终摘要和公开签名检查结果;发布负责人只接受清单中的最终摘要;上传者在发送前再次重算文件摘要。任何人若重新导出、压缩、嵌入资源或改变权限,都必须回到新的候选记录,不能沿用旧批准。
平台接收回执也要限制结论。上传成功只能说明平台在该时点接受了这个提交步骤,不能代替后续审核、设备运行、业务回归或加固强度验证。相反,平台拒绝信息应与最终候选摘要、渠道和签名回执一起保存,便于判断问题属于身份配置、权限声明、隐私材料还是二进制本身,而不是无目标地重复签名。
| 阶段 | 必须冻结的输入 | 允许动作 | 变更后的处理 |
|---|---|---|---|
| 渠道确认 | 渠道、账号责任、能力需求 | 评审与批准 | 更新责任矩阵 |
| 签名执行 | 加固后候选摘要与签名身份 | 在受控环境签名 | 生成新最终摘要 |
| 发布批准 | 最终摘要与检查回执 | 同摘要回归和批准 | 任何字节变化都撤销批准 |
| 平台上传 | 已批准最终文件 | 重算摘要后上传 | 记录拒绝或接收回执 |
把 App Attest 放在服务端验证边界,不能拿它替代发布签名
Apple App Attest server validation 要求服务端验证证明并将其与请求处理绑定。接入说明应指定谁维护服务端验证逻辑、谁保管相关服务端配置、如何生成挑战、如何处理断言计数或重放风险,以及验证失败时采用拒绝、降级还是人工复核。把验证仅放在客户端会让信任决定处于可被本地修改的环境中。
App Attest 与发布签名解决不同问题。发布签名用于代码身份和平台执行链,App Attest 为服务端提供平台证明相关信号;二者都不能独立证明用户已经获得业务授权。加固可能改变客户端代码组织,但是否影响证明接入必须由同一最终候选进行注册、挑战、断言和服务端验证回归,不能从编译通过或签名有效推导。
证明数据也需要最小化处理。日志可以记录请求关联号、结果类别、服务端策略版本和经过控制的诊断字段,不应公开设备标识、完整令牌、私钥或可重放材料。平台证明仍是风险信号,业务系统要结合账号状态、会话、权限和异常处置作决定;校验器只能确认责任字段存在,不能模拟 Apple 服务或宣布真实性已经通过。
| 控制点 | 责任方 | 应保存的证据 | 不能推出的结论 |
|---|---|---|---|
| 挑战生成 | 服务端负责人 | 策略版本与请求关联号 | 客户端绝对可信 |
| 证明验证 | 服务端负责人 | 验证结果类别与时间 | 用户已获业务授权 |
| 失败处置 | 风控与业务负责人 | 拒绝或降级规则 | 所有失败都是攻击 |
| 加固回归 | 安全与服务端团队 | 同摘要端到端回执 | 签名有效即可免测 |
隐私清单和第三方 SDK 由应用方负责,加固不能替代声明核对
Apple privacy manifests 说明,App 与第三方 SDK 的数据收集和 required reason API 信息需要进入有效的隐私清单。加固接入前应由应用方提供当前依赖锁定结果、SDK 版本、隐私清单位置和声明负责人;加固完成后再核对资源是否保留、归档是否包含预期清单,以及最终提交材料是否对应同一候选。
Apple third-party SDK requirements 强调开发者仍对集成 SDK 的代码与数据行为负责,部分列明的 SDK 还涉及签名和隐私清单要求。签名责任矩阵因此要包含 SDK 所有者:谁批准版本、谁确认来源、谁处理供应商更新、谁验证 SDK 自带清单与签名材料。加固服务处理二进制不等于接管这些产品和合规判断。
隐私清单是声明与审核材料,不是运行时数据隔离。文件存在不能证明 SDK 实际行为与声明一致,也不能证明网络传输、存储和服务端使用符合业务政策。若加固流程改变依赖、资源或初始化路径,应重新做清单对比和行为回归;若没有变化,也要以摘要或构建来源记录证明核对对象,而不是沿用上一版本截图。
| 材料 | 所有者 | 加固前检查 | 加固后检查 | 证据边界 |
|---|---|---|---|---|
| App 隐私清单 | 隐私负责人 | 声明与版本基线 | 最终归档中的实际文件 | 不证明运行行为一致 |
| SDK 清单 | SDK 负责人 | 依赖版本与来源 | 资源、签名和声明保留 | 不代表 SDK 已安全评估 |
| required reason API | 研发与隐私负责人 | 调用与理由映射 | 同候选复核 | 不能由文件存在自动确认 |
| 数据行为 | 产品与合规负责人 | 收集目的和去向 | 运行观测与复核 | 不属于代码签名结论 |
用来源证明和 SSDF 记录责任链,但不要把记录格式当成真实保证
SLSA Provenance v1.1 提供把产物主体、构建者、构建类型、外部参数和依赖材料写入来源证明的结构。用于 iOS 加固交付时,主体应绑定实际候选摘要,参数可记录经过审查的配置摘要,材料可引用输入候选和工具链标识。证明若只写版本号而不写主体摘要,仍无法回答最终上传文件是否来自该流程。
NIST SP 800-218 SSDF 将安全开发、来源、验证、变更和供应链风险放进组织实践。责任矩阵应据此指定证据保留人、复核人和异常处置人,并明确多长时间内能够取回候选、签名回执、依赖清单和测试结果。SSDF 不定义某个加固产品的功能,也不会替代 Apple 平台规则或项目实测。
证明格式本身也不保证内容真实。签发者身份、生成环境、时间、输入和主体摘要都要接受独立校验。交付演练应随机选择一个最终候选,从平台回执反向找到摘要、签名检查、来源证明、加固输入、SDK 清单和责任批准;任何断链都应在下一次发布前修复,而不是等线上崩溃或审核拒绝后再补材料。
| 证据 | 绑定对象 | 复核问题 | 失效信号 |
|---|---|---|---|
| 来源证明 | 加固后或最终候选摘要 | 主体、构建者和输入是否一致 | 只记录版本而无摘要 |
| 签名回执 | 最终候选摘要 | 身份、Team ID 和权限是否符合批准 | 回执对应另一文件 |
| SDK 与隐私基线 | 构建来源和最终归档 | 版本、声明与资源是否对应 | 依赖漂移或材料缺失 |
| 平台回执 | 实际上传对象 | 能否反查完整责任链 | 无法确认上传文件身份 |
用只读清单校验器在提交前发现签名责任和候选错配
下面的 Python 示例读取归档根目录和 `ios-signing-boundary.json`。清单必须声明候选相对路径与 SHA-256、Bundle ID、Team ID、分发渠道、六类责任角色、隐私清单和 SDK 清单状态。校验器拒绝绝对路径、目录逃逸、符号链接、格式错误、占位责任人和候选摘要错配,并输出待签名检查表。
脚本只读文件,不调用 `codesign`,不访问钥匙串,也不接触 Apple 账号或私钥。它确认的是清单完整性和候选字节身份,不确认签名证书有效、provisioning 合法、App Attest 服务端验证成功、隐私声明真实或加固保护强度。真实发布仍需在受控签名环境对同一摘要执行平台检查与设备回归。
准备 iOS 加固申请时,可整理三个候选阶段的摘要计划、Bundle ID、Team ID、分发渠道、权限基线、责任矩阵、SDK 与隐私清单、App Attest 服务端责任和来源证明,再通过御盾中央平台提交申请。提交动作不应附带私钥、证书密码或账号会话;校验通过也不表示签名、审核或兼容结果已经确认。
- 候选路径受归档根目录限制且不允许符号链接
- 实际候选 SHA-256 与清单完全一致
- Bundle ID、Team ID 和计划分发渠道格式明确
- 七类责任角色均有非占位负责人
- 隐私清单已经复核并有负责人
- 第三方 SDK 清单已经锁定并有负责人
- 校验结果不冒充签名、审核、兼容或加固效果结论
from pathlib import Path
import hashlib
import json
import os
import re
import sys
if len(sys.argv) != 3:
raise SystemExit(2)
root = Path(sys.argv[1]).resolve()
manifest_path = Path(sys.argv[2]).resolve()
if not root.is_dir() or not manifest_path.is_file() or root not in manifest_path.parents:
raise SystemExit(2)
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
if manifest.get("schema") != "ios-signing-boundary-v1":
raise SystemExit(2)
def safe_file(relative_path):
if not isinstance(relative_path, str) or not relative_path or os.path.isabs(relative_path):
raise SystemExit(2)
unresolved = root / relative_path
resolved = unresolved.resolve()
if root not in resolved.parents or unresolved.is_symlink() or not resolved.is_file():
raise SystemExit(2)
return resolved
def sha256(path):
digest = hashlib.sha256()
with path.open("rb") as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
candidate = manifest.get("candidate")
if not isinstance(candidate, dict):
raise SystemExit(2)
expected = str(candidate.get("sha256", ""))
if not re.fullmatch(r"[0-9a-f]{64}", expected):
raise SystemExit(2)
candidate_path = safe_file(candidate.get("path"))
actual = sha256(candidate_path)
if actual != expected:
raise SystemExit(3)
identifiers = manifest.get("identifiers")
if not isinstance(identifiers, dict):
raise SystemExit(2)
bundle_id = str(identifiers.get("bundleId", ""))
team_id = str(identifiers.get("teamId", ""))
channel = str(identifiers.get("distributionChannel", ""))
if not re.fullmatch(r"[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+", bundle_id):
raise SystemExit(2)
if not re.fullmatch(r"[A-Z0-9]{10}", team_id) or not re.fullmatch(r"[a-z0-9_-]{2,40}", channel):
raise SystemExit(2)
required_roles = {"inputApprover", "signingIdentityOwner", "finalSigner", "releaseApprover", "privacyOwner", "sdkOwner", "attestationVerifier"}
responsibilities = manifest.get("responsibilities")
if not isinstance(responsibilities, dict) or set(responsibilities) != required_roles:
raise SystemExit(2)
for role, owner in responsibilities.items():
if not isinstance(owner, str) or not re.fullmatch(r"[A-Za-z0-9_.@-]{3,80}", owner) or owner.lower() in {"tbd", "unknown", "none"}:
raise SystemExit(2)
materials = manifest.get("materials")
if not isinstance(materials, dict) or materials.get("privacyManifestReviewed") is not True or materials.get("sdkInventoryLocked") is not True:
raise SystemExit(2)
report = {"status": "signing-boundary-complete", "candidateSha256": actual, "bundleId": bundle_id, "teamId": team_id, "distributionChannel": channel, "responsibilityCount": len(responsibilities), "boundary": "manifest validation does not prove signature validity, review acceptance, attestation truth, privacy behavior, compatibility, or hardening strength"}
print(json.dumps(report, ensure_ascii=False, indent=2))事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Apple 平台使用代码签名、证书和运行验证建立 App 代码与获准开发者身份之间的关系。 | Apple app code signing process 描述了 Apple 平台的代码签名过程和验证角色,可支持把签名身份与最终候选绑定为交付门禁。 | 该资料不证明应用逻辑安全、加固强度、隐私合规或同一候选已经通过设备回归,也不能套用为 Android 签名结论。 |
| App Attest 相关证明应由服务端验证并与请求处理绑定,客户端不能独自作出最终信任决定。 | Apple App Attest server validation 给出服务端验证 App Attest 材料的官方路径,因此责任矩阵需要明确服务端验证所有者。 | 平台证明是风险信号,不是绝对设备可信、真实用户身份或业务授权,也不替代发布代码签名。 |
| App 与第三方 SDK 的数据收集和 required reason API 信息需要纳入有效隐私清单。 | Apple privacy manifests 说明了为 App 或第三方 SDK 添加隐私清单的要求,可支持在加固前后核对相关资源和声明。 | 隐私清单是声明与审核材料,文件存在不证明实际运行时数据收集、传输、存储和使用与声明完全一致。 |
| 应用开发者仍对集成的第三方 SDK 代码和数据行为负责,部分 SDK 还受到额外签名与隐私材料要求约束。 | Apple third-party SDK requirements 列出开发者对第三方 SDK 的责任及相关要求,因此 SDK 所有者不能从签名责任中省略。 | 官方列表不表示某个 SDK 已通过安全评估,也不覆盖未列明依赖、Android 依赖或项目特有的数据处理。 |
| 安全发布流程应保留来源、构建、验证、变更和供应链风险处置证据,并指定责任人。 | NIST SP 800-218 SSDF 提供组织级安全软件开发实践,可支持建立证据保留、复核和异常处置责任。 | SSDF 不定义某个 iOS 加固产品的能力、签名步骤或测试通过标准,具体结论仍需项目证据。 |
| 来源证明可以把产物主体与构建者、构建类型、外部参数和依赖材料关联。 | SLSA Provenance v1.1 定义了 provenance 的主体和构建相关字段,可用于把加固输入、输出及配置摘要组成可检查关系。 | provenance 记录的结构不自动保证陈述真实,也不能独立证明运行兼容、隐私行为、平台审核或加固保护强度。 |
| 加固前输入、加固后未签名输出和最终已签名文件必须分别计算摘要并独立批准。 | 这是基于文件字节身份和变更控制的工程判断:重新打包或签名会形成新的文件对象,旧摘要不能继续代表新对象。 | 摘要只能证明字节身份和错配,不能说明文件来源可信、签名有效、业务正确或攻击防护达到目标。 |
| 私钥、证书密码、Apple 账号会话和一次性验证码不应进入普通加固工单或内容系统。 | 这是基于最小权限、职责分离和凭据暴露面的工程判断;交付清单只需记录公开证书信息、角色和动作回执。 | 具体密钥托管方式取决于组织安全策略、签名基础设施和渠道要求,本文不指定唯一实现,也不接触真实凭据。 |
工程常见问题
iOS 加固前为什么不能只写“最终由客户签名”?
因为这句话没有说明签名身份由谁批准、动作在哪里执行、哪个加固后摘要进入签名、谁核对权限、谁批准最终文件以及谁上传。出现安装、权限或审核问题时无法定位责任。应把身份所有者、签名执行者、候选批准者和上传者分别记录。
应该把 iOS 证书和私钥交给加固服务吗?
不能把交付便利当作交付私钥的默认理由。若组织要求资产所有者控制签名,应在其受控环境完成最终签名,只向流程提供公开证书指纹、Team ID、权限基线、候选摘要和签名结果。任何真实凭据传递都应经过独立授权和安全渠道。
为什么输入候选、加固输出和最终签名文件要有三个摘要?
它们是三个字节不同的对象。加固会改变输出,最终签名或重新打包也会改变文件。分别记录摘要才能证明哪个输入产生哪个输出,以及测试、批准和上传究竟针对哪一个文件。只记 Bundle ID、版本或文件名无法排除候选替换。
签名验证通过是否表示 iOS 加固已经成功?
不是。签名验证支持身份与完整性门禁,不证明保护策略抵抗逆向、业务功能兼容、隐私声明准确或第三方 SDK 安全。加固效果和兼容结果必须针对同一最终摘要分别执行安全验证、设备回归和业务验收。
App Attest 能否替代 iOS 发布签名或业务登录鉴权?
不能。App Attest 为服务端提供平台证明相关信号,发布签名建立代码与开发者身份的执行链,业务鉴权决定账号和操作权限。三者目的不同。服务端仍要验证证明、控制重放,并结合账号、会话和业务策略处理风险。
向御盾提交 iOS 加固申请前应准备哪些非敏感材料?
准备输入候选摘要、Bundle ID、版本与构建号、Team ID、计划渠道、权限基线、签名责任矩阵、SDK 锁定清单、隐私清单复核状态、App Attest 服务端责任和来源证明计划,再通过御盾中央平台提交申请。不要把私钥、证书密码或账号会话放入普通申请材料。