先看结论与判断条件
- 产品名称只用于人类识别,资产主键应由平台、applicationId 或 Bundle ID、变体、签名身份和渠道组成。
- 版本号不能代替二进制身份,同一版本可能有不同渠道、依赖、资源和签名候选,每个文件必须计算独立摘要。
- Android build type、product flavor、source set、applicationId 和签名配置组合出的 variant 要作为独立发布资产管理。
- AAB、派生 APK 集与设备实际安装身份分层登记,不能把上传 bundle 的摘要当成所有设备 APK 的摘要。
- 候选记录要关联来源证明、加固配置、测试范围、已知限制和发布状态,任何变化都产生新的不可覆盖记录。
- 每个应用、签名、渠道和候选都要有唯一业务与技术责任人,离职、下架、换签和迁移必须进入状态变更。
产品名和版本号无法唯一识别需要加固的二进制
企业资产目录常以产品名、负责人和当前版本为中心,这对业务盘点有用,却不足以支持软件加固接入。同一个产品可能同时存在 Android 与 iOS,生产与测试、国内与海外、免费与付费、多个渠道和历史签名;相同版本号也可能对应不同构建。若台账只写某应用 5.0,执行团队无法证明收到的是哪一个文件。
加固台账的最小识别链应从平台开始,接着记录 applicationId 或 Bundle ID、构建变体、目标渠道、签名身份和候选文件摘要。产品名、版本名称和版本号是检索字段,不是唯一键。每次接收制品都重新计算摘要,并把输入来源、接收时间和提交人绑定,禁止用文件名相同覆盖原记录。
资产台账的目标是支持接入、处理、验收、发布和退出决策,不是收集越多字段越好。真实用户数据、私钥、密码、令牌和内部凭据不应进入记录;签名只保存公开证书或团队标识,服务地址使用受控引用。字段要足以区分身份和责任,同时减少敏感信息在多个系统复制。
| 字段 | 业务目录用途 | 加固台账要求 | 常见错误 |
|---|---|---|---|
| 产品名 | 人类识别 | 仅作展示别名 | 当作唯一键 |
| 版本号 | 发布计划 | 与摘要共同记录 | 覆盖同版本渠道包 |
| 平台 | 产品分类 | 主键组成部分 | 混合 Android 与 iOS |
| 应用标识 | 技术检索 | 精确读取候选 | 手工猜测 |
| 签名身份 | 发布治理 | 公开指纹或团队 | 保存私钥 |
| 候选摘要 | 通常缺失 | 每个文件独立计算 | 只保存文件名 |
用分层主键表达应用、变体、渠道和候选
推荐把台账分成应用、发布身份、渠道、候选和处理记录五层。应用层保存业务名称、平台与应用标识;发布身份层保存公开签名指纹或 Apple 团队标识;渠道层保存商店、企业分发或测试轨道;候选层保存文件摘要、版本和来源;处理记录再关联加固配置、输出摘要、测试证据和状态。分层后,一个应用可以合法拥有多个候选,而不发生覆盖。
主键的设计原则是稳定与可复算。应用标识从实际制品读取,签名身份从候选或发布记录验证,候选摘要由文件字节计算,渠道由真实发布目标确认。责任人、版本名称和状态会变化,不适合成为主键。业务系统可为每层分配不可变 assetId,外部系统通过 assetId 与摘要引用,不依赖会改名的产品标题。
同一候选摘要只能对应一组平台、应用标识、变体和签名状态。如果相同字节被登记为两个应用身份,说明至少一条记录错误。同一身份可以保留多个历史候选,但当前状态在同一渠道只能有一个明确指向;灰度或并行版本需用独立轨道和生效条件表达,不能都标成唯一当前版本。
| 层级 | 核心字段 | 一对多关系 | 变更方式 |
|---|---|---|---|
| 应用 | 平台与应用标识 | 多个发布身份 | 新建历史不覆盖 |
| 发布身份 | 证书或团队标识 | 多个渠道 | 轮换并保留 lineage |
| 渠道 | 商店轨道和区域 | 多个历史候选 | 状态切换 |
| 候选 | 文件摘要和版本 | 多个处理记录 | 不可变新增 |
| 处理 | 输入输出和配置 | 多个验证回执 | 新配置新记录 |
| 验收 | 范围结果和限制 | 多个发布决定 | 批准状态追加 |
Android build variant 应当作为独立发布资产
Android build variants 说明 build type、product flavor、source set、applicationId 和签名配置会组合成不同 variant。同一代码仓库下的免费版、付费版、区域版和不同环境可能使用不同资源、SDK、权限、后端和证书。台账不能只写仓库或分支,而要保存最终 variant 名称、实际 applicationId、构建类型、flavor 维度与签名配置身份。
variant 信息应从构建输出和制品解析双向核对。构建系统声明 releaseFlavorA,不代表交付文件一定来自该任务;制品中的 applicationId、版本、ABI、资源与签名才是候选事实。若声明与文件冲突,状态标为 blocked 并要求重新提交,不能由资产管理员手工修改一列来让记录看起来一致。
加固规则也要按 variant 关联。反射、序列化、动态加载、第三方 SDK 和渠道资源可能在不同 variant 中变化,一份配置不能默认覆盖全部变体。处理记录保存配置版本与摘要、适用 variant 和排除项。任何源码、依赖、variant、配置或工具版本变化,都生成新候选与新处理记录。
| 字段 | 来源 | 核对动作 | 冲突处置 |
|---|---|---|---|
| applicationId | 构建与制品 | 精确比较 | 停止接入 |
| build type | 构建任务 | 绑定任务记录 | 重建候选 |
| product flavor | Gradle 配置 | 展开完整维度 | 禁止省略 |
| source set | 构建图 | 保存来源摘要 | 补齐材料 |
| 签名配置 | 发布流程 | 核对公开身份 | 修复签名链 |
| 加固配置 | 批准单 | 绑定配置摘要 | 生成新处理记录 |
签名身份要区分平台机制与发布责任
Sign your Android app 区分应用签名密钥、上传密钥、证书和 Play App Signing 责任。Android 台账至少记录最终应用证书指纹、上传身份是否适用、签名发生位置和验签回执,不能用上传包的证书猜测设备最终安装身份。签名轮换时保留旧身份、lineage 或迁移状态,并明确哪些历史候选仍可更新。
Apple app code signing process 说明 Apple 平台通过代码签名、证书与运行验证建立应用身份。iOS 记录使用 Bundle ID、团队或证书公开身份、分发方式、entitlement 摘要和候选摘要。Apple 与 Android 的签名链机制不同,应分别存储和验证,不能把同一指纹字段强行复用为跨平台身份。
资产台账不保存生产私钥、p12 文件、口令或渠道令牌。它保存谁负责密钥治理、签名在哪个受控边界发生、候选验签结果和公开身份引用。若执行方交付未签名产物,状态应明确 awaiting-signature;签名后产生新候选摘要并关联原处理记录,不能把未签名摘要更新为签名后摘要。
| 平台 | 公开身份 | 责任字段 | 禁止存储 |
|---|---|---|---|
| Android | 应用证书指纹 | 应用签名持有方 | 签名私钥 |
| Android Play | 上传与应用身份 | 托管签名责任 | 上传凭据 |
| iOS | 团队与证书身份 | 分发签名责任 | p12 与口令 |
| 企业分发 | 组织和分发身份 | 到期与撤销责任 | 渠道令牌 |
| 测试签名 | 测试身份 | 适用环境 | 冒充生产身份 |
| 轮换记录 | 旧新身份关联 | 迁移批准人 | 覆盖历史身份 |
AAB、派生 APK 和设备安装身份需要分层记录
Android App Bundle format 描述 AAB 由 base、feature、配置与资产模块组成,最终设备安装的是派生 APK 集。上传 AAB 的摘要能识别输入 bundle,却不能代表每台设备得到的 APK 字节。台账要分开记录 bundle 候选、平台派生关系、测试生成的 APK 集和设备实际安装身份,不能把它们压成一个安装包字段。
加固接入时先说明处理对象是 AAB、通用 APK、split APK 集还是中间制品。若处理 AAB,记录模块清单、签名前后阶段和派生验证策略;若处理 APK,记录它对应的 ABI、密度、语言或渠道条件。测试证据必须指向具体设备所安装的集合与最终签名身份,而不是只引用上传 bundle 的测试报告。
渠道也会产生不同候选与签名路径。Play、第三方商店、企业分发和官网包可以共享源码,却不必共享文件、证书、资源或更新机制。每个渠道资产保存当前候选、历史候选、发布状态、负责人和支持边界。跨渠道复制处理记录前必须证明输入和配置一致,不能因版本号相同自动复用。
用来源证明和声明把候选与处理记录绑定
SLSA Provenance v1.1 使用产物主体、构建者、构建类型、外部参数和材料描述构建证明。资产台账可以把 provenance 摘要或受控引用挂到候选,回答文件来自哪个构建、使用什么参数和依赖。provenance 记录不证明运行时安全,也不能单独确认加固效果;它的价值是减少来源不明和候选错配。
in-toto Attestation Statement v1 用 subject 摘要、predicateType 和 predicate 组织声明。处理回执可借鉴这一结构,让输入摘要、输出摘要、配置身份和测试声明引用同一 subject。格式正确不保证声明真实,因此台账还要保存签名者身份、验证状态和门禁结果,并对实际候选重新计算摘要。
证明材料不要直接堆进一个附件字段。分别记录构建 provenance、加固处理声明、签名回执、功能测试、安全验证、已知限制和发布决定,每项有类型、生成者、时间、适用候选和状态。这样能在变更时判断哪些证据失效,也能避免一份旧报告被复制到新候选上。
| 证据类型 | 绑定对象 | 验证动作 | 适用边界 |
|---|---|---|---|
| 构建 provenance | 输入候选 | 验签和摘要复算 | 不证明运行安全 |
| 处理声明 | 输入输出和配置 | 核对 subject | 不证明配置充分 |
| 签名回执 | 最终候选 | 验证公开身份 | 不证明渠道通过 |
| 功能测试 | 候选与设备 | 检查覆盖范围 | 不代表未测组合 |
| 安全验证 | 控制与候选 | 读取事实和限制 | 不证明攻击阻断 |
| 发布决定 | 渠道当前候选 | 核对批准人 | 不覆盖未来版本 |
状态、责任人与历史记录不能被当前值覆盖
候选状态可分为 received、blocked、ready-for-hardening、processed、under-test、accepted、published、superseded 和 retired。状态转换由明确回执驱动,例如摘要核对完成、处理输出生成、测试范围通过或渠道发布成功。管理员不能直接把 blocked 改成 accepted 而不提供批准记录。历史状态追加保存,当前状态只是计算结果。
责任人至少包括业务所有者、构建负责人、加固负责人、测试批准人、签名负责人和渠道负责人。一个人可以承担多个角色,但每个角色要有唯一当前主体与替补。人员离职或组织调整产生责任变更记录,不回写历史批准人。共同负责、研发团队和待定都不能作为可执行 owner。
资产退出时保留最后候选、签名身份、来源证明、加固配置、测试范围、限制和发布状态的受控索引,并回收提交、处理、签名和渠道访问。保留期限和销毁按独立政策执行,不能因为产品下架立即删除所有身份链,也不能无限保留供应方权限。台账记录退出批准与仍未解决的责任。
用代码发现一包多身份和同身份多当前候选
下面的 Python 示例读取公开安全的应用资产台账,验证 assetId、平台、应用标识、variant、签名指纹、候选摘要、渠道、负责人和状态。它拒绝敏感字段、重复 assetId、同一文件摘要对应多个身份,以及同一平台、应用、variant、签名和渠道同时存在多个 current 候选。代码只做结构与冲突检查,不接触真实包或签名私钥。
示例把身份键定义为 platform、applicationId、variant、signingFingerprint 和 channel。组织可根据实际情况增加区域、tenant 或分发类型,但新增字段必须有稳定来源,不要把产品名和版本名称塞回主键。历史候选使用 historical 状态保留,因此同一身份可以拥有多个历史摘要;只有 current 唯一。
校验通过不能证明记录内容真实,仍需从制品、签名和渠道回执独立核对。实际落库应使用唯一索引和事务保证并发写入,不只依赖离线脚本。若发现冲突,先冻结相关接入和发布动作,确认哪条记录绑定真实候选,再通过状态变更保留错误记录的审计轨迹。
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
REQUIRED = {
'assetId',
'platform',
'applicationId',
'variant',
'signingFingerprint',
'artifactSha256',
'channel',
'owner',
'status',
}
FORBIDDEN = {'privateKey', 'password', 'token', 'customerData'}
PLATFORMS = {'android', 'ios'}
STATUSES = {'current', 'historical', 'blocked', 'retired'}
SHA256 = re.compile(r'^[a-f0-9]{64}$')
AMBIGUOUS = re.compile(r'^(both|joint|tbd|双方|共同负责|待定)$', re.I)
def stop(message):
raise SystemExit(message)
def required_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:
stop(f'record {index} lacks fields: {missing}')
if record['platform'] not in PLATFORMS:
stop(f'record {index} has invalid platform')
if record['status'] not in STATUSES:
stop(f'record {index} has invalid status')
digest = required_text(record, 'artifactSha256', index)
if not SHA256.fullmatch(digest):
stop(f'record {index} has invalid artifact digest')
owner = required_text(record, 'owner', index)
if AMBIGUOUS.fullmatch(owner):
stop(f'record {index} has ambiguous owner')
identity = (
record['platform'],
required_text(record, 'applicationId', index),
required_text(record, 'variant', index),
required_text(record, 'signingFingerprint', index),
required_text(record, 'channel', index),
)
return required_text(record, 'assetId', index), digest, identity, record['status']
if len(sys.argv) != 2:
stop('usage: validate_asset_register.py register.json')
source = Path(sys.argv[1])
if not source.is_file():
raise SystemExit('asset register file is missing')
data = json.loads(source.read_text(encoding='utf-8'))
records = data.get('assets') if isinstance(data, dict) else None
if not isinstance(records, list) or not records:
raise SystemExit('assets array is missing')
validated = [validate(record, index) for index, record in enumerate(records)]
asset_ids = [item[0] for item in validated]
if len(asset_ids) != len(set(asset_ids)):
raise SystemExit('assetId is not unique')
digest_identities = defaultdict(set)
current_by_identity = defaultdict(set)
for asset_id, digest, identity, status in validated:
digest_identities[digest].add(identity)
if status == 'current':
current_by_identity[identity].add(digest)
conflicting_digests = [digest for digest, identities in digest_identities.items() if len(identities) > 1]
if conflicting_digests:
raise SystemExit('one artifact digest maps to multiple identities')
conflicting_current = [identity for identity, digests in current_by_identity.items() if len(digests) > 1]
if conflicting_current:
raise SystemExit('one identity has multiple current candidates')
print(json.dumps({'assets': len(records), 'status': 'pass'}))让台账直接服务加固接入和发布判断
接入流程从选择应用身份和渠道开始,再领取唯一 current 候选。系统验证摘要、签名、variant、来源证明和责任人完整后,才能建立处理记录。处理完成生成新输出摘要,测试证据和限制继续绑定输出候选。批准人只接受同一候选,发布负责人也只能从已接受候选中选择,避免邮件附件绕过台账。
看板可以展示各应用当前候选、加固状态、测试阻塞、签名待办和渠道状态,但每个数字都能下钻到候选与回执。没有真实来源、排名、客户或性能数据时不填推测值。资产完整率也不等于安全强度,它只说明身份和责任字段是否具备,防护效果仍由候选级验证决定。
准备企业接入时,可先整理平台应用标识、variant、签名身份、渠道、候选摘要、来源证明、配置、设备矩阵和责任角色,再参考本站的软件加固 SOW 交付边界文章定义验收。需要进一步梳理资产与接入计划,可携带脱敏台账通过御盾中央平台提交申请。任何实际能力、兼容和发布结论都应以项目回执为准。
| 门禁 | 必需字段 | 失败状态 | 解除证据 |
|---|---|---|---|
| 身份门禁 | 平台应用与 variant | blocked-identity | 制品解析回执 |
| 签名门禁 | 公开签名身份 | blocked-signing | 验签回执 |
| 来源门禁 | 构建材料与证明 | blocked-origin | 可信 provenance |
| 处理门禁 | 配置与输入摘要 | blocked-config | 批准配置 |
| 验收门禁 | 设备范围和限制 | under-test | 候选级回执 |
| 发布门禁 | 渠道和批准人 | awaiting-release | 真实渠道记录 |
事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| Android build type、product flavor、source set、applicationId 和签名配置会组合成不同 variant。 | Android build variants 描述构建变体、source set、应用标识和签名配置的组合。 | 同一代码仓库不代表所有 variant 具有相同 SDK、资源、证书和运行行为。 |
| Android 发布需要区分应用签名密钥、上传密钥、证书与 Play App Signing 责任。 | Sign your Android app 说明 Android 应用签名和托管签名职责。 | 官方文档不能确认某个实际候选使用正确生产证书,仍需最终候选验签。 |
| AAB 由 base、feature、配置与资产模块组成,设备安装的是派生 APK 集。 | Android App Bundle format 描述 bundle 模块与派生安装制品的关系。 | AAB 本身不是设备直接安装的最终 APK,其摘要不能替代设备 APK 集身份。 |
| Apple 平台通过代码签名、证书与运行验证建立 App 身份。 | Apple app code signing process 说明 Apple 平台应用签名与验证过程。 | Apple 签名链与 Android APK 签名方案不同,不能混为同一证据模型。 |
| 构建证明可以把产物主体与构建者、构建类型、外部参数和材料关联。 | SLSA Provenance v1.1 定义 provenance 模型及主要字段语义。 | provenance 只能证明记录的构建过程,不能单独证明运行时安全或加固效果。 |
| 供应链声明应把产物摘要与有类型的声明负载绑定。 | in-toto Attestation Statement v1 定义 subject、predicateType 与 predicate 的结构。 | 声明格式不保证内容真实,仍需可信签名者、门禁与候选摘要复算。 |
| 企业加固资产主键应由平台、应用标识、variant、签名身份、渠道和候选摘要组成。 | 工程判断:这些字段能区分实际发布身份和二进制,产品名与版本号不能。 | 具体主键可按组织增加区域或 tenant,但新增字段必须有稳定、可核验来源。 |
| 同一身份可以保留多个历史候选,但同一渠道只能有一个明确 current 候选。 | 工程判断:不可变历史与唯一当前指针能避免覆盖、歧义和错误发布。 | 灰度并行需要独立轨道与生效条件,不能简单删除历史候选或强行合并。 |
工程常见问题
企业 App 资产台账为什么不能用产品名加版本号作为唯一键?
同一产品和版本可能有多个平台、variant、渠道、签名与文件。应使用应用标识、签名、渠道和文件摘要识别实际候选,产品名只作展示。
同一 applicationId 可以同时存在多个加固候选吗?
可以保留多个历史或测试候选,但同一 variant、签名和渠道的 current 指针必须唯一。灰度并行应使用独立轨道与生效条件表达。
上传 AAB 的 SHA256 能否代表设备最终安装包?
不能。AAB 会派生设备相关 APK 集,应分开记录 bundle 候选、派生关系和设备实际安装身份,并让测试证据绑定具体安装集合。
台账需要保存 Android 或 iOS 的生产签名私钥吗?
不需要,也不应保存。记录公开证书或团队身份、密钥责任人、签名位置和验签回执,私钥、口令和渠道令牌留在专用控制边界。
候选经过加固和签名后是否继续使用原摘要?
不能。任何会改变文件字节的处理和签名都会产生新摘要,应新建输出候选,并通过处理关系关联输入、配置和签名前后状态。
向御盾中央平台提交企业接入计划前准备什么?
准备平台、应用标识、variant、签名公开身份、渠道、候选摘要、来源证明、处理配置、设备矩阵、当前状态和业务、构建、测试、签名及发布责任人。