PDF 生成模板编辑器
开源可视化 PDF 生成引擎,支持自定义模板,并提供面向开发者的 API,灵活构建文档生成流程。
CPDFSignatureWorkflow 提供一站式签名与结构化验证。与 创建数字签名 中"先创建签名域、再写入签名"的方式不同,工作流在一次调用中完成创建签名域、签名和写出文件,并在签名后自动重新加载输出文件、生成验证报告。创建数字签名至验证数字签名 中的接口保持不变,需要更细粒度的控制时仍可使用。
使用工作流前请注意:
document.documentURL 读取磁盘上的文件,内存中未保存的修改不会包含在内,请先保存文档。签名类型由 CPDFSignatureCreateOptions 的 profile 属性指定:
| 值 | 说明 |
|---|---|
CPDFSignatureProfilePKCS7Detached | 默认值。普通的 adbe.pkcs7.detached 签名,兼容性最广。 |
CPDFSignatureProfilePAdESB | PAdES Baseline-B 签名(ETSI.CAdES.detached),不含时间戳。 |
CPDFSignatureProfilePAdEST | PAdES Baseline-T 签名,在 Baseline-B 基础上内嵌签名时间戳,需要配置 TSA。 |
CPDFSignatureProfileDocumentTimestamp | 文档时间戳(ETSI.RFC3161),不代表任何签名者身份,需配合 documentTimestampIdentityWithServerURL: 或 documentTimestampIdentityWithTSAClient: 创建的签名身份使用。 |
PAdES Baseline-LT 与 Baseline-LTA 请参考 PAdES 与长期验证(LTV)。
一步完成签名
使用工作流签名的步骤如下:
通过 CPDFSigningIdentity 指定 PKCS#12 证书及其密码。
通过 CPDFSignatureCreateOptions 设置签名类型、签名域位置、签名原因等信息,签名外观使用与 创建数字签名 相同的 CPDFSignatureConfig。
调用 signToPath:options: 签名,并读取返回的 CPDFSignatureCreateResult。
verifiesAfterSign 默认为 YES,签名后会重新加载输出文件,并按 postSignValidationOptions 生成 postSignReport;批量签名且另行验证时可将其关闭。success 为 YES 只表示签名文件已写出,签后报告仍可能未通过,请同时检查 postSignReport。
以下是使用签名工作流创建数字签名的示例代码:
// 签名身份:PKCS#12 证书及其密码
let identity = CPDFSigningIdentity(pkcs12Path: certificatePath, password: "123")
// 签名外观:与创建数字签名一节使用同一个 CPDFSignatureConfig
let appearance = CPDFSignatureConfig()
appearance.text = "Alan"
appearance.isDrawKey = true
let reasonItem = CPDFSignatureConfigItem()
reasonItem.key = "Reason"
reasonItem.value = "I am the owner of the document."
appearance.contents = [reasonItem]
// 签后验证所用的信任根证书(PEM 文件或 OpenSSL 哈希目录)
let postSignChecks = CPDFSignatureValidationOptions()
postSignChecks.trustedCertificatePath = trustedRootsPath
let options = CPDFSignatureCreateOptions()
options.profile = .pAdESB
options.signer = identity
options.fieldName = "Signature1"
options.pageIndex = 0
options.bounds = CGRect(x: 28, y: 420, width: 150, height: 70)
options.reason = "I am the owner of the document."
options.location = "Singapore"
options.appearance = appearance
options.verifiesAfterSign = true
options.postSignValidationOptions = postSignChecks
// 工作流读取 document.documentURL 指向的已保存文件,所有方法均为同步调用
DispatchQueue.global(qos: .userInitiated).async {
let workflow = CPDFSignatureWorkflow(document: document)
let result = workflow.sign(toPath: signedFilePath, options: options)
guard result.success else {
// 签名失败,errors 说明失败原因
for item in result.errors {
print("\(item.code): \(item.message)")
}
return
}
// 签名成功,postSignReport 为签后自动验证的报告
print("Signed \(result.fieldName) with \(result.subFilter), verdict: \(result.postSignReport.overallStatus.rawValue)")
}// 签名身份:PKCS#12 证书及其密码
CPDFSigningIdentity *identity = [CPDFSigningIdentity identityWithPKCS12Path:certificatePath password:@"123"];
// 签名外观:与创建数字签名一节使用同一个 CPDFSignatureConfig
CPDFSignatureConfig *appearance = [[CPDFSignatureConfig alloc] init];
appearance.text = @"Alan";
appearance.isDrawKey = YES;
CPDFSignatureConfigItem *reasonItem = [[CPDFSignatureConfigItem alloc] init];
reasonItem.key = @"Reason";
reasonItem.value = @"I am the owner of the document.";
appearance.contents = @[reasonItem];
// 签后验证所用的信任根证书(PEM 文件或 OpenSSL 哈希目录)
CPDFSignatureValidationOptions *postSignChecks = [[CPDFSignatureValidationOptions alloc] init];
postSignChecks.trustedCertificatePath = trustedRootsPath;
CPDFSignatureCreateOptions *options = [[CPDFSignatureCreateOptions alloc] init];
options.profile = CPDFSignatureProfilePAdESB;
options.signer = identity;
options.fieldName = @"Signature1";
options.pageIndex = 0;
options.bounds = CGRectMake(28, 420, 150, 70);
options.reason = @"I am the owner of the document.";
options.location = @"Singapore";
options.appearance = appearance;
options.verifiesAfterSign = YES;
options.postSignValidationOptions = postSignChecks;
// 工作流读取 document.documentURL 指向的已保存文件,所有方法均为同步调用
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
CPDFSignatureWorkflow *workflow = [[CPDFSignatureWorkflow alloc] initWithDocument:document];
CPDFSignatureCreateResult *result = [workflow signToPath:signedFilePath options:options];
if (!result.success) {
// 签名失败,errors 说明失败原因
for (CPDFValidationReportItem *item in result.errors) {
NSLog(@"%@: %@", item.code, item.message);
}
return;
}
// 签名成功,postSignReport 为签后自动验证的报告
NSLog(@"Signed %@ with %@, verdict: %ld", result.fieldName, result.subFilter, (long)result.postSignReport.overallStatus);
});注: 工作流中的签名外观支持文字与图片内容;手绘路径、字号与旋转角度不在 appearance 中提供,需要这些效果时请使用 创建数字签名 中基于 CPDFSignatureWidgetAnnotation 的方式。签名工作流不支持创建认证签名(DocMDP),也不支持签名后锁定表单域。
验证文档中的签名
validateDocumentWithOptions: 验证已打开文档中的所有签名(只验证某一个签名时,设置 CPDFSignatureWorkflowValidationOptions 的 fieldName);validatePath:options: 直接验证磁盘上的 PDF 文件,无需打开文档。验证结果中每个签名对应一份 CPDFSignatureValidationReport,报告中的检查项 CPDFValidationReportItem 按类别(category)、状态(status)和严重程度(severity:CPDFValidationSeverityInfo、CPDFValidationSeverityWarning、CPDFValidationSeverityError)给出。
通过 CPDFSignatureValidationOptions 设置验证项与信任材料:
trustedCertificatePath:信任根证书,可以是 PEM 证书包文件或 OpenSSL 哈希目录。SDK 不内置任何信任根证书,未设置时证书链(包括时间戳的 TSA 证书链)的结论为 CPDFValidationStatusIndeterminate,而不是 CPDFValidationStatusValid。该路径与 信任证书 中的信任证书文件夹相互独立:通过 addToTrustedCertificates 信任的证书不参与此处的证书链验证,也不能将信任证书文件夹直接作为该路径传入。checksRevocation:是否检查 OCSP / CRL 撤销状态,默认为 NO。开启后验证过程中可能联网。validationTimeEpochSeconds:验证时间(Unix 时间戳,单位为秒)。默认为 0,此时依次使用可信时间戳时间、签名时间和当前时间。checksIntegrity、checksCoverage、checksDocumentPermissions、checksCertificateChain、checksTimestamp:分别控制签名完整性、签名覆盖范围、文档权限、证书链与时间戳检查,默认均为 YES。以下是验证文档中数字签名的示例代码:
let validationOptions = CPDFSignatureValidationOptions()
// SDK 不内置信任根证书;未提供时,证书链的结论为 Indeterminate
validationOptions.trustedCertificatePath = Bundle.main.path(forResource: "trusted_roots", ofType: "pem")
// 撤销检查默认关闭;开启后验证过程中可能联网获取 OCSP / CRL
validationOptions.checksRevocation = false
let workflowOptions = CPDFSignatureWorkflowValidationOptions()
workflowOptions.validationOptions = validationOptions
DispatchQueue.global(qos: .userInitiated).async {
let workflow = CPDFSignatureWorkflow(document: document)
let result = workflow.validateDocument(with: workflowOptions)
// 每个签名对应一份报告
for report in result.reports {
switch report.overallStatus {
case .valid:
print("\(report.fieldName): valid")
case .invalid:
print("\(report.fieldName): invalid")
case .indeterminate:
print("\(report.fieldName): indeterminate")
default:
print("\(report.fieldName): not checked")
}
// 按严重程度读取:只列出错误项
for item in report.items(with: .error) {
print(" [error] \(item.code): \(item.message)")
}
// 按维度读取:例如证书链的检查结果
for item in report.items where item.category == .certificateChain {
print(" [chain] status \(item.status.rawValue): \(item.message)")
}
}
}CPDFSignatureValidationOptions *validationOptions = [[CPDFSignatureValidationOptions alloc] init];
// SDK 不内置信任根证书;未提供时,证书链的结论为 Indeterminate
validationOptions.trustedCertificatePath = [[NSBundle mainBundle] pathForResource:@"trusted_roots" ofType:@"pem"];
// 撤销检查默认关闭;开启后验证过程中可能联网获取 OCSP / CRL
validationOptions.checksRevocation = NO;
CPDFSignatureWorkflowValidationOptions *workflowOptions = [[CPDFSignatureWorkflowValidationOptions alloc] init];
workflowOptions.validationOptions = validationOptions;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
CPDFSignatureWorkflow *workflow = [[CPDFSignatureWorkflow alloc] initWithDocument:document];
CPDFSignatureWorkflowValidationResult *result = [workflow validateDocumentWithOptions:workflowOptions];
// 每个签名对应一份报告
for (CPDFSignatureValidationReport *report in result.reports) {
switch (report.overallStatus) {
case CPDFValidationStatusValid:
NSLog(@"%@: valid", report.fieldName);
break;
case CPDFValidationStatusInvalid:
NSLog(@"%@: invalid", report.fieldName);
break;
case CPDFValidationStatusIndeterminate:
NSLog(@"%@: indeterminate", report.fieldName);
break;
default:
NSLog(@"%@: not checked", report.fieldName);
break;
}
// 按严重程度读取:只列出错误项
for (CPDFValidationReportItem *item in [report itemsWithSeverity:CPDFValidationSeverityError]) {
NSLog(@" [error] %@: %@", item.code, item.message);
}
// 按维度读取:例如证书链的检查结果
for (CPDFValidationReportItem *item in report.items) {
if (item.category == CPDFValidationCategoryCertificateChain) {
NSLog(@" [chain] status %ld: %@", (long)item.status, item.message);
}
}
}
});验证状态 CPDFValidationStatus 的取值如下:
| 值 | 说明 |
|---|---|
CPDFValidationStatusValid | 检查通过。 |
CPDFValidationStatusInvalid | 检查未通过。 |
CPDFValidationStatusIndeterminate | 检查已执行但无法得出结论,例如证书链无法追溯到已知的信任根证书。 |
CPDFValidationStatusNotChecked | 检查未执行:被选项关闭,或缺少所需的输入(例如离线时无法获取撤销信息)。不代表失败。 |
检查项类别 CPDFValidationCategory 的取值如下:
| 值 | 说明 |
|---|---|
CPDFValidationCategorySignatureIntegrity | 签名的密码学完整性(PKCS#7 / CMS)。 |
CPDFValidationCategoryByteRangeCoverage | ByteRange 是否覆盖其声明的文档内容。 |
CPDFValidationCategoryDocumentRevision | 签名所对应的文档修订版本。 |
CPDFValidationCategoryDocumentPermissions | DocMDP / 文档权限状态。 |
CPDFValidationCategoryCertificateChain | 证书链构建与信任。 |
CPDFValidationCategoryRevocation | OCSP / CRL 撤销状态。 |
CPDFValidationCategoryTimestamp | 内嵌时间戳与文档时间戳。 |
CPDFValidationCategoryAlgorithmPolicy | 算法策略(弱摘要算法、密钥长度等)。 |
CPDFValidationCategoryPadesProfile | PAdES 基线级别。 |
CPDFValidationCategorySignatureAppearance | 签名外观。 |
CPDFValidationCategoryInput | 输入文件或参数。 |
注: CPDFSignatureWorkflowValidationResult 的 success 只有在 overallStatus 为 CPDFValidationStatusValid 时才为 YES。为 NO 既不代表验证没有执行,也不代表签名已损坏,例如 TSA 证书链不受信任或缺少长期验证材料同样会得到 NO,请读取 overallStatus 与各份报告加以区分。程序判断请匹配检查项的 code(例如 timestamp.embedded.imprint.mismatch),message 仅用于显示,且未做本地化。
配置算法策略
CPDFSignatureAlgorithmPolicy 定义遇到弱算法时的处理方式(CPDFWeakAlgorithmActionAllow、CPDFWeakAlgorithmActionWarn、CPDFWeakAlgorithmActionReject)。默认情况下,SHA-1 给出警告,MD5 被拒绝,RSA 密钥最短为 2048 位。签名时将策略设置在 CPDFSignatureCreateOptions 的 algorithmPolicy,签后验证会自动使用同一策略(覆盖 postSignValidationOptions 中的策略);单独验证时设置在 CPDFSignatureValidationOptions 的 algorithmPolicy。违反策略的检查项归入 CPDFValidationCategoryAlgorithmPolicy 类别。
以下是使用自定义算法策略验证签名的示例代码:
let policy = CPDFSignatureAlgorithmPolicy()
policy.sha1Action = .reject // 默认 warn
policy.md5Action = .reject // 默认 reject
policy.minimumRSAKeyBits = 3072 // 默认 2048
let validationOptions = CPDFSignatureValidationOptions()
validationOptions.algorithmPolicy = policy
let workflowOptions = CPDFSignatureWorkflowValidationOptions()
workflowOptions.validationOptions = validationOptions
DispatchQueue.global(qos: .userInitiated).async {
// 验证磁盘上的文件不需要打开文档
let result = CPDFSignatureWorkflow().validatePath(signedFilePath, options: workflowOptions)
for report in result.reports {
for item in report.items where item.category == .algorithmPolicy {
print("\(report.fieldName) \(item.code): \(item.message)")
}
}
}CPDFSignatureAlgorithmPolicy *policy = [[CPDFSignatureAlgorithmPolicy alloc] init];
policy.sha1Action = CPDFWeakAlgorithmActionReject; // 默认 Warn
policy.md5Action = CPDFWeakAlgorithmActionReject; // 默认 Reject
policy.minimumRSAKeyBits = 3072; // 默认 2048
CPDFSignatureValidationOptions *validationOptions = [[CPDFSignatureValidationOptions alloc] init];
validationOptions.algorithmPolicy = policy;
CPDFSignatureWorkflowValidationOptions *workflowOptions = [[CPDFSignatureWorkflowValidationOptions alloc] init];
workflowOptions.validationOptions = validationOptions;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
// 验证磁盘上的文件不需要打开文档
CPDFSignatureWorkflow *workflow = [[CPDFSignatureWorkflow alloc] init];
CPDFSignatureWorkflowValidationResult *result = [workflow validatePath:signedFilePath options:workflowOptions];
for (CPDFSignatureValidationReport *report in result.reports) {
for (CPDFValidationReportItem *item in report.items) {
if (item.category == CPDFValidationCategoryAlgorithmPolicy) {
NSLog(@"%@ %@: %@", report.fieldName, item.code, item.message);
}
}
}
});