先看结论与判断条件

  • 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 当成可信结论。

PoC 候选身份包
身份项采集时点校验规则不一致处置
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 复测触发器
变化最小影响范围旧证据状态必须动作
候选摘要或签名全部条款失效重新执行完整 PoC
加固配置受保护对象与运行路径待评估重跑关联条款
应用或依赖变更模块及消费者待评估差异分析后复测
支持矩阵新增 device、API、ABI原范围仍有效补充新 cell
测试方法或阈值使用该方法的条款不可直接比较重建基线并复测
  • 验收前冻结 candidateSha256、签名摘要、配置和测试计划版本
  • 每条 blocker 都有明确预期、执行环境和可读证据,不接受口头确认
  • requiredMatrixCells 来自正式支持范围与风险组合,不由通过结果反向删减
  • failed、blocked、not-run 和 accepted-limitation 分开呈现,不折算为技术通过
  • 证据路径限制在受控目录,摘要不一致立即阻断
  • 候选、配置、应用、依赖、矩阵或阈值变化后重评并复测关联条款
校验软件加固 PoC 条款、矩阵和证据绑定
#!/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 前应准备哪些材料?

准备未加固发布基线、待保护资产、核心路径、正式支持矩阵、业务阈值、测试账号与安全数据、签名要求和证据格式,再通过御盾中央平台提交申请。

想用自己的 App 验证?

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

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