Skip to content
Demo示例FAQ

PDF 生成模板编辑器

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

查看 GitHub

不可见数字签名 ​

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

ComPDF SDK 创建标准 PDF 签名表单域(/FT /Sig)及其 Widget 注释(/Subtype /Widget)。签名域的值 /V 指向包含 /Contents 和 /ByteRange 的签名字典。将签名域的 bounds 设置为 CGRectZero 后,写入 PDF 的 Widget 矩形为 /Rect [0 0 0 0],宽度和高度均为零,符合 PDF 规范对不可见签名域及其注释的表示方式。此结构可参考 PDF Association 数字签名技术说明的第 1.2 节。ComPDF SDK 为该 Widget 写入的外观流边界框同样为 [0 0 0 0],且不包含任何绘制内容,因此签名后页面的渲染结果与签名前一致。

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

步骤 1:准备签名材料

按照添加许可证密钥应用包含数字签名功能的许可证后,再执行以下代码。准备:

  • 已保存到文件、至少包含一页的输入 PDF。签名时 ComPDF SDK 会读取 CPDFDocument 的 documentURL 所指向的文件。
  • 包含私钥、签名者证书及所需中间证书的 PKCS#12 文件(.p12 或 .pfx),以及该文件的密码。
  • 与输入路径不同的输出路径。

下面的代码块构成一个顺序执行的流程,应放在同一个方法中。签名和验证会读取并写出整个文件,处理较大的文件时建议在后台队列中执行。

swift
// 输入 PDF、PKCS#12 证书文件及其密码,以及与输入路径不同的输出路径。
let documentsPath = NSHomeDirectory() + "/Documents"
let inputURL = URL(fileURLWithPath: documentsPath + "/input.pdf")
let outputURL = URL(fileURLWithPath: documentsPath + "/invisible-signed.pdf")
let certificatePath = documentsPath + "/signer.p12"
let certificatePassword = "YOUR_PKCS12_PASSWORD"

guard FileManager.default.fileExists(atPath: inputURL.path),
      FileManager.default.fileExists(atPath: certificatePath) else {
    print("Cannot find the input PDF or the PKCS#12 file")
    return
}
guard inputURL.standardizedFileURL != outputURL.standardizedFileURL else {
    print("Use different input and output paths")
    return
}
objective-c
// 输入 PDF、PKCS#12 证书文件及其密码,以及与输入路径不同的输出路径。
NSString *documentsPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"];
NSURL *inputURL = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:@"input.pdf"]];
NSURL *outputURL = [NSURL fileURLWithPath:[documentsPath stringByAppendingPathComponent:@"invisible-signed.pdf"]];
NSString *certificatePath = [documentsPath stringByAppendingPathComponent:@"signer.p12"];
NSString *certificatePassword = @"YOUR_PKCS12_PASSWORD";

if (![[NSFileManager defaultManager] fileExistsAtPath:inputURL.path] ||
    ![[NSFileManager defaultManager] fileExistsAtPath:certificatePath]) {
    NSLog(@"Cannot find the input PDF or the PKCS#12 file");
    return;
}
if ([inputURL.URLByStandardizingPath isEqual:outputURL.URLByStandardizingPath]) {
    NSLog(@"Use different input and output paths");
    return;
}

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

这里的导入指从指定 PKCS#12 文件读取签名身份,不会将身份安装到系统钥匙串,也不会将证书设为可信。certificateWithPKCS12Path:password: 在密码错误或文件无法解析时返回 nil,返回的 CPDFSignatureCertificate 可用于检查主题、有效期等证书信息。

swift
// 从 PKCS#12 文件读取签名证书。返回 nil 表示密码错误或文件无法解析。
guard let signingCertificate = CPDFSignatureCertificate(pkcs12Path: certificatePath,
                                                        password: certificatePassword) else {
    print("Cannot unlock the PKCS#12 file")
    return
}
// 检查证书有效期。
let now = Date()
guard let validityStarts = signingCertificate.validityStarts,
      let validityEnds = signingCertificate.validityEnds,
      validityStarts <= now, now <= validityEnds else {
    print("Certificate is outside its validity period")
    return
}
print("Signing certificate: \(signingCertificate.subjectDict["CN"] ?? "")")
objective-c
// 从 PKCS#12 文件读取签名证书。返回 nil 表示密码错误或文件无法解析。
CPDFSignatureCertificate *signingCertificate = [CPDFSignatureCertificate certificateWithPKCS12Path:certificatePath
                                                                                          password:certificatePassword];
if (!signingCertificate) {
    NSLog(@"Cannot unlock the PKCS#12 file");
    return;
}
// 检查证书有效期。
NSDate *now = [NSDate date];
if (!signingCertificate.validityStarts || !signingCertificate.validityEnds ||
    [now compare:signingCertificate.validityStarts] == NSOrderedAscending ||
    [now compare:signingCertificate.validityEnds] == NSOrderedDescending) {
    NSLog(@"Certificate is outside its validity period");
    return;
}
NSLog(@"Signing certificate: %@", signingCertificate.subjectDict[@"CN"]);

后续的 writeSignatureToURL:withWidget:PKCS12Cert:password:location:reason:isSetSignAp:permissions: 从同一个 PKCS#12 文件加载私钥及签名身份。

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

创建签名域,设置字段名并将 bounds 设置为 CGRectZero。不要调用 signAppearanceConfig: 配置文字、图片或标识外观,并将 isSetSignAp 设为 NO(Swift 中为 false)。下面的输出使用 DocMDP 权限 CPDFSignaturePermissionsForbidChange,禁止签名后修改文档。

swift
guard let document = CPDFDocument(url: inputURL), let page = document.page(at: 0),
      let widget = CPDFSignatureWidgetAnnotation(page: page, document: document) else {
    print("Cannot create the signature field")
    return
}
// 零尺寸签名域:不配置文字、图片或标识外观。
widget.setFieldName("InvisibleSignature")
widget.bounds = .zero
page.updateAndAddAnnotation(widget)

// isSetSignAp: false:不生成动态签名外观。
// .forbidChange:DocMDP 权限,签名后不允许修改文档。
let didSign = document.writeSignature(to: outputURL,
                                      withWidget: widget,
                                      pkcs12Cert: certificatePath,
                                      password: certificatePassword,
                                      location: "Singapore",
                                      reason: "Approval",
                                      isSetSignAp: false,
                                      permissions: .forbidChange)
guard didSign else {
    print("Failed to write the invisible signature")
    return
}
objective-c
CPDFDocument *document = [[CPDFDocument alloc] initWithURL:inputURL];
CPDFPage *page = [document pageAtIndex:0];
CPDFSignatureWidgetAnnotation *widget = page ? [[CPDFSignatureWidgetAnnotation alloc] initWithPage:page document:document] : nil;
if (!widget) {
    NSLog(@"Cannot create the signature field");
    return;
}
// 零尺寸签名域:不配置文字、图片或标识外观。
[widget setFieldName:@"InvisibleSignature"];
widget.bounds = CGRectZero;
[page updateAndAddAnnotation:widget];

// isSetSignAp:NO:不生成动态签名外观。
// CPDFSignaturePermissionsForbidChange:DocMDP 权限,签名后不允许修改文档。
BOOL didSign = [document writeSignatureToURL:outputURL
                                  withWidget:widget
                                  PKCS12Cert:certificatePath
                                    password:certificatePassword
                                    location:@"Singapore"
                                      reason:@"Approval"
                                 isSetSignAp:NO
                                 permissions:CPDFSignaturePermissionsForbidChange];
if (!didSign) {
    NSLog(@"Failed to write the invisible signature");
    return;
}

注: 签名成功后,ComPDF SDK 会将签名证书添加到本机的信任证书文件夹(参见 信任证书)。因此在执行签名的设备上,该签名者证书会被判定为可信;在其他设备上验证时,证书是否可信取决于该设备的信任设置。

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

验证实际保存的输出文件,分别检查密码学完整性、文档修改权限、证书有效期和证书信任:

  • verifySignature::签名覆盖的数据是否未被篡改。
  • verifyDocument::签名后的修改是否符合签名设置的权限。
  • verifySignatureWithDocument::执行上述两项验证,并将结果写入 CPDFSigner 的 isSignVerified,两项均通过时为 YES。
  • isCertTrusted:签名者证书能否通过信任证书文件夹中的证书建立信任。
swift
guard let signedDocument = CPDFDocument(url: outputURL),
      let signatures = signedDocument.signatures(), signatures.count == 1,
      let signature = signatures.first else {
    print("Expected one signature in the output PDF")
    return
}
// 验证签名及文档修改状态,结果写入每个 CPDFSigner。
signature.verifySignature(with: signedDocument)

let integrity = signature.verifySignature(signedDocument)
let permissionsValid = signature.verifyDocument(signedDocument)
var signersVerified = !signature.signers.isEmpty
var certificatesTrusted = !signature.signers.isEmpty
var certificatesWithinValidity = !signature.signers.isEmpty
for signer in signature.signers {
    signersVerified = signersVerified && signer.isSignVerified
    certificatesTrusted = certificatesTrusted && signer.isCertTrusted
    if let certificate = signer.certificates.first,
       let starts = certificate.validityStarts, let ends = certificate.validityEnds {
        certificatesWithinValidity = certificatesWithinValidity && starts <= now && now <= ends
    } else {
        certificatesWithinValidity = false
    }
}
print("Field: \(signature.fieldName ?? ""), bounds: \(signature.bounds)")
print("Signature integrity: \(integrity)")
print("Document permissions valid: \(permissionsValid)")
print("Signer verified: \(signersVerified)")
print("Certificate within validity: \(certificatesWithinValidity)")
print("Certificate trusted: \(certificatesTrusted)")
objective-c
CPDFDocument *signedDocument = [[CPDFDocument alloc] initWithURL:outputURL];
NSArray<CPDFSignature *> *signatures = [signedDocument signatures];
if (signatures.count != 1) {
    NSLog(@"Expected one signature in the output PDF");
    return;
}
CPDFSignature *signature = signatures.firstObject;
// 验证签名及文档修改状态,结果写入每个 CPDFSigner。
[signature verifySignatureWithDocument:signedDocument];

BOOL integrity = [signature verifySignature:signedDocument];
BOOL permissionsValid = [signature verifyDocument:signedDocument];
BOOL signersVerified = signature.signers.count > 0;
BOOL certificatesTrusted = signature.signers.count > 0;
BOOL certificatesWithinValidity = signature.signers.count > 0;
for (CPDFSigner *signer in signature.signers) {
    signersVerified = signersVerified && signer.isSignVerified;
    certificatesTrusted = certificatesTrusted && signer.isCertTrusted;
    CPDFSignatureCertificate *certificate = signer.certificates.firstObject;
    BOOL withinValidity = certificate.validityStarts && certificate.validityEnds &&
        [now compare:certificate.validityStarts] != NSOrderedAscending &&
        [now compare:certificate.validityEnds] != NSOrderedDescending;
    certificatesWithinValidity = certificatesWithinValidity && withinValidity;
}
NSLog(@"Field: %@, bounds: %@", signature.fieldName, NSStringFromCGRect(signature.bounds));
NSLog(@"Signature integrity: %d", integrity);
NSLog(@"Document permissions valid: %d", permissionsValid);
NSLog(@"Signer verified: %d", signersVerified);
NSLog(@"Certificate within validity: %d", certificatesWithinValidity);
NSLog(@"Certificate trusted: %d", certificatesTrusted);

签名后的文件被修改时(例如签名覆盖范围内的字节被篡改),verifySignature: 和 isSignVerified 均返回 NO。不可见签名的 bounds 读回为零矩形,pageIndexWithDocument: 返回签名域所在的页码。

这是基础签名验证,不检查证书吊销状态,也不验证 LT/LTA 级别。需要 OCSP 检查时可使用 CPDFSignatureCertificate 的 verifyOCSPWithCompletionHandler:;需要结构化验证报告或长期验证时,请参考 签名工作流与验证报告 与 PAdES 与长期验证(LTV)。

查看结果

签名后的 PDF 页面不显示任何可视签名外观。通过 signatures 可读取该签名的字段名、签名者、签名时间、原因和位置等信息;支持数字签名的 PDF 阅读器也可在签名面板中查看签名信息。阅读器未信任签名者证书时(例如使用自签名证书),可能提示签名者身份未知,这不影响签名完整性的验证结果。