PDF 生成模板编辑器
开源可视化 PDF 生成引擎,支持自定义模板,并提供面向开发者的 API,灵活构建文档生成流程。
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 代码。准备:
.p12 或 .pfx),以及该文件的密码。下面的代码块构成一个顺序执行的流程,应放在声明 throws IOException 的 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;在应用中设置路径和密码:
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 文件读取签名身份,不会将身份安装到操作系统,也不会自动将证书设为可信。
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 权限设置,禁止后续修改文档。
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 中嵌入的证书及配置的信任目录。
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 的信任证书前,它仍可能显示签名者身份未知。