news 2026/10/14 17:56:32

ofdrw OFD完整性保护协议:基于 SM2/SM3 国密签名的防夹带与完整性校验实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ofdrw OFD完整性保护协议:基于 SM2/SM3 国密签名的防夹带与完整性校验实战
  • 后端
  • 文档

【免费下载链接】ofdrw

OFD Reader & Writer 开源的OFD处理库,支持文档生成、数字签名、文档保护、文档合并、转换、导出等功能,文档格式遵循《GB/T 33190-2016 电子文件存储与交换格式版式文档》。

项目地址:https://gitcode.com/gh_mirrors/of/ofdrw
点击查看免费下载

本篇技术指南围绕 ofdrw 仓库ofdrw-crypto模块实现的《GM/T 0099-2020》OFD完整性保护协议展开,讲解如何利用国密 SM2 私钥与 SM2 证书对 OFD 包内文件清单执行带签名值的防夹带保护,以及如何在验证端复算 SM3 杂凑、校验 GB/T 35275 签名值并检测夹带文件。读完后,你将能够独立完成完整性保护与验证的完整闭环,并通过ProtectSigner/ProtectVerifier接口定制个性化的签名与摘要方案。

协议概述:什么是"夹带文件"

OFD 本质上是一个 ZIP 容器,从技术机制上可以包含任意文件。《GM/T 0099-2020》附录 D 总体说明给出了"夹带文件"的定义:

如果一个 OFD 文件包内的文件,其在按照"OFD.xml""Encryptions.xml"及其衍生文件充分遍历后,仍未被任何其他文件引用,这就属于"夹带文件"。

也就是说,一个声称只有若干文档页面的 OFD 包内,可能隐藏着与文档内容完全无关的文件。为了防止这种情况,《GM/T 0099-2020》7.4.2 数据格式要求规定:

  • OFD 的完整性保护信息使用 XML 文件描述,即完整性保护描述文件;
  • 完整性保护文件中应记录 OFD 文件内与文件内容相关的所有包内文件;
  • 应对完整性保护文件实现带有时间戳的数字签名;
  • 签名值应符合"GB/T 35275"标准。

从源码结构看,该协议可以概括为三步(对应 协议文档 中的描述):

  1. 把所有 OFD 内容所有相关联的文件放入一个"清单",也就是完整性保护文件(OFDEntries.xml);
  2. 对完整性保护文件实行带有时间戳的数字签名;
  3. 把完整性保护文件和数字签名放入 OFD 包内,重新打包。

验证侧则是其逆过程:

  1. 验证签名是否正确;
  2. 遍历包内所有文件,如果存在不在完整性保护文件列表中的文件,则认为是"夹带"。

容器中的两个关键文件

结合 ofdrw-crypto 模块总览 与 OFDEntries.java 的注释,GM/T 0099 在标准容器结构中引入了以下与完整性保护直接相关的文件对象:

文件位置作用
OFDEntries.xmlOFD 根目录(与OFD.xml同级)完整性保护描述文件,描述支撑文件完整性的包内文件列表(FileList)与签名值位置(SignedValueLoc)
signedvalue.datOFD 根目录(ofdrw 的默认约定)二进制签名值文件,数据结构符合 GB/T 35275

OFDEntries.java 中的元素说明与《GM/T 0099-2020》表 D.1 一一对应:

  • ID(必选属性):描述文件标识符;
  • CreatorName、Version、CreationDate(可选属性):创建者名称、模块版本、创建时间;
  • SignedValueLoc(必选):指向签名值文件的容器内路径,ofdrw 实现中固定写入/signedvalue.dat;
  • FileList(必选):防止夹带文件列表,其中每个条目由 File.java 与 FileList.java 承载,记录文件的ID与容器内绝对路径(FileLoc)。

快速开始:国密完整性保护

进行 OFD 完整性保护需要以下五个步骤:

  1. 准备进行完整性保护签名者的私钥和签名证书;
  2. 创建完整性保护对象OFDIntegrity;
  3. 创建签名实现容器(ProtectSigner实现);
  4. 执行完整性保护;
  5. 结束时关闭对象,清空工作过程中的临时文件。

下面以没有加密的 SM2 私钥和数字证书为例,演示该过程:

class HelloWorld { public static void main(String[] args) { Path src = Paths.get("src/test/resources/hello.ofd"); Path out = Paths.get("target/hello-integrity.ofd"); Path certPemFile = Paths.get("src/test/resources", "sign_cert.pem"); Path keyPemFile = Paths.get("src/test/resources", "sign_key.pem"); // 1. 准备 签名私钥 和 签名证书 final PrivateKey privateKey = PEMLoader.loadPrivateKey(keyPemFile); final Certificate certificate = PEMLoader.loadCert(certPemFile); // 2. 创建完整性保护对象 try (OFDIntegrity ofdIntegrity = new OFDIntegrity(src, out)) { // 3. 创建签名实现容器 GMProtectSigner gmSigner = new GMProtectSigner(privateKey, certificate); // 4. 执行完整性保护 ofdIntegrity.protect(gmSigner); } // 5. 关闭对象,清除临时文件 try() System.out.println(">> " + out.toAbsolutePath()); } }

该示例的完整测试版位于 OFDIntegrityTest.java。为了便于测试,仓库在 ofdrw-crypto/src/test/resources 准备了一对测试用的 PEM(PKCS#8)证书和密钥:

  • SM2证书 sign_cert.pem
  • SM2密钥 sign_key.pem

以及待保护样例 hello.ofd 与已保护的 hello-integrity.ofd。

几点使用注意:

  • 测试代码中显式执行了Security.addProvider(new BouncyCastleProvider())。由于 SM2/SM3 运算依赖 BouncyCastle(BC 提供 SM3WithSM2 签名算法与 SM3 杂凑),如果你的环境尚未注册 BC Provider,建议在初始化前自行注册一次;
  • PEMLoader.loadPrivateKey/PEMLoader.loadCert见 PEMLoader.java,它通过 BouncyCastle 的PEMParser解析明文 PEM 私钥(PKCS#8/密钥对)与 X.509 证书,内部静态块已注册 BC Provider;
  • OFDIntegrity构造时会校验源文件存在性与输出位置,随后把 OFD 解压到系统临时目录(ofd-tmp-前缀),并在close()时删除该工作目录。因此必须使用 try-with-resources 或在结束时调用close(),否则会残留临时文件。

自定义签名实现:ProtectSigner 接口

示例中采用GMProtectSigner实现摘要和签名的计算过程,它实现了ProtectSigner接口。如果对签名和摘要计算有特殊需求(例如换用其他签名方案、加入时间戳属性、使用硬件密码设备等),请通过实现 ProtectSigner 接口来个性化定制签名:

@FunctionalInterface public interface ProtectSigner { /** * 根据 GM/T 0099 OFD完整性保护协议 执行完整性保护 7.4.3 中的 c) d) 步骤 * c) 根据签名方案,计算完整性保护文件的杂凑值; * d) 根据签名方案,使用版式文件合成者的签名私钥对杂凑值进行数字签名; * * @param tbs 待签名文件路径 * @return 签名值应符合"GB/T 35275"标准 */ byte[] digestThenSign(Path tbs) throws GeneralSecurityException, IOException; }

从接口定义可见,ofdrw 将协议中"杂凑 + 签名"两个密码学步骤封装为一次函数式调用,协议骨架(清单组装、文件写入、打包)由OFDIntegrity负责,密码运算完全交给实现类。

源码纵深:OFDIntegrity.protect 的保护流程

OFDIntegrity.java 实现了Closeable,其protect(ProtectSigner signer)方法(L78-L121)严格按《GM/T 0099-2020》7.4.3 的步骤执行:

第一步:构造清单头。创建OFDEntries对象,写入自增ID、创建者名称(固定为ofdrw-crypto)、模块版本与创建时间,并把SignedValueLoc固定指向/signedvalue.dat:

final OFDEntries ofdEntries = new OFDEntries() .setID(String.valueOf(this.idProvider.incrementAndGet())) .setCreatorName("ofdrw-crypto") .setVersion(GlobalVar.Version) .setCreationDate(LocalDateTime.now()); ofdEntries.setSignedValueLoc(ST_Loc.getInstance("/signedvalue.dat")) .setFileList(fileList);

第二步:a) 确认文件包内的所有文件。使用Files.walkFileTree遍历解压后的工作目录,将每个文件的绝对路径替换掉文件系统根前缀,得到容器内 Unix 风格的绝对路径(如/Doc_0/Pages/Page_0/Content_0.xml),逐个fileList.addFile(id, abxFilePath)记入清单。这一步正是协议中"记录与文件内容相关的所有包内文件"的落地实现。

第三步:b) 组装并落盘完整性保护文件。通过ElemCup.dump(ofdEntries, ofdEntriesPath)将清单序列化为工作目录下的OFDEntries.xml。

第四步:c) d) 计算杂凑值并签名。调用signer.digestThenSign(ofdEntriesPath),把OFDEntries.xml的摘要用签名私钥签名,得到 GB/T 35275 格式的签名值字节流。

第五步:e) 写入签名值并打包。将签名结果写入signedvalue.dat,随后this.ofdDir.jar(dest)把整个工作目录重新打包输出为带完整性保护的 OFD 文件。

此外,OFDIntegrity构造函数(L54-L67)会对ofdFile不存在、dest为空等非法入参抛出IllegalArgumentException,避免在错误的输入上执行昂贵的解压与签名流程;close()(L131-L144)则负责删除临时工作目录,这就是文档强调"结束,关闭清空工作过程中的临时文件"的原因。

GMProtectSigner:SM3 + SM2 的签名值构造

默认国密实现 GMProtectSigner.java 的digestThenSign(L68-L88)做了四件事:

  1. 使用 BouncyCastle 的SM3.Digest对OFDEntries.xml全量字节计算杂凑值(作为待签名原文);
  2. 使用Signature.getInstance("SM3WithSM2", new BouncyCastleProvider()),以签名私钥对杂凑值签名;
  3. 通过 SignedDataBuilder 组装包含原文、签名值与签名者证书的SignedDataASN.1 结构;
  4. 外层再包一层ContentInfo(OIDs.signedData, signedData)并做 DER 编码,最终字节流即写入signedvalue.dat的签名值。

需要注意:标准 7.4.2 要求"带有时间戳的数字签名",而从源码结构看,当前GMProtectSigner生成的SignerInfo并未附加时间戳属性,签名值仅包含原文、SM2 签名与签名者证书信息。如果你的业务要求时间戳存证,可以在自定义ProtectSigner实现中于签名属性阶段补充时间戳信息,验证端再同步支持解析。

完整性验证

验证代码示例

进行 OFD 的完整性验证需要三个步骤:

  1. 创建 OFD 完整性验证器;
  2. 创建用于验证签名值的验证器;
  3. 执行完整性验证,获取验证结果。
class HelloWorld { public static void main(String[] args) { Path in = Paths.get("src/test/resources/hello-integrity.ofd"); // 1. 创建OFD完整性验证器。 OFDIntegrityVerifier ofdIntegrityVerifier = new OFDIntegrityVerifier(); // 2. 创建用于验证签名值的验证器。 GMProtectVerifier gmProtectVerifier = new GMProtectVerifier(); // 3. 执行完整性验证,获取验证结果。 final boolean integrity = ofdIntegrityVerifier.integrity(in, gmProtectVerifier); System.out.println(">> OFD完整性校验: " + integrity); } }

完整测试见 OFDIntegrityVerifierTest.java,其中断言hello-integrity.ofd的完整性校验结果为true。

示例中采用GMProtectVerifier实现摘要与签名验证,它实现了ProtectVerifier接口。如果对签名和摘要计算有特殊需求,请通过实现 ProtectVerifier 接口来实现个性化定制签名验证:

@FunctionalInterface public interface ProtectVerifier { /** * 根据 GM/T 0099 OFD完整性保护协议 执行完整性保护 7.4.6 * b) 根据签名方案,调用杂凑算法计算完整性保护文件得到杂凑值 * c) 读取签名值文件,进行签名验证 * * @param ofdEntriesXmlPath 防夹带文件路径 * @param signedValue 待验证的签名值,数据结构参照《GM/T 0099 2020》7.4.3 密码算法要求 * @return 签名值验证结果 */ boolean digestThenVerify(Path ofdEntriesXmlPath, byte[] signedValue) throws GeneralSecurityException, IOException; }

OFDIntegrityVerifier:双重校验逻辑

OFDIntegrityVerifier.java 的integrity(Path in, ProtectVerifier verifier)方法(L44-L79)遵循《GM/T 0099》7.4.6 校验流程,最终返回值为integrity && hasExtraFile的"与"结果,即签名验证通过且无夹带文件才返回true。具体分为:

a) 读取完整性保护描述文件。同样把 OFD 解压到临时目录(finally块保证删除),通过OFDDir定位根目录下的OFDEntries.xml。若文件不存在会抛出FileNotFoundException——这本身就可以作为"未执行完整性保护"的判据。

b) c) 签名验证。解析OFDEntries对象得到SignedValueLoc,读出signedvalue.dat的字节流,再调用verifier.digestThenVerify(ofdEntriesPath, signedValue)完成杂凑与签名校验。

d) 防夹带比对(checkNoExtraFile)。L88-L125 的实现逻辑是:

  1. 把FileList中所有条目的FileLoc(统一补全为/开头的容器内绝对路径)加入HashSet;
  2. 再把SignedValueLoc(签名值文件)本身加入集合,因为OFDEntries.xml只保护清单本身,signedvalue.dat需要单独登记;
  3. 遍历包内实际出现的所有文件,任何不在集合中的路径都会记录在invalidFileLocHolder并立即终止遍历;
  4. 非法路径为空则返回true(没有夹带),否则返回false。

OFDEntries.xml自身被显式忽略(不参与比对),因为其内容由签名值保护,不进入文件列表。

GMProtectVerifier:GB/T 35275 签名值验证

默认的国密验证器 GMProtectVerifier.java 内部委托给 GBT35275Validate.validate,其验证逻辑对应《GM/T 0099-2020》7.2.2 数据格式要求中"签名值为数字签名且签名算法使用 SM2 时,签名值数据应符合 GB/T 35275 要求":

  1. 解析signedvalue.dat为ContentInfo,检查内容类型必须是signedData,再解析出SignedData;
  2. 用 SM3 重新计算OFDEntries.xml的杂凑值,与签名结构中携带的原文/摘要比对(并对非规范的 Base64 编码摘要做了兼容处理),不一致则返回"待签名原文不符";
  3. 从SignerInfo中取发行者-序列号(IssuerAndSerialNumber)在签名值内定位签名者证书,用证书公钥执行SM3withSm2验签,签名值不一致返回"签名值不一致";
  4. 任何环节失败都以VerifyInfo形式携带错误描述返回,方法整体返回boolean结果。

端到端验证路径小结

把保护与验证串联起来,一条完整且可复现的路径是:

阶段输入关键类输出
密钥准备sign_key.pem/sign_cert.pemPEMLoader(ofdrw-gm)PrivateKey/Certificate
保护hello.ofd+ProtectSignerOFDIntegrity + GMProtectSigner含OFDEntries.xml与signedvalue.dat的新 OFD
验证受保护 OFD +ProtectVerifierOFDIntegrityVerifier + GMProtectVerifierboolean(完整且无夹带)

对应测试用例:

  • 保护流程:OFDIntegrityTest.java;
  • 验证流程:OFDIntegrityVerifierTest.java。

相关实现均位于ofdrw-crypto模块的org.ofdrw.crypto.integrity包,XML 结构对象由ofdrw-core的org.ofdrw.core.integrity包提供,国密 ASN.1 结构与 PEM 工具来自ofdrw-gm模块,三者组合构成了 ofdrw 对《GM/T 0099-2020》OFD完整性保护协议的落地实现。

  • 后端
  • 文档

【免费下载链接】ofdrw

OFD Reader & Writer 开源的OFD处理库,支持文档生成、数字签名、文档保护、文档合并、转换、导出等功能,文档格式遵循《GB/T 33190-2016 电子文件存储与交换格式版式文档》。

项目地址:https://gitcode.com/gh_mirrors/of/ofdrw
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 17:56:25

无人叉车全流程方案:从物流拆解到部署避坑的完整指南

简介:一份聚焦制造业与仓储物流智能化转型的演示文稿资源,面向物流规划、仓储管理及自动化选型人员。方案围绕人工叉车招工难、用工贵、管理低效等痛点,说明无人叉车相对于传统自动导引车与自主移动机器人在重载搬运、高位货架存储、产线配送…

作者头像 李华
网站建设 2026/10/14 17:55:34

CCF-CSP真题不是题库,而是算法能力诊断仪

简介:本资源为CCF-CSP认证考试历年真题汇编PDF,面向计算机专业学生、算法与编程初学者及备考CSP认证的应试者,旨在提供权威、规范的官方赛题训练材料,助力夯实基础编程能力、熟悉评测环境与题型结构。压缩包仅含1个PDF文件&#x…

作者头像 李华
网站建设 2026/10/14 17:54:52

基于Kubernetes的CI/CD实践:从镜像构建到多环境发布

1. 为什么要把 CI/CD 搬上 Kubernetes过去几年我经手过不少部署方案,从最早的脚本打包、到后来基于虚拟机的 Jenkins 集群、再到容器化部署,每一步都是在解决同一个问题:环境不一致导致的上线事故。而把 CI/CD 这套流程本身跑在 Kubernetes 上…

作者头像 李华
网站建设 2026/10/14 17:54:52

电阻应变式电子秤设计:从传感器选型到高精度标定

简介:一份基于电阻应变式传感器的电子秤课程设计文档,面向电子、测控类专业学生及传感器应用开发者。方案围绕2kg手提电子秤展开,完整覆盖电阻应变式传感器工作原理、惠斯通电桥测量电路、LM358差动放大电路、A/D转换与LED显示电路&#xff0…

作者头像 李华
网站建设 2026/10/14 17:54:30

洛谷P1309瑞士轮:归并排序优化实战与C++实现

1. 从一道竞赛题看排序算法的实战选择"瑞士轮"这个赛制名词,看比赛的人都不陌生——每轮比赛结束后,成绩相近的选手重新配对,赢的往上走,输的往下掉,最终决出排名。洛谷上的P1309这道题,就是把这…

作者头像 李华