先看结论与判断条件

  • 产品名称只用于人类识别,资产主键应由平台、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、配置或工具版本变化,都生成新候选与新处理记录。

Android variant 资产字段
字段来源核对动作冲突处置
applicationId构建与制品精确比较停止接入
build type构建任务绑定任务记录重建候选
product flavorGradle 配置展开完整维度禁止省略
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 唯一。

校验通过不能证明记录内容真实,仍需从制品、签名和渠道回执独立核对。实际落库应使用唯一索引和事务保证并发写入,不只依赖离线脚本。若发现冲突,先冻结相关接入和发布动作,确认哪条记录绑定真实候选,再通过状态变更保留错误记录的审计轨迹。

校验企业 App 加固资产台账的身份冲突
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 交付边界文章定义验收。需要进一步梳理资产与接入计划,可携带脱敏台账通过御盾中央平台提交申请。任何实际能力、兼容和发布结论都应以项目回执为准。

资产台账驱动的接入门禁
门禁必需字段失败状态解除证据
身份门禁平台应用与 variantblocked-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、签名公开身份、渠道、候选摘要、来源证明、处理配置、设备矩阵、当前状态和业务、构建、测试、签名及发布责任人。

想用自己的 App 验证?

提交候选包、目标系统和关键业务路径,申请御盾 PoC 与兼容性评估。

继续阅读: 御盾 VMP 接入前准备清单