先看结论与判断条件
- PoC 通过必须绑定唯一候选摘要、签名身份、构建输入和配置,测试了哪个文件比演示了什么功能更先确认。
- 每条标准都要包含对象、前置条件、操作、预期结果、阻断级别、证据类型和复测条件,避免使用“基本可用”“兼容良好”等模糊表述。
- 核心业务路径与异常、取消、升级、后台、重启等失败路径都要覆盖,加固后能够启动只是入口条件,不是验收结论。
- 兼容矩阵必须来自真实用户范围与最低支持声明,按设备、系统、ABI、形态和关键依赖组合记录,不允许用单机结果代表全量。
- 安全检查验证的是已声明控制及其边界,不能把抗篡改或抗逆向控制写成绝对阻断,也不能替代服务端授权和发布链。
- 最终报告必须区分 passed、failed、blocked、not-run 和 accepted-limitation;缺证或候选变化会触发复测,不能折算为通过。
先把 PoC 从产品演示改成可判定的验收合同
软件加固 PoC 最常见的问题不是测试数量少,而是测试前没有定义什么算通过。演示人员完成安装、启动和一个核心页面,只能说明当时那条路径能够运行。买方真正需要的是一组事先冻结、执行后只有明确状态的条款,用来判断同一候选是否满足业务兼容、安全控制、稳定性和交付证据要求。
每条验收标准应写清 criterionId、对象、业务风险、前置条件、输入、操作步骤、预期结果、阻断级别、证据格式、执行环境和复测触发条件。描述不能使用“基本正常”“无明显影响”“性能可接受”这类无法复核的词。若业务方尚未给出阈值,就把该项标为 blocked,等待责任人确认,而不是由供应方临时替业务做决定。
PoC 结论还必须区分事实与工程判断。事实是某个摘要的候选在某台设备、某个时间窗执行了某条路径并产生了回执;工程判断是这些覆盖是否足以支持采购或上线决策。本文提供验收条款模板,没有真实候选与回执,因此不声称任何御盾或其他产品已经通过 PoC。
| 字段 | 必须回答 | 不合格写法 | 合格证据 |
|---|---|---|---|
| criterionId | 哪一条唯一规则 | 兼容性测试 | 稳定编号和版本 |
| scope | 测试哪个对象和路径 | 测试主要功能 | 入口、输入与结束状态 |
| expected | 什么结果算通过 | 运行正常 | 可观察状态与阈值 |
| severity | 失败是否阻断 | 后续再看 | blocker、major 或 advisory |
| evidence | 怎样复核结果 | 现场看过 | 候选、环境、日志与回执 |
候选身份和构建来源是所有结论的主键
验收开始前先计算 APK、AAB、IPA 或 Native 产物的 SHA-256,并记录包标识、版本号、签名证书摘要、目标 ABI、加固配置摘要和生成时间。后续每条证据都引用这个 candidateSha256。文件名、下载链接或控制台任务号可以辅助定位,但不能替代不可变摘要,因为同名文件可能被覆盖,任务也可能重新执行。
SLSA Provenance v1.1 把构建证明与产物主体、构建者、构建类型、外部参数和依赖材料关联。对于 PoC,最低要求是能回答候选来自什么输入、采用什么受控配置以及谁生成。provenance 只能证明记录的构建过程,不能单独证明运行时安全;若声明主体摘要与现场文件不一致,应立即停止并重新确认候选。
in-toto Attestation Statement v1 提供把产物摘要与有类型声明负载绑定的结构。验收方可以借鉴 subject 与 predicateType 的关系,避免把兼容报告、扫描结果或审批单挂到错误文件上。声明格式本身不保证内容真实,还需核对签名者权限、生成工具、时间和证据来源,不能把一个格式正确的 JSON 当成可信结论。
| 身份项 | 采集时点 | 校验规则 | 不一致处置 |
|---|---|---|---|
| candidateSha256 | 收到文件立即计算 | 所有证据完全一致 | 停止验收 |
| 签名摘要 | 安装与发布前 | 符合约定身份 | 判定候选无效 |
| 包与版本 | 静态检查 | 与测试计划一致 | 更正计划或重建 |
| 加固配置摘要 | 生成候选时 | 变更可追溯 | 重新评估受影响项 |
| 构建来源 | 交付时 | 主体、参数和材料可关联 | 补证或标记 blocked |
核心路径要按业务结果和失败条件拆成独立条款
核心路径不是页面名称列表,而是从可复现输入到业务结果的完整链。例如登录路径应包含启动、凭据输入、网络响应、会话建立、受保护页面和退出,而支付、媒体、离线、推送或设备能力还要包含各自依赖。每条路径都要指定测试账号或公开安全数据、前置状态、允许的网络条件和结束状态。
成功路径之外,至少要覆盖权限拒绝、网络中断、超时、取消、后台切换、组件重建、进程重启、升级安装和无效输入。加固变换可能放大时序、反射、动态加载、JNI、序列化或资源查找的历史问题,只有失败路径才能暴露清理、重试和错误传播是否保持。测试计划应基于应用真实实现选择,不得凭文章虚构统一清单。
Android instrumented tests 适合验证依赖真实 Android 运行时、组件和系统 API 的语义。关键断言应尽量落在业务结果而非截图颜色,例如数据库状态、页面语义、返回码或可观察事件。单一 instrumented test 通过不能代表完整设备范围,且测试代码本身也要固定版本、输入和断言,避免候选与测试同时漂移。
| 路径层次 | 典型动作 | 预期结果 | 阻断信号 |
|---|---|---|---|
| 入口 | 安装、启动、深链或通知 | 进入正确业务状态 | 崩溃、白屏或错误路由 |
| 主流程 | 提交真实安全测试输入 | 业务状态符合基线 | 结果缺失或状态漂移 |
| 异常 | 拒权、断网、超时、无效输入 | 明确错误并可恢复 | 卡死、泄漏或错误成功 |
| 生命周期 | 后台、重建、重启 | 状态按设计保存或清理 | 恢复错误或重复执行 |
| 升级 | 从已发布基线覆盖安装 | 数据和身份保持约定 | 丢失、迁移失败或签名冲突 |
兼容矩阵必须从真实发布范围反推
兼容标准要先冻结支持范围:最低与目标系统版本、主要设备型号、CPU ABI、屏幕形态、语言方向、关键厂商系统、网络模式,以及 WebView、图形、Native 库或第三方 SDK 等高风险依赖。矩阵不是型号越多越好,而是能够覆盖真实用户、最低承诺和已知风险组合。没有分布数据时要明确依据和限制。
Firebase Test Lab Android matrices 把设备、系统版本、方向与 locale 组合为矩阵执行,并说明单个执行失败会影响矩阵结果。PoC 报告应保留每个 cell 的独立状态,不能把九个通过和一个失败平均成百分比后宣布兼容。失败的阻断级别取决于该 cell 是否属于正式支持范围和路径风险,但判定规则必须在执行前冻结。
云真机适合扩大覆盖,仍不能完全替代现场硬件、企业网络、外设和特定厂商能力。矩阵应标记 executionProvider、deviceModel、osVersion、abi、orientation、locale 与关键依赖版本,并保留 unavailable 或 infrastructure-failed 状态。基础设施失败不等于产品失败,也绝不能改写成 passed,应修复环境后复测。
| 维度 | 示例字段 | 来源 | 缺失风险 |
|---|---|---|---|
| 设备 | model、manufacturer | 用户分布与支持声明 | 厂商差异未覆盖 |
| 系统 | API 与构建版本 | minSdk、targetSdk 和发布范围 | 边界版本遗漏 |
| 架构 | arm64-v8a 等 | 打包与 Native 依赖 | 特定 ABI 无法加载 |
| 形态 | 方向、尺寸、窗口模式 | 产品场景 | 重建与布局路径遗漏 |
| 环境 | locale、网络与关键依赖 | 真实业务条件 | 解析、资源或服务差异 |
安全控制要验证声明范围,不能承诺绝对阻断
安全验收应从 PoC 明确声明的控制开始,例如完整性检查、调试环境策略、关键代码保护范围、敏感常量处理、重打包响应或运行时风险信号。每项都要写明保护对象、威胁假设、触发条件、预期响应和可观察证据。没有声明或无法公开安全验证的能力,不应为了让表格完整而临时编造。
OWASP MASVS-RESILIENCE 将抗篡改与抗逆向放在移动端纵深防御中。它可以帮助组织测试问题,却不证明某个候选达到任何强度,也不能替代服务端授权、密钥管理和完整发布链。PoC 条款应避免“无法逆向”“百分之百阻断”之类不可证伪承诺,改为针对指定对象、工具假设和时间窗的可观察结果。
安全测试报告要区分 control-present、trigger-observed、response-observed 与 boundary-known。静态看到校验代码不等于运行时触发,运行时弹出提示也不等于业务密钥安全。对于可能形成攻击复现链的细节,公开报告只保留方法类别、候选身份和结论边界,敏感步骤进入受控附件,不在文章或公开代码中提供绕过方法。
| 条款部分 | 要写什么 | 可接受证据 | 禁止外推 |
|---|---|---|---|
| 保护对象 | 代码、资源或运行状态 | 配置与候选映射 | 所有资产均受保护 |
| 威胁假设 | 工具能力和权限边界 | 审核过的测试计划 | 覆盖未知全部攻击者 |
| 触发条件 | 何种变化或环境 | 受控执行回执 | 一次触发代表所有路径 |
| 响应 | 阻断、降级、告警或记录 | 状态与日志关联 | 响应等于根因消除 |
| 限制 | 未覆盖对象和条件 | 明确 accepted-limitation | 标准目录等于产品通过 |
性能与稳定性阈值必须在测试前由业务确认
加固 PoC 常把“体积增加可接受”“启动没有明显变慢”写进报告,但没有基线、测量方法和阈值就无法复核。每个性能指标应指定同一设备、同一版本输入、冷热状态、采样窗口、统计方式和业务阈值。文章不能替所有应用给出统一数字,因为启动、包体、内存、耗电和 Native 开销的业务权重不同。
稳定性验收要覆盖重复执行、长会话、前后台切换、低资源条件、异常恢复和进程退出,并把崩溃、ANR、失败率或资源趋势与候选摘要关联。一次长时间不崩溃只是一个样本,不能证明所有用户稳定。若测试环境存在网络抖动、云设备不可用或服务端故障,应标记 blocked 或 infrastructure-failed,修复后使用相同条件复测。
阈值应有 owner 和 rationale。业务负责人决定可接受上限,测试负责人固定方法,供应方提供候选和诊断材料,最终审批人只对证据覆盖范围负责。若阈值在看到结果后被放宽,必须记录变更理由、批准时间和影响项,再重新计算结论,不能悄悄修改表格让候选通过。
| 字段 | 作用 | 示例形式 | 失败处置 |
|---|---|---|---|
| baseline | 建立可比参照 | 未加固同版本同输入 | 基线缺失则 blocked |
| method | 固定测量方式 | 设备、状态、窗口、统计量 | 方法漂移则重测 |
| threshold | 给出判定边界 | 由业务批准的上限 | 越界按 severity 处理 |
| evidence | 支持复核 | 原始结果与摘要 | 只有结论则缺证 |
| retestTrigger | 控制结果漂移 | 候选、配置或依赖变化 | 触发后旧结论失效 |
证据必须与候选、条款和环境一一绑定
NIST SP 800-218 SSDF 要求在安全软件开发中保留来源、构建、验证和变更证据,并关注供应链风险。PoC 可以把这项原则落到 evidence record:criterionId、candidateSha256、environmentId、method、status、artifactPath、artifactSha256、executedAt 和 executor。缺少主键的截图或口头确认只能作为线索,不能关闭阻断项。
每个条款只允许 passed、failed、blocked、not-run 或 accepted-limitation 等受控状态。passed 表示预期结果和必需证据都满足;failed 表示执行完成但结果不符;blocked 表示前置条件或环境阻止执行;not-run 表示尚未安排;accepted-limitation 必须有明确责任人和商业决策。后面三类不能计入技术通过。
证据本身也要校验摘要和访问性。日志、视频、测试报告或设备回执的路径必须存在,并可计算 SHA-256;引用外部系统时保存不可变回执标识,而不是易变化的仪表盘截图。涉及用户、客户或凭据的数据应脱敏并限制访问,公开报告只展示足以复核条款的最小信息。
| 状态 | 技术含义 | 能否计入通过 | 下一步 |
|---|---|---|---|
| passed | 结果与证据满足冻结条款 | 可以 | 保留回执 |
| failed | 已执行但结果不符 | 不可以 | 修复并复测 |
| blocked | 前置或环境不满足 | 不可以 | 解除阻塞后复测 |
| not-run | 尚未执行 | 不可以 | 安排覆盖 |
| accepted-limitation | 商业上接受已知限制 | 不属于技术通过 | 记录责任人与范围 |
用机器门禁汇总结论,并把变更写成复测触发器
下方脚本读取 PoC 计划 JSON 和证据目录,验证 candidateSha256、条款唯一性、阻断级别、必需矩阵 cell、证据文件摘要和状态。它要求每个 blocker 都是 passed 且拥有可读证据,任何 failed、blocked、not-run、候选错配或矩阵缺口都会返回非零。accepted-limitation 只允许非 blocker 使用,并要求责任人与批准记录。
脚本的通过状态只说明输入文件满足这套结构化规则,不证明证据内容真实,也不替代人工复核视频、日志、签名、业务状态或安全测试。矩阵是否合理、阈值是否足够和控制是否覆盖真实威胁仍由项目责任人决定。代码不连接生产系统,不执行攻击,不包含密钥、客户数据或内部地址。
PoC 报告要列出复测触发器:候选字节、签名、加固配置、应用代码、NDK 或依赖、系统支持范围、关键第三方 SDK、测试脚本和阈值任一变化,都要重新评估受影响条款。最终交付应给出 passed、failed、blocked 和限制清单,而不是一个模糊总分;准备好候选与证据计划后,可通过御盾中央平台提交 PoC 申请。
| 变化 | 最小影响范围 | 旧证据状态 | 必须动作 |
|---|---|---|---|
| 候选摘要或签名 | 全部条款 | 失效 | 重新执行完整 PoC |
| 加固配置 | 受保护对象与运行路径 | 待评估 | 重跑关联条款 |
| 应用或依赖 | 变更模块及消费者 | 待评估 | 差异分析后复测 |
| 支持矩阵 | 新增 device、API、ABI | 原范围仍有效 | 补充新 cell |
| 测试方法或阈值 | 使用该方法的条款 | 不可直接比较 | 重建基线并复测 |
- 验收前冻结 candidateSha256、签名摘要、配置和测试计划版本
- 每条 blocker 都有明确预期、执行环境和可读证据,不接受口头确认
- requiredMatrixCells 来自正式支持范围与风险组合,不由通过结果反向删减
- failed、blocked、not-run 和 accepted-limitation 分开呈现,不折算为技术通过
- 证据路径限制在受控目录,摘要不一致立即阻断
- 候选、配置、应用、依赖、矩阵或阈值变化后重评并复测关联条款
#!/usr/bin/env python3
import hashlib
import json
import re
import sys
from pathlib import Path
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
ALLOWED_STATUS = {"passed", "failed", "blocked", "not-run", "accepted-limitation"}
ALLOWED_SEVERITY = {"blocker", "major", "advisory"}
def fail(message, code=2):
print(message, file=sys.stderr)
raise SystemExit(code)
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_json(path):
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
fail("cannot read PoC plan: " + str(exc))
if not isinstance(value, dict):
fail("PoC plan 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 validate_evidence(item, evidence_root, candidate_sha, criterion_id, findings):
if not isinstance(item, dict):
fail(criterion_id + " evidence must be an object")
if item.get("candidateSha256") != candidate_sha:
findings.append({"criterionId": criterion_id, "kind": "candidate-mismatch"})
relative = require_text(item, "artifactPath", criterion_id + " evidence")
expected_sha = require_text(item, "artifactSha256", criterion_id + " evidence").lower()
if not SHA256_RE.fullmatch(expected_sha):
fail(criterion_id + " evidence has invalid artifactSha256")
artifact = (evidence_root / relative).resolve()
try:
artifact.relative_to(evidence_root.resolve())
except ValueError:
fail(criterion_id + " evidence escapes evidence root")
if not artifact.is_file():
findings.append({"criterionId": criterion_id, "kind": "evidence-missing", "path": relative})
return
actual_sha = sha256_file(artifact)
if actual_sha != expected_sha:
findings.append({"criterionId": criterion_id, "kind": "evidence-hash-mismatch", "path": relative})
def validate_criterion(record, evidence_root, candidate_sha, seen_ids, matrix_cells, findings):
if not isinstance(record, dict):
fail("every criterion must be an object")
criterion_id = require_text(record, "criterionId", "criterion")
if criterion_id in seen_ids:
fail("duplicate criterionId: " + criterion_id)
seen_ids.add(criterion_id)
severity = require_text(record, "severity", criterion_id)
status = require_text(record, "status", criterion_id)
require_text(record, "scope", criterion_id)
require_text(record, "expected", criterion_id)
require_text(record, "retestTrigger", criterion_id)
if severity not in ALLOWED_SEVERITY:
fail(criterion_id + " has unsupported severity")
if status not in ALLOWED_STATUS:
fail(criterion_id + " has unsupported status")
cell = require_text(record, "matrixCell", criterion_id)
matrix_cells.add(cell)
evidence = record.get("evidence", [])
if not isinstance(evidence, list):
fail(criterion_id + " evidence must be an array")
for item in evidence:
validate_evidence(item, evidence_root, candidate_sha, criterion_id, findings)
if status == "passed" and not evidence:
findings.append({"criterionId": criterion_id, "kind": "passed-without-evidence"})
if severity == "blocker" and status != "passed":
findings.append({"criterionId": criterion_id, "kind": "blocker-not-passed", "status": status})
if status == "accepted-limitation":
if severity == "blocker":
findings.append({"criterionId": criterion_id, "kind": "blocker-cannot-be-accepted-limitation"})
require_text(record, "limitationOwner", criterion_id)
require_text(record, "approvalRecord", criterion_id)
def main():
if len(sys.argv) != 3:
print("Usage: validate_poc_acceptance.py POC_PLAN_JSON EVIDENCE_DIRECTORY", file=sys.stderr)
raise SystemExit(2)
plan_path = Path(sys.argv[1])
evidence_root = Path(sys.argv[2])
if not plan_path.is_file() or not evidence_root.is_dir():
print("plan file and evidence directory must exist", file=sys.stderr)
raise SystemExit(2)
plan = load_json(plan_path)
candidate_sha = require_text(plan, "candidateSha256", "plan").lower()
if not SHA256_RE.fullmatch(candidate_sha):
fail("candidateSha256 must be 64 lowercase hexadecimal characters")
criteria = plan.get("criteria")
required_cells = plan.get("requiredMatrixCells")
if not isinstance(criteria, list) or not criteria:
fail("criteria must be a non-empty array")
if not isinstance(required_cells, list) or not required_cells:
fail("requiredMatrixCells must be a non-empty array")
if any(not isinstance(cell, str) or not cell.strip() for cell in required_cells):
fail("requiredMatrixCells must contain non-empty strings")
findings = []
seen_ids = set()
observed_cells = set()
for record in criteria:
validate_criterion(record, evidence_root, candidate_sha, seen_ids, observed_cells, findings)
for cell in sorted(set(required_cells) - observed_cells):
findings.append({"criterionId": None, "kind": "required-matrix-cell-missing", "matrixCell": cell})
result = {
"status": "blocked" if findings else "acceptance-structure-passed",
"candidateSha256": candidate_sha,
"planSha256": sha256_file(plan_path),
"criterionCount": len(criteria),
"requiredMatrixCellCount": len(set(required_cells)),
"findings": findings,
"boundary": "Structure and hashes passed do not prove the evidence content or product capability.",
}
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
if findings:
raise SystemExit(20)
if __name__ == "__main__":
main()事实依据与适用边界
以下内容区分官方事实、本文工程判断和不能外推的范围,避免把设计建议写成未经验证的产品结论。
| 本文判断 | 事实或工程依据 | 适用限制 |
|---|---|---|
| 安全发布应保留来源、构建、验证与变更证据并关注供应链风险。 | NIST SP 800-218 SSDF 给出组织级安全软件开发实践。 | SSDF 不定义某个 App 加固产品的具体功能或 PoC 通过标准。 |
| 移动端抗篡改与抗逆向属于纵深防御控制。 | OWASP MASVS-RESILIENCE 描述移动端韧性控制域。 | 控制目录不证明候选达到任何强度,也不能替代服务端授权和发布链。 |
| 构建证明可绑定产物主体、构建者、构建类型、外部参数和依赖材料。 | SLSA Provenance v1.1 定义构建来源证明的数据模型。 | provenance 只能支持记录的构建过程,不能单独证明运行时安全。 |
| 供应链声明可把产物摘要与有类型的声明负载绑定。 | in-toto Attestation Statement v1 定义 subject 与 predicate 的声明结构。 | 格式正确不保证内容真实,仍需可信签名者与门禁核验。 |
| 依赖 Android 运行时、组件和系统 API 的语义应通过设备端测试验证。 | Android instrumented tests 说明设备端测试的适用范围。 | 单一设备通过不能代表完整 API、ABI 与厂商矩阵。 |
| Android 云测试矩阵由设备、系统、方向和 locale 等执行组合组成。 | Firebase Test Lab Android matrices 说明 Android 测试矩阵的配置与执行。 | 云真机范围仍需依据真实用户和最低支持声明选择。 |
| PoC 的每个阻断条款必须绑定唯一候选、执行环境、预期结果和可复核证据。 | 工程判断:缺少这些主键时,报告无法证明测试对象和结论属于同一候选。 | 字段完整只证明结构可审计,不证明证据内容真实或产品能力成立。 |
| 候选、配置、应用、依赖、支持矩阵、测试方法或阈值变化会触发受影响条款复测。 | 工程判断:这些输入变化会改变原证据支持结论的适用范围。 | 影响范围需由项目差异分析确定,不能一律声称全量结果继续有效。 |
工程常见问题
软件加固 PoC 能安装并启动,是否可以判定通过?
不能。启动只是入口条件,还要覆盖核心业务、异常与生命周期、正式支持矩阵、安全声明、稳定性阈值、交付证据和复测条件。
PoC 通过率达到某个百分比是否足够?
不能只看平均比例。任一 blocker 失败或缺证都应阻断,矩阵中的失败也要按正式支持范围和预先冻结的 severity 单独处理。
云真机矩阵能否完全替代自有真实设备?
不能。云矩阵适合扩大设备和系统覆盖,但企业网络、外设、特定厂商能力与现场环境仍可能需要自有设备验证。
accepted-limitation 是否等同于技术通过?
不等同。它表示责任人商业上接受明确限制,必须记录范围、影响和批准;不能用来覆盖 blocker,也不能计入技术 passed。
候选只改了一项加固配置,旧 PoC 报告还能使用吗?
需要先做差异分析。配置可能改变代码、资源、启动、动态加载或运行时控制,受影响条款必须重新执行,旧候选摘要的证据不能直接挂到新文件。
发起软件加固 PoC 前应准备哪些材料?
准备未加固发布基线、待保护资产、核心路径、正式支持矩阵、业务阈值、测试账号与安全数据、签名要求和证据格式,再通过御盾中央平台提交申请。