先看结论与判断条件
- 配置基线记录的是某个候选如何生成及适用哪些边界,不是“强度档位”的截图;每个字段都要有稳定名称、类型、所有者和变更理由。
- 发布变体必须包含 build type、product flavor、applicationId、版本、ABI、minSdk、targetSdk 与签名责任,因为同仓库不同 variant 不是同一发布对象。
- 工具二进制、规则架构、输入应用、依赖锁、保留规则和 Native 规则都应保存版本或 SHA-256,只有可读名称无法发现内容漂移。
- 保护目标、排除项和运行策略要分别记录;每个排除项必须有原因、owner、风险、到期或复审条件,禁止永久无主例外。
- 基线不得包含私钥、keystore 口令、API token、客户数据或内部密钥材料,只保存允许公开或受控审计的证书摘要和责任引用。
- 规范化配置摘要必须进入候选来源证明、验收报告与发布批准;摘要一致支持可追溯,不代表具体防护强度或运行兼容通过。
配置基线要回答如何生成,而不是展示选中了什么
软件加固控制台通常有保护模式、规则文件、排除项、运行策略和签名选项。截图只能说明某个时刻界面显示了什么,无法稳定表达默认值、隐藏参数、规则正文、工具版本和输入产物。真正的配置基线应是一份结构化、可规范化、可计算摘要的记录,能够让审核者解释某个候选使用了哪些输入与边界。
基线的目标有三个:复现同一生成条件、比较两个候选的配置差异、让验收与发布证据挂到正确配置。它不是把敏感实现全部公开,也不是保存一份“最强参数模板”。需要保密的规则正文可以存放在受控仓库,基线只记录内容摘要、版本和责任引用,既能核对漂移又不泄露密钥或内部细节。
NIST SP 800-218 SSDF 要求安全发布保留来源、构建、验证和变更证据,并关注供应链风险。配置基线是其中的项目级证据之一,但 SSDF 是组织实践框架,不定义御盾或任何产品功能。本文没有具体候选与验证回执,因此只定义字段和门禁,不声称某组配置具备更高防护强度。
| 类别 | 需要回答 | 合格记录 | 不足记录 |
|---|---|---|---|
| 身份 | 哪个工具和输入 | 版本、摘要与来源 | 任务名称 |
| 范围 | 保护和排除什么 | 对象、模式、原因与 owner | 全量保护 |
| 行为 | 运行策略如何响应 | 触发、处置与边界 | 开启防护 |
| 责任 | 谁审批和发布 | 角色、身份、时间与范围 | 已确认 |
| 证据 | 候选怎样绑定配置 | 规范化摘要与声明 | 控制台截图 |
发布变体和应用身份必须完整固定
Android build variants 说明 build type、product flavor、source set、applicationId 和签名配置会组合成不同变体。同一个仓库的 release、staging、不同渠道或地区 flavor 可能包含不同 SDK、资源、Manifest、证书和运行行为。配置基线必须指定准确 variant,不能只写“Android 正式版”。
变体字段至少包括 buildType、flavors、applicationId、versionCode、versionName、minSdk、targetSdk、compileSdk、abiFilters 和输出格式。iOS 或其他平台要采用等价的目标、bundle identity、构建配置、架构和最低系统字段。跨平台字段可以共享 schema,但不能用 Android 的概念强行填充 Apple 产物。
输入应用文件还要计算 SHA-256,并记录它是未签名、中间签名还是最终发布输入。加固前产物变化会使全部旧配置证据失去直接适用性,即使 variant 名称没有变化。源码提交和构建任务可辅助追溯,但只有输入文件摘要能够确认处理的是哪个字节对象。
| 字段 | 为什么需要 | 变化影响 | 核对来源 |
|---|---|---|---|
| buildType/flavors | 定位真实配置组合 | SDK、资源和规则变化 | 构建系统 |
| applicationId/version | 定位发布序列 | 安装、升级与渠道变化 | 最终 Manifest |
| minSdk/targetSdk | 界定平台行为 | API 与兼容边界变化 | 构建产物 |
| abiFilters | 界定 Native 范围 | 打包和设备矩阵变化 | 构建配置与 APK |
| inputSha256 | 固定加固输入字节 | 全部候选证据需重建 | 实际输入文件 |
工具、规则架构和依赖材料都要有不可变身份
工具名称和营销版本不足以复现配置。基线应记录 toolName、toolVersion、binarySha256、ruleSchemaVersion、runnerImageSha256 和执行模式。若工具由托管服务执行,至少保存服务版本、任务类型、受控配置导出和生成回执。相同显示版本下的二进制变化仍应触发差异审阅。
输入材料还包括应用产物、依赖锁、映射文件、保留规则、Native 规则、资源配置和相关策略。每个材料记录 logicalName、sha256、sourceReference 与 sensitivityClass。对规则正文只保存摘要不能支持完全复现时,受控仓库还要保存不可变版本;公开交接只展示摘要和授权引用。
SLSA Provenance v1.1 把构建证明与产物主体、构建者、构建类型、外部参数和依赖材料绑定。配置基线可以作为 external parameters 或专门材料被来源证明引用。provenance 只能说明记录的构建过程,不能单独证明候选运行兼容或安全效果。
| 对象 | 身份字段 | 存储位置 | 漂移处置 |
|---|---|---|---|
| 加固工具 | 版本与 binarySha256 | 工具登记表 | 重建候选与复测 |
| 规则架构 | schemaVersion | 配置仓库 | 迁移并审阅字段 |
| 依赖锁 | lockSha256 | 构建证据库 | 重新计算影响 |
| 保留与 Native 规则 | 内容摘要与版本 | 受控规则仓库 | 差异审阅和回归 |
| 执行环境 | runner image 或服务版本 | 来源证明 | 确认可复现边界 |
保护目标要按对象、模式和依赖关系表达
“开启代码保护”没有说明哪些包、类、方法、Native 库或资源被处理,也没有说明采用何种模式。基线应把 targets 写成稳定对象选择器,记录 objectType、selector、mode、owner、rationale 和 requiredTests。选择器必须能在候选生成前解析成明确清单,并保存解析结果摘要。
目标范围要与业务资产和兼容边界对应。支付、授权、算法或协议代码可能有不同保护需求,反射框架、序列化模型和第三方入口又需要兼容策略。工程判断应记录为什么某个对象进入某种模式,而不是把所有内容统一提高档位。更大范围不自动代表更强或更合适,仍要接受启动、体积、性能和运行回归。
OWASP MASVS-RESILIENCE 把抗篡改与抗逆向视为移动端纵深防御控制。它可以帮助定义控制目标,却不证明某个配置达到任何强度,也不能替代服务端授权、密钥管理和完整发布链。基线中的 resilienceControl 只记录声明与测试路由,不应写成绝对阻断承诺。
| 字段 | 示例对象 | 决策用途 | 禁止写法 |
|---|---|---|---|
| objectType | package、class、method、native-lib | 选择正确处理器 | 核心代码 |
| selector | 受控命名或清单引用 | 解析实际范围 | 全部 |
| mode | 已定义配置模式 ID | 固定处理行为 | 最高强度 |
| rationale | 业务资产与威胁边界 | 支持审批 | 安全需要 |
| requiredTests | 启动、反射、JNI 或性能 | 生成回归路由 | 测试一下 |
排除项和保留规则必须有责任人与复审条件
排除项经常为修复一次崩溃临时加入,随后永久留在配置中。基线必须把 exclusions 与 targets 分开,每项记录 selector、excludedModes、reason、owner、riskAcceptedBy、createdAt、reviewAt 或 expiresAt、linkedIssue 和 requiredRegression。没有 owner 或理由的排除项应阻断批准。
排除范围要最小化并能解释实际失败条件。一个反射入口需要保留类名,不代表整个 SDK 或应用包都应跳过保护;一个 Native 库出现兼容问题,也不代表所有 SO 使用相同例外。配置审查应比较解析后的对象数量与上版变化,出现意外扩大时要求重新说明。
到期并不意味着自动删除规则,而是触发复审。复审要使用当前应用、依赖、工具和候选重新验证失败是否仍存在,决定缩小、保留或移除。旧问题单已关闭不等于运行问题消失,必须有对应设备回执;同样,删除排除项后静态构建成功也不能代替真实路径回归。
| 字段 | 作用 | 缺失风险 | 门禁 |
|---|---|---|---|
| selector/modes | 界定例外范围 | 整包意外排除 | 解析清单差异 |
| reason/linkedIssue | 记录失败依据 | 无法复核历史 | 证据路径存在 |
| owner/riskAcceptedBy | 确定技术与商业责任 | 永久无主例外 | 身份与权限有效 |
| reviewAt/expiresAt | 触发定期复审 | 历史债务沉积 | 到期阻断新发布 |
| requiredRegression | 关闭兼容风险 | 只看构建结果 | 真实路径回执 |
R8、保留规则和加固配置要分层记录
Enable app optimization with R8 说明 R8 负责代码与资源缩减、优化和名称混淆。R8 与软件加固可能都改变代码形态,但职责、输入与输出不同。基线应记录 R8 是否启用、规则摘要、mapping 摘要、资源缩减状态和流水线顺序,再单独记录加固规则,避免把两层配置混成一个“混淆开关”。
消费者 keep rules、应用规则和临时诊断规则的来源要区分。升级 SDK 可能改变 consumer rules,进而改变加固输入;调试时添加的 keep all 若残留,也可能让候选看似兼容却显著改变保护与优化范围。每层规则保存内容摘要、优先级、来源和解析输出,候选变更后重新生成 mapping 与清单。
R8 编译优化不等于 VMP,也不证明抗动态分析能力。反过来,加固处理也不能修复错误 keep rules、缺失反射入口或资源裁剪问题。基线的 pipelineOrder 用于定位责任:先确认未加固 release 变体的优化结果,再确认加固候选在同一输入和路径上的变化。
| 层 | 记录内容 | 主要责任 | 证据 |
|---|---|---|---|
| 编译器 | 版本与编译标志 | 源码到目标代码 | 构建来源 |
| R8 | 启用状态、规则、mapping | 缩减、优化与名称混淆 | 规则与输出摘要 |
| 资源处理 | shrink 状态与保留配置 | 资源裁剪 | 资源清单差异 |
| 加固 | 目标、模式、排除和运行策略 | 声明的保护变换 | 规范化配置摘要 |
| 签名 | 责任与证书摘要 | 发布身份 | 签名验证回执 |
审批、密钥边界和变更记录同样属于基线
配置完成后需要技术 owner、兼容测试、安全审核和发布责任按各自范围批准。approval 记录包含 role、identity、approvedConfigSha256、scope、decision、approvedAt 与 evidenceReference。审批人不能只签任务号,因为任务可能重新执行;其决定必须引用规范化配置摘要和预期输入摘要。
基线绝不能保存 password、token、privateKey、keystore 文件、恢复码、客户数据或内部密钥材料。签名部分只记录 expectedCertificateSha256、signingResponsibility 和受控密钥系统引用。配置生成器应扫描常见敏感字段并失败关闭,输出用于审计的 JSON 也要按 sensitivityClass 控制访问。
变更记录要说明 fromConfigSha256、toConfigSha256、changedFields、reason、owner、影响门禁和新候选。控制台默认值或工具升级导致的隐式变化也属于变更,不能因为没人手工编辑就忽略。审批只对明确摘要有效,配置或输入任一变化后,旧批准应进入需评估状态。
| 对象 | 允许记录 | 禁止记录 | 变化处置 |
|---|---|---|---|
| 审批 | 角色、身份、摘要、范围和时间 | 口头同意 | 重新评估受影响范围 |
| 证书 | 公开 SHA-256 与责任 | 私钥 | 签名身份复核 |
| 密钥系统 | 受控引用和回执 ID | token 与口令 | 权限审计 |
| 规则 | 内容摘要和仓库版本 | 不必要的公开正文 | 差异审查 |
| 诊断数据 | 脱敏回执引用 | 客户数据和真实凭据 | 隔离与最小保留 |
把规范化摘要与候选证明绑定并用代码校验
in-toto Attestation Statement v1 可以把产物 subject 摘要与有类型 predicate 绑定。配置基线可作为 hardening-configuration predicate,其中包含 configSha256、inputArtifactSha256、variant、tool identity 和审批引用。声明格式不保证内容真实,仍要核对生成者、签名、schema 与候选文件。
下方脚本读取配置 JSON 和候选文件,拒绝敏感字段、未知顶层字段、无摘要材料、无 owner 排除项和不完整变体;将有序集合排序后生成规范化 JSON 和 configSha256,再输出以候选 SHA-256 为 subject 的声明。它不修改候选、不接触密钥,也不会声称配置强度或运行兼容。
规范化摘要相同只表示脚本可见字段相同;若工具存在未导出的默认值、远端策略或执行环境漂移,仍需来源证明补足。最终证据包应保存 schema、脚本摘要、原始配置、规范化摘要、候选、构建来源、验证回执和批准。准备实际配置治理方案时,可通过御盾中央平台提交申请。
| 证据 | 主键 | 支持结论 | 不能支持 |
|---|---|---|---|
| 原始配置 | sourceConfigSha256 | 输入记录未变 | 字段完整 |
| 规范化配置 | configSha256 | 可比较的语义快照 | 隐藏默认值相同 |
| 候选文件 | artifactSha256 | 实际产物身份 | 运行兼容 |
| attestation | subject 与 predicateType | 配置和候选关联 | 声明内容真实 |
| 验收与批准 | 同一 config 和 artifact | 覆盖范围内的决定 | 未知能力成立 |
- 配置 schema、平台、variant、工具和全部输入材料具有稳定身份
- targets 与 exclusions 分离,均含 owner、理由和 requiredTests
- 保留规则、Native 规则、依赖锁和输入应用以 SHA-256 记录
- 审批引用规范化 configSha256 与明确范围,不只引用任务号
- 配置不包含私钥、口令、token、客户数据或恢复材料
- 配置摘要与实际候选 subject、来源证明和验证回执绑定
#!/usr/bin/env python3
import copy
import hashlib
import json
import re
import sys
from pathlib import Path
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
FORBIDDEN_KEYS = {
"password", "passwd", "token", "apiKey", "secret",
"privateKey", "keystorePassword", "keyPassword", "recoveryCode",
}
ALLOWED_TOP_LEVEL = {
"schemaVersion", "platform", "variant", "tool", "materials",
"targets", "exclusions", "runtimePolicies", "signing", "approvals",
}
SORTED_LIST_FIELDS = {
"abiFilters", "flavors", "requiredTests", "excludedModes",
"runtimePolicies", "resolvedObjects",
}
def fail(message, code=2):
print(message, file=sys.stderr)
raise SystemExit(code)
def sha256_bytes(value):
return hashlib.sha256(value).hexdigest()
def sha256_file(path):
digest = hashlib.sha256()
with path.open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def load_config(path):
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
fail("cannot read configuration: " + str(exc))
if not isinstance(value, dict):
fail("configuration must be a JSON object")
return value
def require_text(record, field, context):
value = record.get(field)
if not isinstance(value, str) or not value.strip():
fail(context + " is missing " + field)
return value.strip()
def reject_secrets(value, path="$"):
if isinstance(value, dict):
for key, child in value.items():
if key in FORBIDDEN_KEYS:
fail("forbidden sensitive field: " + path + "." + key, 3)
reject_secrets(child, path + "." + key)
elif isinstance(value, list):
for index, child in enumerate(value):
reject_secrets(child, path + "[" + str(index) + "]")
def validate_sha(value, context):
if not isinstance(value, str) or not SHA256_RE.fullmatch(value.lower()):
fail(context + " must be a SHA-256 digest")
return value.lower()
def validate_variant(variant):
if not isinstance(variant, dict):
fail("variant must be an object")
for field in ("buildType", "applicationId", "versionCode", "versionName", "minSdk", "targetSdk"):
require_text(variant, field, "variant")
for field in ("flavors", "abiFilters"):
if not isinstance(variant.get(field), list):
fail("variant." + field + " must be an array")
def validate_tool(tool):
if not isinstance(tool, dict):
fail("tool must be an object")
for field in ("name", "version", "ruleSchemaVersion"):
require_text(tool, field, "tool")
validate_sha(tool.get("binarySha256"), "tool.binarySha256")
def validate_materials(materials):
if not isinstance(materials, list) or not materials:
fail("materials must be a non-empty array")
names = set()
for record in materials:
if not isinstance(record, dict):
fail("every material must be an object")
name = require_text(record, "logicalName", "material")
if name in names:
fail("duplicate material logicalName: " + name)
names.add(name)
validate_sha(record.get("sha256"), name + ".sha256")
require_text(record, "sourceReference", name)
require_text(record, "sensitivityClass", name)
def validate_targets(targets):
if not isinstance(targets, list) or not targets:
fail("targets must be a non-empty array")
for index, record in enumerate(targets):
context = "target[" + str(index) + "]"
if not isinstance(record, dict):
fail(context + " must be an object")
for field in ("objectType", "selector", "mode", "owner", "rationale"):
require_text(record, field, context)
if not isinstance(record.get("requiredTests"), list) or not record["requiredTests"]:
fail(context + " requiredTests must be a non-empty array")
def validate_exclusions(exclusions):
if not isinstance(exclusions, list):
fail("exclusions must be an array")
for index, record in enumerate(exclusions):
context = "exclusion[" + str(index) + "]"
if not isinstance(record, dict):
fail(context + " must be an object")
for field in ("selector", "reason", "owner", "riskAcceptedBy", "linkedIssue", "reviewAt"):
require_text(record, field, context)
if not isinstance(record.get("excludedModes"), list) or not record["excludedModes"]:
fail(context + " excludedModes must be a non-empty array")
def validate_approvals(approvals):
if not isinstance(approvals, list) or not approvals:
fail("approvals must be a non-empty array")
for index, record in enumerate(approvals):
context = "approval[" + str(index) + "]"
if not isinstance(record, dict):
fail(context + " must be an object")
for field in ("role", "identity", "scope", "decision", "approvedAt", "evidenceReference"):
require_text(record, field, context)
def normalize(value, parent_key=""):
if isinstance(value, dict):
return {key: normalize(value[key], key) for key in sorted(value)}
if isinstance(value, list):
normalized = [normalize(item) for item in value]
if parent_key in SORTED_LIST_FIELDS and all(isinstance(item, str) for item in normalized):
return sorted(set(normalized))
if all(isinstance(item, dict) and "logicalName" in item for item in normalized):
return sorted(normalized, key=lambda item: item["logicalName"])
return normalized
return value
def main():
if len(sys.argv) != 3:
print("Usage: baseline_hardening_config.py CONFIG_JSON CANDIDATE_FILE", file=sys.stderr)
raise SystemExit(2)
config_path = Path(sys.argv[1])
candidate_path = Path(sys.argv[2])
if not config_path.is_file() or not candidate_path.is_file():
print("configuration and candidate files must exist", file=sys.stderr)
raise SystemExit(2)
config = load_config(config_path)
unknown = sorted(set(config) - ALLOWED_TOP_LEVEL)
if unknown:
fail("unknown top-level fields: " + ", ".join(unknown))
reject_secrets(config)
require_text(config, "schemaVersion", "configuration")
require_text(config, "platform", "configuration")
validate_variant(config.get("variant"))
validate_tool(config.get("tool"))
validate_materials(config.get("materials"))
validate_targets(config.get("targets"))
validate_exclusions(config.get("exclusions"))
validate_approvals(config.get("approvals"))
if not isinstance(config.get("runtimePolicies"), list):
fail("runtimePolicies must be an array")
signing = config.get("signing")
if not isinstance(signing, dict):
fail("signing must be an object")
validate_sha(signing.get("expectedCertificateSha256"), "signing.expectedCertificateSha256")
require_text(signing, "responsibility", "signing")
normalized = normalize(copy.deepcopy(config))
canonical = json.dumps(normalized, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
config_sha = sha256_bytes(canonical)
candidate_sha = sha256_file(candidate_path)
input_material = next((item for item in normalized["materials"] if item["logicalName"] == "input-application"), None)
if input_material is None:
fail("materials must include input-application")
statement = {
"_type": "https://in-toto.io/Statement/v1",
"subject": [{"name": candidate_path.name, "digest": {"sha256": candidate_sha}}],
"predicateType": "https://example.invalid/hardening-configuration/v1",
"predicate": {
"configSha256": config_sha,
"sourceConfigSha256": sha256_file(config_path),
"inputArtifactSha256": input_material["sha256"],
"schemaVersion": normalized["schemaVersion"],
"platform": normalized["platform"],
"variant": normalized["variant"],
"tool": normalized["tool"],
"boundary": "Configuration identity does not prove protection strength or runtime compatibility.",
},
}
print(json.dumps(statement, ensure_ascii=False, indent=2, sort_keys=True))
if __name__ == "__main__":
main()事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 安全发布应保留来源、构建、验证和变更证据并关注供应链风险。 | NIST SP 800-218 SSDF 给出组织级安全软件开发实践。 | SSDF 不定义某个 App 加固产品的具体功能或配置强度。 |
| 构建证明可以绑定产物主体、构建者、构建类型、外部参数和依赖材料。 | SLSA Provenance v1.1 定义来源证明的数据模型。 | provenance 只能支持记录的构建过程,不能单独证明运行时安全。 |
| 供应链声明可以把产物摘要与有类型的配置负载关联。 | in-toto Attestation Statement v1 定义 subject 和 predicateType。 | 声明格式不保证内容真实,仍需可信生成者、签名与门禁核验。 |
| R8 负责代码和资源缩减、优化与名称混淆,发布构建需保留规则和输出。 | Enable app optimization with R8 说明 Android 应用优化流程。 | R8 编译优化不等于 VMP,也不证明抗动态分析能力。 |
| build type、product flavor、source set、applicationId 和签名配置会组成不同变体。 | Android build variants 说明 Android 构建变体模型。 | 同一仓库不代表所有 variant 拥有相同 SDK、资源、证书和行为。 |
| 移动端抗篡改与抗逆向属于纵深防御控制。 | OWASP MASVS-RESILIENCE 描述移动端韧性控制域。 | 控制目录不证明某个候选或配置达到任何防护强度。 |
| 保护目标、排除项、运行策略、材料和审批应进入规范化配置摘要。 | 工程判断:这些字段共同决定候选生成边界,缺失时无法解释配置漂移。 | 字段与摘要完整只支持可追溯,不证明实际运行与保护效果。 |
| 排除项必须有 owner、理由、风险接受者和复审条件。 | 工程判断:无主永久例外会扩大未受控范围并使后续变更无法审计。 | 复审到期不代表应自动删除,仍需当前候选与真实路径验证。 |
工程常见问题
保存软件加固控制台截图能否作为配置基线?
不能单独作为基线。截图难以表达默认值、隐藏参数、规则内容和工具身份;应导出结构化配置,规范化生成摘要,并保存截图作为辅助证据。
为什么同一仓库不同 product flavor 要分别记录?
flavor 可能改变 SDK、资源、Manifest、applicationId、证书和规则。它们是不同发布对象,不能共享一个含糊的“release 配置”身份。
配置基线是否应该保存 keystore 和密码以便复现?
不应该。基线只记录证书摘要、签名责任和受控密钥系统引用,私钥、keystore 口令、token 与恢复材料应留在专门密钥系统。
configSha256 相同是否说明候选一定相同?
不说明。输入应用、工具二进制、依赖、执行环境或隐藏默认值仍可能变化,因此还要比较材料摘要、候选 SHA-256 和构建来源。
排除项到期后是否应该自动删除?
到期应触发复审,不宜直接删除。需要在当前工具、依赖和候选上重现原失败并执行回归,再决定缩小、保留或移除。
建立软件加固配置基线要准备哪些材料?
准备发布变体、输入应用、工具与环境身份、依赖锁、保护目标、排除项、规则摘要、运行策略、签名责任、审批角色和验证计划,再通过御盾中央平台提交申请。