Skip to content
Demo示例FAQ

PDF 生成模板编辑器

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

查看 GitHub

不可见数字签名 ​

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

不可见数字签名不会在页面上显示签名文字、图片、标识或其他可视签名外观。签名信息嵌入 PDF 文档中,用户仍可通过 PDF 阅读器的签名面板查看签名信息并验证签名。

SDK 创建标准 PDF 签名表单域(/FT /Sig)及其 Widget 注释(/Subtype /Widget)。签名域的值 /V 指向包含 /Contents 和 /ByteRange 的签名字典。将 Widget 矩形设置为 [0, 0, 0, 0],使其宽度和高度均为零,符合 PDF 规范对不可见签名域及其注释的表示方式。此结构可参考 PDF Association 数字签名技术说明的第 1.2 节。

不可见只影响页面外观,不改变签名的密码学完整性或证书信任要求。

步骤 1:准备 Java 应用和签名材料

按照如何运行 Demo初始化 ComPDFKit 后,再执行以下 Java 代码。准备:

  • 至少包含一页、已经保存的输入 PDF。
  • 包含私钥、签名者证书及所需中间证书的 PKCS#12 文件(.p12 或 .pfx),以及该文件的密码。
  • 存放经过独立确认的可信 PEM 证书的目录,用于验证证书链。
  • 与输入路径不同的输出路径。

下面的代码块构成一个顺序执行的流程,应放在声明 throws IOException 的 Java 方法中。需要以下导入:

java
import com.compdfkit.core.android.graphics.RectF;
import com.compdfkit.core.annotation.form.CPDFSignatureWidget;
import com.compdfkit.core.annotation.form.CPDFWidget;
import com.compdfkit.core.document.CPDFDocument;
import com.compdfkit.core.signature.CPDFSignature;
import com.compdfkit.core.signature.CPDFSigner;
import com.compdfkit.core.signature.CPDFX509;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;

在应用中设置路径和密码:

java
File input = new File("input.pdf");
File output = new File("output/invisible-signed.pdf");
File certificateFile = new File("certs/signer.p12");
String certificatePassword = "YOUR_PKCS12_PASSWORD";
File trustDirectory = new File("certs/trusted");
String trustedPath = trustDirectory.getAbsolutePath() + File.separator;

if (!input.isFile() || !input.canRead()) {
    throw new IllegalStateException("Cannot read the input PDF");
}
if (input.getCanonicalFile().equals(output.getCanonicalFile())) {
    throw new IllegalArgumentException("Use different input and output paths");
}
if (!trustDirectory.isDirectory()) {
    throw new IllegalStateException("Prepare the trusted certificate directory");
}
Files.createDirectories(output.getAbsoluteFile().getParentFile().toPath());

步骤 2:导入证书并检查密码和有效期

这里的导入指从指定 PKCS#12 文件读取签名身份,不会将身份安装到操作系统,也不会自动将证书设为可信。

java
if (!certificateFile.isFile() || !certificateFile.canRead()) {
    throw new IllegalStateException("Cannot read the PKCS#12 file");
}
if (!CPDFSignature.checkPKCS12Password(
        certificateFile.getAbsolutePath(), certificatePassword)) {
    throw new IllegalStateException("Cannot unlock the PKCS#12 file");
}
CPDFX509 signingCertificate = CPDFSignature.getX509ByPKCS12Cert(
        certificateFile.getAbsolutePath(), certificatePassword);
if (signingCertificate == null || !signingCertificate.isValid()) {
    throw new IllegalStateException("Cannot import the signing certificate");
}
if (signingCertificate.isExpired()) {
    throw new IllegalStateException("Certificate is outside its validity period");
}
System.out.println("Signing certificate: "
        + signingCertificate.getCertInfo().getSubject().getCommonName());

getX509ByPKCS12Cert() 读取证书供检查;后续 writeSignature() 从同一个 PKCS#12 文件加载私钥及签名身份。

步骤 3:创建不可见签名域并应用签名

将 Widget 矩形设为零,不配置可见文字、图片或标识外观。下面的输出使用 DocMDP 权限设置,禁止后续修改文档。

java
CPDFDocument document = new CPDFDocument();
try {
    document.open(input.getAbsolutePath());
    if (!document.isValid() || document.getPageCount() == 0) {
        throw new IllegalStateException("Cannot open a PDF page for signing");
    }
    CPDFSignatureWidget widget = (CPDFSignatureWidget) document.pageAtIndex(0)
            .addFormWidget(CPDFWidget.WidgetType.Widget_SignatureFields, document);
    if (widget == null || !widget.isValid()) {
        throw new IllegalStateException("Cannot create the signature field");
    }
    widget.setFieldName("InvisibleSignature");
    widget.setRect(new RectF(0, 0, 0, 0));

    boolean signed = document.writeSignature(
            widget,
            output.getAbsolutePath(),
            certificateFile.getAbsolutePath(),
            certificatePassword,
            "Signing location",
            "Approval",
            CPDFDocument.PDFDocMdpP.PDFDocMdpPForbidAllModify);
    if (!signed) {
        throw new IllegalStateException("Invisible signature creation failed");
    }
} finally {
    document.close();
}

步骤 4:重新打开输出并验证签名

验证实际保存的输出,分别检查密码学完整性、文档修改权限、证书有效期和证书链信任。信任检查使用签名 PDF 中嵌入的证书及配置的信任目录。

java
CPDFDocument signedDocument = new CPDFDocument();
try {
    signedDocument.open(output.getAbsolutePath());
    if (!signedDocument.isValid() || signedDocument.getSignatureCount() != 1) {
        throw new IllegalStateException("Expected one signature in the output PDF");
    }
    CPDFSignature signature = signedDocument.getPdfSignature(0);
    if (signature == null || !signature.isValid()) {
        throw new IllegalStateException("Cannot read the signature");
    }
    CPDFSigner[] signers = signature.getSignerArr();
    if (signers == null || signers.length == 0) {
        throw new IllegalStateException("The signature has no signer");
    }

    boolean integrity = signature.verify(signedDocument);
    boolean permissionsValid = signature.verifyDocument(signedDocument);
    boolean certificatesTrusted = true;
    boolean certificatesWithinValidity = true;
    for (int i = 0; i < signers.length; i++) {
        if (signers[i] == null || !signers[i].isValid()) {
            throw new IllegalStateException("Cannot read the signer");
        }
        CPDFX509 certificate = signers[i].getCert();
        if (certificate == null || !certificate.isValid()) {
            throw new IllegalStateException("Cannot read the signer certificate");
        }
        boolean withinValidity = !certificate.isExpired();
        boolean chainTrusted = certificate.verifyGetChain(trustedPath, signature);
        certificatesWithinValidity &= withinValidity;
        certificatesTrusted &= chainTrusted;
        System.out.println("Signer " + i + " certificate within validity: "
                + withinValidity);
        System.out.println("Signer " + i + " certificate chain trusted: "
                + chainTrusted);
    }
    boolean verified = integrity && permissionsValid
            && certificatesWithinValidity && certificatesTrusted;
    System.out.println("Signature integrity: " + integrity);
    System.out.println("Document permissions valid: " + permissionsValid);
    System.out.println("Basic verification passed: " + verified);
    if (!verified) {
        throw new IllegalStateException("Invisible signature verification failed");
    }
} finally {
    signedDocument.close();
}

这是基础签名验证,不检查吊销状态,不建立可信签名时间,也不验证 LT/LTA 级别。需要长期验证时,使用长期验证(LTV)与 PAdES中的 LTV 流程。

运行 sample 并查看结果

samples/SignatureTest/SignatureTest.java 的 signInvisibleDigitalSignature() 和 verifyInvisibleDigitalSignature() 实现了完整流程。在 samples/SignatureTest 目录执行 ./RunTest.sh,示例会生成测试身份、读取证书、应用不可见签名、重新打开输出,并打印各项验证结果。输出为 samples/out/SignatureTest/digitalSignature/Invisible_Signed.pdf。

示例将本次生成的自签名测试身份明确设为可信,保存至 samples/SignatureTest/trusted/invisible-demo.pem。使用已有身份时,修改 INVISIBLE_CERTIFICATE_PATH、INVISIBLE_CERTIFICATE_PASSWORD、INVISIBLE_TRUST_DIRECTORY,将 TRUST_INVISIBLE_DEMO_CERTIFICATE 设为 false,并准备经过独立确认的信任锚。这些参数固定在代码中,不需要额外命令行参数。

在 Acrobat 中打开输出的签名面板,可以查看签名信息;PDF 页面不显示任何可视签名外观。Java sample 的信任设置不会修改 Acrobat 的信任设置,因此在单独配置 Acrobat 的信任证书前,它仍可能显示签名者身份未知。