PDF 生成模板编辑器
开源可视化 PDF 生成引擎,支持自定义模板,并提供面向开发者的 API,灵活构建文档生成流程。
不可见数字签名不会在页面上显示签名文字、图片、标识或其他可视签名外观。签名信息嵌入 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:准备签名材料
按照添加许可证密钥应用包含数字签名功能的许可证后,再执行以下代码。准备:
CPDFDocument 的 documentURL 所指向的文件。.p12 或 .pfx),以及该文件的密码。下面的代码块构成一个顺序执行的流程,应放在同一个方法中。签名和验证会读取并写出整个文件,处理较大的文件时建议在后台队列中执行。
// 输入 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
}// 输入 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 可用于检查主题、有效期等证书信息。
// 从 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"] ?? "")")// 从 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,禁止签名后修改文档。
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
}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:签名者证书能否通过信任证书文件夹中的证书建立信任。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)")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 阅读器也可在签名面板中查看签名信息。阅读器未信任签名者证书时(例如使用自签名证书),可能提示签名者身份未知,这不影响签名完整性的验证结果。