Skip to content
Demo示例FAQ

PDF 生成模板编辑器

开源可视化 PDF 生成引擎,支持自定义模板,并提供面向开发者的 API,灵活构建文档生成流程。

查看 GitHub

二阶段外部签名 ​

SDK 使用前提及通用导入请参见数字签名概览。

二阶段签名将 PDF 准备与外部签名操作分开,支持延迟响应、另一个 JVM 或另一台机器,不要求原文档或 native 指针继续存活。

  1. Prepare: 创建新签名域、预留 /Contents、设置 /ByteRange,返回摘要和便携 Session。
  2. 外部签名: 外部服务针对本轮准备的摘要生成签名。
  3. Fill: 将完整 CMS 容器或原始签名值回填到未改变的 prepared PDF。
  4. 验证: 检查完整验签报告。
摘要模式外部服务签署的内容回填方式
DOCUMENT_DIGESTPDF /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 个二进制字节。

java
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 后,下面代码可以在另一个进程执行。

java
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 内容。

java
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。

java
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。