PDF 生成模板编辑器
开源可视化 PDF 生成引擎,支持自定义模板,并提供面向开发者的 API,灵活构建文档生成流程。
SDK 使用前提及通用导入请参见数字签名概览。
二阶段签名将 PDF 准备与外部签名操作分开,支持延迟响应、另一个 JVM 或另一台机器,不要求原文档或 native 指针继续存活。
/Contents、设置 /ByteRange,返回摘要和便携 Session。| 摘要模式 | 外部服务签署的内容 | 回填方式 |
|---|---|---|
DOCUMENT_DIGEST | PDF /ByteRange 覆盖字节的摘要 | 完整 CMS 或原始签名值 |
SIGNED_ATTRIBUTES | 冻结的 CMS signed attributes 的摘要 | 原始签名值 |
支持 SHA-256、SHA-384、SHA-512。原始 RSA 签名值的已验证契约为 PKCS#1 v1.5;当前接口不表达 RSA-PSS 参数。
准备签名
SIGNED_ATTRIBUTES 模式要求 Prepare 时提供签名证书,因为属性绑定该证书。默认新签名域不可见,完整 CMS 预留容量为 65536 个二进制字节。
Files.createDirectories(Paths.get("output"));
CPDFExternalSignPrepareOptions options = new CPDFExternalSignPrepareOptions()
.setFieldName("ExternalSignature")
.setHashMode(CPDFExternalSignPrepareOptions.HashMode.SIGNED_ATTRIBUTES)
.setDigestAlgorithm(CPDFSignatureDigest.SHA256)
.setEstimatedContentsSize(65536)
.setCertificate(
Files.readAllBytes(Paths.get("certs/signer.der")),
new byte[][] {Files.readAllBytes(Paths.get("certs/issuer.der"))});
CPDFExternalSignPrepareResult prepared = CPDFExternalSignSession.prepare(
"input.pdf", "output/prepared.pdf", options);
if (!prepared.isSuccess()) {
throw new IllegalStateException(prepared.getErrorCode() + ": " + prepared.getErrorMessage());
}
CPDFExternalSignSession.saveSession("output/signing-session.txt", prepared.getSession());
Files.write(Paths.get("output/hash-to-sign.bin"), prepared.getHashToSign());
System.out.println("Digest algorithm: " + prepared.getDigestAlgorithm());
System.out.println("Hash to sign: "
+ Base64.getEncoder().encodeToString(prepared.getHashToSign()));hashToSign 已经是摘要,不能再次做普通消息摘要,也不能签 Base64 文本。prepared PDF、完整 Session、hash 和摘要算法必须属于同一事务。重新 Prepare 可能改变字节、签署时间和 hash。
saveSession() 创建新的 UTF-8 文件,不覆盖已有文件。Session 是 Core 格式,不是 Java 对象序列化;须完整保存,不裁剪或改写,调用方负责事务持久化成功。
回填原始签名值
外部服务为前一例返回 external-signature.bin 后,下面代码可以在另一个进程执行。
String session = CPDFExternalSignSession.loadSession("output/signing-session.txt");
byte[] signatureValue = Files.readAllBytes(Paths.get("external-signature.bin"));
CPDFExternalSignFillOptions options = new CPDFExternalSignFillOptions()
.setValidationOptions(new CPDFSignatureValidationOptions()
.setTrustedCertPath("certs/trusted-roots.pem"));
CPDFExternalSignFillResult result = CPDFExternalSignSession.fillWithSignatureValue(
session,
"output/prepared.pdf",
"output/external-signed.pdf",
signatureValue,
options);
System.out.println("Fill success: " + result.isSuccess());
System.out.println(result.getErrorCode() + ": " + result.getErrorMessage());
System.out.println(result.getValidationResult());SIGNED_ATTRIBUTES 使用 Session 冻结的证书和属性。DOCUMENT_DIGEST 的原始值回填要求在 Prepare 或 CPDFExternalSignFillOptions.setCertificate() 中提供证书。
prepared 文件可以移动,但其字节和 Session 必须保留原样。Fill 失败后保留 prepared 文件,可用正确的 payload 重试。
回填完整 CMS 容器
外部服务返回完整 detached CMS/PKCS#7 时,应准备独立的 DOCUMENT_DIGEST 事务。带 signed attributes 的容器,其 messageDigest 必须匹配 prepared PDF 的精确 ByteRange 内容。
Files.createDirectories(Paths.get("output"));
CPDFExternalSignPrepareResult prepared = CPDFExternalSignSession.prepare(
"input.pdf",
"output/container-prepared.pdf",
new CPDFExternalSignPrepareOptions()
.setHashMode(CPDFExternalSignPrepareOptions.HashMode.DOCUMENT_DIGEST));
if (!prepared.isSuccess()) {
throw new IllegalStateException(prepared.getErrorCode());
}
CPDFExternalSignSession.saveSession("output/container-session.txt", prepared.getSession());
// Obtain this CMS from the service for the preparation above.
byte[] containerDer = Files.readAllBytes(Paths.get("external-container.der"));
CPDFExternalSignFillResult result = CPDFExternalSignSession.fillWithContainer(
CPDFExternalSignSession.loadSession("output/container-session.txt"),
"output/container-prepared.pdf",
"output/container-signed.pdf",
containerDer,
new CPDFExternalSignFillOptions()
.setVerifyMessageDigest(true)
.setValidationOptions(new CPDFSignatureValidationOptions()
.setTrustedCertPath("certs/trusted-roots.pem")));
System.out.println(result.getValidationResult());payload 必须是单个完整、含 signer info 的 detached SignedData DER。attached CMS、损坏容器或额外尾字节会被拒绝。容量必须覆盖完整编码容器,包括证书链和时间戳;容量不足需重新 Prepare,并重新取得签名。
内存签名
Prepare 和两种 Fill 均提供 byte[] 重载,通过 getPreparedPdf() 取得准备字节、getOutputPdf() 取得完成字节。文件 Prepare/内存 Fill,以及内存 Prepare/文件 Fill 可复用同一 Core Session。
byte[] inputPdf = Files.readAllBytes(Paths.get("input.pdf"));
CPDFExternalSignPrepareResult prepared = CPDFExternalSignSession.prepare(
inputPdf,
new CPDFExternalSignPrepareOptions()
.setHashMode(CPDFExternalSignPrepareOptions.HashMode.SIGNED_ATTRIBUTES)
.setCertificate(
Files.readAllBytes(Paths.get("certs/signer.der")),
new byte[][] {Files.readAllBytes(Paths.get("certs/issuer.der"))}));
if (!prepared.isSuccess()) {
throw new IllegalStateException(prepared.getErrorCode());
}
// Obtain the service response for prepared.getHashToSign().
byte[] signatureValue = Files.readAllBytes(Paths.get("external-memory-signature.bin"));
CPDFExternalSignFillResult result = CPDFExternalSignSession.fillWithSignatureValue(
prepared.getSession(), prepared.getPreparedPdf(), signatureValue,
new CPDFExternalSignFillOptions()
.setValidationOptions(new CPDFSignatureValidationOptions()
.setTrustedCertPath("certs/trusted-roots.pem")));
if (result.isSuccess()) {
Files.createDirectories(Paths.get("output"));
Files.write(Paths.get("output/memory-signed.pdf"), result.getOutputPdf());
}
System.out.println(result.getValidationResult());内存 Prepare/Fill 本身不写 PDF 文件。可选验签使用精确原始字节的可寻址后端:Linux 可用时使用匿名内存文件,其他情况使用验证后删除的私有临时副本,不重新序列化 PDF。
Fill 成功仅表示生成 PDF,文件入口还表示发布完成,不证明签名正确或证书可信。默认开启验证,须查看 getValidationResult();关闭后为 NOT_CHECKED。
二阶段签名使用 adbe.pkcs7.detached,不自动收集 LTV 或注入内嵌时间戳。外部服务可返回含内嵌时间戳的完整容器,应用也可在 Fill 后追加独立文档时间戳。
固定配置及恢复示例见 SignatureTwoPhaseTest。