- 后端
- 文档
【免费下载链接】ofdrw
OFD Reader & Writer 开源的OFD处理库,支持文档生成、数字签名、文档保护、文档合并、转换、导出等功能,文档格式遵循《GB/T 33190-2016 电子文件存储与交换格式版式文档》。
本篇技术指南围绕 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"标准。
从源码结构看,该协议可以概括为三步(对应 协议文档 中的描述):
- 把所有 OFD 内容所有相关联的文件放入一个"清单",也就是完整性保护文件(
OFDEntries.xml); - 对完整性保护文件实行带有时间戳的数字签名;
- 把完整性保护文件和数字签名放入 OFD 包内,重新打包。
验证侧则是其逆过程:
- 验证签名是否正确;
- 遍历包内所有文件,如果存在不在完整性保护文件列表中的文件,则认为是"夹带"。
容器中的两个关键文件
结合 ofdrw-crypto 模块总览 与 OFDEntries.java 的注释,GM/T 0099 在标准容器结构中引入了以下与完整性保护直接相关的文件对象:
| 文件 | 位置 | 作用 |
|---|---|---|
OFDEntries.xml | OFD 根目录(与OFD.xml同级) | 完整性保护描述文件,描述支撑文件完整性的包内文件列表(FileList)与签名值位置(SignedValueLoc) |
signedvalue.dat | OFD 根目录(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 完整性保护需要以下五个步骤:
- 准备进行完整性保护签名者的私钥和签名证书;
- 创建完整性保护对象
OFDIntegrity; - 创建签名实现容器(
ProtectSigner实现); - 执行完整性保护;
- 结束时关闭对象,清空工作过程中的临时文件。
下面以没有加密的 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)做了四件事:
- 使用 BouncyCastle 的
SM3.Digest对OFDEntries.xml全量字节计算杂凑值(作为待签名原文); - 使用
Signature.getInstance("SM3WithSM2", new BouncyCastleProvider()),以签名私钥对杂凑值签名; - 通过 SignedDataBuilder 组装包含原文、签名值与签名者证书的
SignedDataASN.1 结构; - 外层再包一层
ContentInfo(OIDs.signedData, signedData)并做 DER 编码,最终字节流即写入signedvalue.dat的签名值。
需要注意:标准 7.4.2 要求"带有时间戳的数字签名",而从源码结构看,当前GMProtectSigner生成的SignerInfo并未附加时间戳属性,签名值仅包含原文、SM2 签名与签名者证书信息。如果你的业务要求时间戳存证,可以在自定义ProtectSigner实现中于签名属性阶段补充时间戳信息,验证端再同步支持解析。
完整性验证
验证代码示例
进行 OFD 的完整性验证需要三个步骤:
- 创建 OFD 完整性验证器;
- 创建用于验证签名值的验证器;
- 执行完整性验证,获取验证结果。
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 的实现逻辑是:
- 把
FileList中所有条目的FileLoc(统一补全为/开头的容器内绝对路径)加入HashSet; - 再把
SignedValueLoc(签名值文件)本身加入集合,因为OFDEntries.xml只保护清单本身,signedvalue.dat需要单独登记; - 遍历包内实际出现的所有文件,任何不在集合中的路径都会记录在
invalidFileLocHolder并立即终止遍历; - 非法路径为空则返回
true(没有夹带),否则返回false。
OFDEntries.xml自身被显式忽略(不参与比对),因为其内容由签名值保护,不进入文件列表。
GMProtectVerifier:GB/T 35275 签名值验证
默认的国密验证器 GMProtectVerifier.java 内部委托给 GBT35275Validate.validate,其验证逻辑对应《GM/T 0099-2020》7.2.2 数据格式要求中"签名值为数字签名且签名算法使用 SM2 时,签名值数据应符合 GB/T 35275 要求":
- 解析
signedvalue.dat为ContentInfo,检查内容类型必须是signedData,再解析出SignedData; - 用 SM3 重新计算
OFDEntries.xml的杂凑值,与签名结构中携带的原文/摘要比对(并对非规范的 Base64 编码摘要做了兼容处理),不一致则返回"待签名原文不符"; - 从
SignerInfo中取发行者-序列号(IssuerAndSerialNumber)在签名值内定位签名者证书,用证书公钥执行SM3withSm2验签,签名值不一致返回"签名值不一致"; - 任何环节失败都以
VerifyInfo形式携带错误描述返回,方法整体返回boolean结果。
端到端验证路径小结
把保护与验证串联起来,一条完整且可复现的路径是:
| 阶段 | 输入 | 关键类 | 输出 |
|---|---|---|---|
| 密钥准备 | sign_key.pem/sign_cert.pem | PEMLoader(ofdrw-gm) | PrivateKey/Certificate |
| 保护 | hello.ofd+ProtectSigner | OFDIntegrity + GMProtectSigner | 含OFDEntries.xml与signedvalue.dat的新 OFD |
| 验证 | 受保护 OFD +ProtectVerifier | OFDIntegrityVerifier + GMProtectVerifier | boolean(完整且无夹带) |
对应测试用例:
- 保护流程: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 电子文件存储与交换格式版式文档》。
相关推荐
提升React Native应用用户体验:react-native-hyperlink高级用法与最佳实践
提升React Native应用用户体验:react native hyperlink高级用法与最佳实践 react native hyperlink是一个强大
ofdrw-crypto:基于 GM/T 0099-2020 的 OFD 密码应用——文档加密、完整性保护与扩展容器结构
ofdrw crypto:基于 GM/T 0099 2020 的 OFD 密码应用——文档加密、完整性保护与扩展容器结构 OFD 是"xml+zip"格式架构的
后端文档Thorium 浏览器:让用了几年的旧电脑再快一截
Thorium 浏览器:让用了几年的旧电脑再快一截 旧笔记本风扇嗡嗡转,标签页一开多就卡,在线视频转半天圈。多半不是网的问题,是 CPU 没被充分利用。Thor
桌面应用跨平台
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考