Skip to content
Demo示例FAQ

PDF 生成模板编辑器

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

查看 GitHub

签名工作流与验证报告 ​

CPDFSignatureWorkflow 提供一站式签名与结构化验证。与 创建数字签名 中"先创建签名域、再写入签名"的方式不同,工作流在一次调用中完成创建签名域、签名和写出文件,并在签名后自动重新加载输出文件、生成验证报告。创建数字签名至验证数字签名 中的接口保持不变,需要更细粒度的控制时仍可使用。

使用工作流前请注意:

  • 工作流从 document.documentURL 读取磁盘上的文件,内存中未保存的修改不会包含在内,请先保存文档。
  • 签名总是写入新的输出路径,不会修改源文件。

签名类型由 CPDFSignatureCreateOptions 的 profile 属性指定:

值说明
CPDFSignatureProfilePKCS7Detached默认值。普通的 adbe.pkcs7.detached 签名,兼容性最广。
CPDFSignatureProfilePAdESBPAdES Baseline-B 签名(ETSI.CAdES.detached),不含时间戳。
CPDFSignatureProfilePAdESTPAdES Baseline-T 签名,在 Baseline-B 基础上内嵌签名时间戳,需要配置 TSA。
CPDFSignatureProfileDocumentTimestamp文档时间戳(ETSI.RFC3161),不代表任何签名者身份,需配合 documentTimestampIdentityWithServerURL: 或 documentTimestampIdentityWithTSAClient: 创建的签名身份使用。

PAdES Baseline-LT 与 Baseline-LTA 请参考 PAdES 与长期验证(LTV)。

一步完成签名

使用工作流签名的步骤如下:

  1. 通过 CPDFSigningIdentity 指定 PKCS#12 证书及其密码。

  2. 通过 CPDFSignatureCreateOptions 设置签名类型、签名域位置、签名原因等信息,签名外观使用与 创建数字签名 相同的 CPDFSignatureConfig。

  3. 调用 signToPath:options: 签名,并读取返回的 CPDFSignatureCreateResult。

verifiesAfterSign 默认为 YES,签名后会重新加载输出文件,并按 postSignValidationOptions 生成 postSignReport;批量签名且另行验证时可将其关闭。success 为 YES 只表示签名文件已写出,签后报告仍可能未通过,请同时检查 postSignReport。

以下是使用签名工作流创建数字签名的示例代码:

swift
// 签名身份: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)")
}
objective-c
// 签名身份: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。

以下是验证文档中数字签名的示例代码:

swift
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)")
        }
    }
}
objective-c
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)。
CPDFValidationCategoryByteRangeCoverageByteRange 是否覆盖其声明的文档内容。
CPDFValidationCategoryDocumentRevision签名所对应的文档修订版本。
CPDFValidationCategoryDocumentPermissionsDocMDP / 文档权限状态。
CPDFValidationCategoryCertificateChain证书链构建与信任。
CPDFValidationCategoryRevocationOCSP / CRL 撤销状态。
CPDFValidationCategoryTimestamp内嵌时间戳与文档时间戳。
CPDFValidationCategoryAlgorithmPolicy算法策略(弱摘要算法、密钥长度等)。
CPDFValidationCategoryPadesProfilePAdES 基线级别。
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 类别。

以下是使用自定义算法策略验证签名的示例代码:

swift
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)")
        }
    }
}
objective-c
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);
            }
        }
    }
});