Skip to content
Demo示例FAQ

PDF 生成模板编辑器

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

查看 GitHub

外部签名与二阶段签名 ​

当私钥保存在智能卡、HSM 或远程签名服务中,无法以 PKCS#12 文件的形式交给 SDK 时,可以使用外部签名。CPDFSignatureWorkflow 提供以下三种方式,这些方法都会忽略 CPDFSignatureCreateOptions 的 signer 属性:

场景方法
私钥在 HSM 或远程服务中,由 SDK 组装签名容器signExternalDetachedPKCS7ToPath:options:signer:,配合 CPDFSignExternal
签名设备直接返回完整的 CMS 容器signWithContainerToPath:options:container:,配合 CPDFSignContainer
签名在另一个进程或另一台机器上完成prepareSignatureToPath:options: → 外部签名 → fillSignatureAtPath:options:

ComPDF SDK 不内置 PKCS#11 或云 KMS 签名适配。接入此类签名源时,请在 CPDFSignExternal 的实现中调用相应的 SDK 或服务。

使用外部私钥签名

实现 CPDFSignExternal 协议时请注意:

  • signRequest: 对 SDK 提供的数据签名,并返回原始签名值(RSA 为与模长等长的字节块,ECDSA 为 DER 编码的 SEQUENCE { r, s }),不要返回签名容器。dataKind 说明数据的含义,默认为 CPDFExternalSignDataKindSignedAttributes,即需要签名的 CMS SignedAttributes。
  • CPDFExternalSignResult 中声明的签名算法必须与实际使用的算法一致。例如使用 RSA-PSS 签名却声明为 CPDFExternalSignatureAlgorithmRSAPKCS1v15,生成的签名将无法通过验证。
  • 失败时请返回 failureResultWithErrorCode:errorMessage: 创建的结果,不要抛出异常,否则进程会终止。
  • providesCertificate 返回 YES 时,需通过 signerCertificateDER 与 certificateChainDER 提供签名证书与中间证书。

以下是使用外部私钥签名的示例代码:

swift
// 实现 CPDFSignExternal:私钥运算由 HSM 或远程签名服务完成,私钥不进入 App
class RemoteSigner: NSObject, CPDFSignExternal {
    let certificateDER: Data
    let chainDER: [Data]

    init(certificateDER: Data, chainDER: [Data]) {
        self.certificateDER = certificateDER
        self.chainDER = chainDER
        super.init()
    }

    func signRequest(_ request: CPDFExternalSignRequest) -> CPDFExternalSignResult {
        // 默认情况下 request.data 是 DER 编码的 CMS SignedAttributes,
        // 签名服务需按 request.digestAlgorithm 计算摘要后再签名
        guard let signatureValue = MySigningService.sign(request.data, digestAlgorithm: request.digestAlgorithm) else {
            return CPDFExternalSignResult.failureResultWithErrorCode("remote.unavailable", errorMessage: "The signing service did not respond.")
        }
        // 返回原始签名值(不是容器),并如实声明签名算法
        return CPDFExternalSignResult(signatureValue: signatureValue, algorithm: .rsapkcs1v15)
    }

    func providesCertificate() -> Bool {
        return true
    }

    func signerCertificateDER() -> Data? {
        return certificateDER
    }

    func certificateChainDER() -> [Data]? {
        return chainDER
    }
}

let options = CPDFSignatureCreateOptions()
options.profile = .pAdESB
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."

let remoteSigner = RemoteSigner(certificateDER: certificateDER, chainDER: chainDER)

// signRequest(_:) 在调用线程上被同步调用,请在后台线程执行
DispatchQueue.global(qos: .userInitiated).async {
    let workflow = CPDFSignatureWorkflow(document: document)
    let result = workflow.signExternalDetachedPKCS7(toPath: signedFilePath, options: options, signer: remoteSigner)
    if !result.success {
        // 签名器返回的 errorCode / errorMessage 会原样出现在 errors 中
        for item in result.errors {
            print("\(item.code): \(item.message)")
        }
    }
}
objective-c
// 实现 CPDFSignExternal:私钥运算由 HSM 或远程签名服务完成,私钥不进入 App
@interface RemoteSigner : NSObject <CPDFSignExternal>
@property (nonatomic, copy) NSData *certificateDER;
@property (nonatomic, copy) NSArray<NSData *> *chainDER;
@end

@implementation RemoteSigner

- (CPDFExternalSignResult *)signRequest:(CPDFExternalSignRequest *)request {
    // 默认情况下 request.data 是 DER 编码的 CMS SignedAttributes,
    // 签名服务需按 request.digestAlgorithm 计算摘要后再签名
    NSData *signatureValue = [MySigningService sign:request.data digestAlgorithm:request.digestAlgorithm];
    if (!signatureValue) {
        return [CPDFExternalSignResult failureResultWithErrorCode:@"remote.unavailable" errorMessage:@"The signing service did not respond."];
    }
    // 返回原始签名值(不是容器),并如实声明签名算法
    return [CPDFExternalSignResult resultWithSignatureValue:signatureValue algorithm:CPDFExternalSignatureAlgorithmRSAPKCS1v15];
}

- (BOOL)providesCertificate {
    return YES;
}

- (NSData *)signerCertificateDER {
    return self.certificateDER;
}

- (NSArray<NSData *> *)certificateChainDER {
    return self.chainDER;
}

@end

CPDFSignatureCreateOptions *options = [[CPDFSignatureCreateOptions alloc] init];
options.profile = CPDFSignatureProfilePAdESB;
options.fieldName = @"Signature1";
options.pageIndex = 0;
options.bounds = CGRectMake(28, 420, 150, 70);
options.reason = @"I am the owner of the document.";

RemoteSigner *remoteSigner = [[RemoteSigner alloc] init];
remoteSigner.certificateDER = certificateDER;
remoteSigner.chainDER = chainDER;

// signRequest: 在调用线程上被同步调用,请在后台线程执行
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    CPDFSignatureWorkflow *workflow = [[CPDFSignatureWorkflow alloc] initWithDocument:document];
    CPDFSignatureCreateResult *result = [workflow signExternalDetachedPKCS7ToPath:signedFilePath options:options signer:remoteSigner];
    if (!result.success) {
        // 签名器返回的 errorCode / errorMessage 会原样出现在 errors 中
        for (CPDFValidationReportItem *item in result.errors) {
            NSLog(@"%@: %@", item.code, item.message);
        }
    }
});

使用外部签名容器

当签名设备直接返回完整的分离式 CMS 容器时,实现 CPDFSignContainer 协议并调用 signWithContainerToPath:options:container:。除非确实需要自行控制容器结构,否则建议使用 CPDFSignExternal,由 SDK 构建符合 PAdES 要求的容器。

以下是使用外部签名容器签名的示例代码:

swift
// 签名设备直接返回完整的 CMS 容器时,实现 CPDFSignContainer
class ApplianceContainer: NSObject, CPDFSignContainer {
    func signContainer(with request: CPDFExternalContainerRequest) -> CPDFExternalContainerResult {
        // 将文档摘要交给签名设备,取回 DER 编码的分离式 CMS SignedData
        guard let containerDER = MySigningAppliance.container(for: request.documentDigest, digestAlgorithm: request.digestAlgorithm) else {
            return CPDFExternalContainerResult.failureResultWithErrorCode("appliance.failed", errorMessage: "No container returned.")
        }
        return CPDFExternalContainerResult(containerDER: containerDER)
    }
}

DispatchQueue.global(qos: .userInitiated).async {
    let workflow = CPDFSignatureWorkflow(document: document)
    let result = workflow.signWithContainer(toPath: signedFilePath, options: options, container: ApplianceContainer())
    print("Signed: \(result.success)")
}
objective-c
// 签名设备直接返回完整的 CMS 容器时,实现 CPDFSignContainer
@interface ApplianceContainer : NSObject <CPDFSignContainer>
@end

@implementation ApplianceContainer

- (CPDFExternalContainerResult *)signContainerWithRequest:(CPDFExternalContainerRequest *)request {
    // 将文档摘要交给签名设备,取回 DER 编码的分离式 CMS SignedData
    NSData *containerDER = [MySigningAppliance containerForDigest:request.documentDigest digestAlgorithm:request.digestAlgorithm];
    if (!containerDER) {
        return [CPDFExternalContainerResult failureResultWithErrorCode:@"appliance.failed" errorMessage:@"No container returned."];
    }
    return [CPDFExternalContainerResult resultWithContainerDER:containerDER];
}

@end

dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    CPDFSignatureWorkflow *workflow = [[CPDFSignatureWorkflow alloc] initWithDocument:document];
    CPDFSignatureCreateResult *result = [workflow signWithContainerToPath:signedFilePath options:options container:[[ApplianceContainer alloc] init]];
    NSLog(@"Signed: %d", result.success);
});

二阶段签名

二阶段签名适用于签名需要异步完成,或需要在另一个进程、另一台机器上完成的场景,步骤如下:

  1. 准备阶段:调用 prepareSignatureToPath:options:,写出一个包含真实 ByteRange 与空 /Contents 占位的 PDF,并返回待签名的摘要 documentDigest。

  2. 外部签名:将摘要交给持有私钥的一方签名。

  3. 回填阶段:使用 CPDFDetachedPKCS7ContainerOptions 将签名值组装为分离式 PKCS#7 容器,再调用 fillSignatureAtPath:options: 写入占位。

跨进程执行时,在准备阶段通过 exportSessionMetadata 或 saveSessionMetadataToPath: 保存状态,在回填端通过 importSessionMetadata: 或 loadSessionMetadataFromPath: 恢复。会话元数据是带版本号的 ASCII 文本,可以存入文件、数据库或通过网络传输。准备好的 PDF 在回填之前不能修改;占位大小由 estimatedContentsSize 决定,签名容器超出占位时回填会失败,需要重新执行准备阶段。

以下是准备阶段的示例代码:

swift
let options = CPDFSignatureCreateOptions()
options.fieldName = "Signature1"
options.pageIndex = 0
options.bounds = CGRect(x: 28, y: 420, width: 150, height: 70)
// 为签名容器预留空间;证书链较长或需要内嵌时间戳时请调大
options.estimatedContentsSize = 16384

DispatchQueue.global(qos: .userInitiated).async {
    let workflow = CPDFSignatureWorkflow(document: document)
    let prepared = workflow.prepareSignature(toPath: preparedPath, options: options)
    guard prepared.success, let info = prepared.preparedSignature else {
        for item in prepared.errors {
            print("\(item.code): \(item.message)")
        }
        return
    }
    // 保存会话元数据;在回填之前,preparedPath 指向的 PDF 不能被修改
    _ = info.saveSessionMetadata(toPath: metadataPath)

    // documentDigest 即需要外部私钥签名的摘要
    sendToSigner(info.documentDigest)
}
objective-c
CPDFSignatureCreateOptions *options = [[CPDFSignatureCreateOptions alloc] init];
options.fieldName = @"Signature1";
options.pageIndex = 0;
options.bounds = CGRectMake(28, 420, 150, 70);
// 为签名容器预留空间;证书链较长或需要内嵌时间戳时请调大
options.estimatedContentsSize = 16384;

dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    CPDFSignatureWorkflow *workflow = [[CPDFSignatureWorkflow alloc] initWithDocument:document];
    CPDFPrepareSignatureResult *prepared = [workflow prepareSignatureToPath:preparedPath options:options];
    CPDFPreparedExternalSignatureInfo *info = prepared.preparedSignature;
    if (!prepared.success || !info) {
        for (CPDFValidationReportItem *item in prepared.errors) {
            NSLog(@"%@: %@", item.code, item.message);
        }
        return;
    }
    // 保存会话元数据;在回填之前,preparedPath 指向的 PDF 不能被修改
    [info saveSessionMetadataToPath:metadataPath];

    // documentDigest 即需要外部私钥签名的摘要
    sendToSigner(info.documentDigest);
});

以下是回填阶段的示例代码:

swift
DispatchQueue.global(qos: .userInitiated).async {
    // 可在另一个进程中执行:从会话元数据恢复准备阶段的状态
    let imported = CPDFPreparedExternalSignatureInfo.loadSessionMetadata(fromPath: metadataPath)
    guard imported.success, let info = imported.preparedSignature else {
        print("\(imported.errorCode): \(imported.errorMessage)")
        return
    }

    // 用外部返回的签名值组装分离式 PKCS#7 容器;摘要算法须与准备阶段一致(默认 SHA-256)
    let containerOptions = CPDFDetachedPKCS7ContainerOptions()
    containerOptions.digestAlgorithm = .SHA256
    containerOptions.signerCertificateDER = signerCertificateDER
    containerOptions.certificateChainDER = chainDER
    containerOptions.signatureValue = signatureValue
    containerOptions.signatureAlgorithm = .rsapkcs1v15
    let container = CPDFDetachedPKCS7ContainerOptions.buildDetachedPKCS7Container(with: containerOptions)
    guard container.success else {
        print("\(container.errorCode ?? ""): \(container.errorMessage ?? "")")
        return
    }

    // 回填签名容器,回填阶段不需要打开文档
    let fillOptions = CPDFFillSignatureOptions()
    fillOptions.preparedSignature = info
    fillOptions.signatureContents = container.containerDER
    let result = CPDFSignatureWorkflow().fillSignature(atPath: signedFilePath, options: fillOptions)
    print("Signed: \(result.success), output: \(result.outputPath)")
}
objective-c
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    // 可在另一个进程中执行:从会话元数据恢复准备阶段的状态
    CPDFExternalSigningSessionImportResult *imported = [CPDFPreparedExternalSignatureInfo loadSessionMetadataFromPath:metadataPath];
    if (!imported.success || !imported.preparedSignature) {
        NSLog(@"%@: %@", imported.errorCode, imported.errorMessage);
        return;
    }

    // 用外部返回的签名值组装分离式 PKCS#7 容器;摘要算法须与准备阶段一致(默认 SHA-256)
    CPDFDetachedPKCS7ContainerOptions *containerOptions = [[CPDFDetachedPKCS7ContainerOptions alloc] init];
    containerOptions.digestAlgorithm = CPDFSignatureDigestAlgorithmSHA256;
    containerOptions.signerCertificateDER = signerCertificateDER;
    containerOptions.certificateChainDER = chainDER;
    containerOptions.signatureValue = signatureValue;
    containerOptions.signatureAlgorithm = CPDFExternalSignatureAlgorithmRSAPKCS1v15;
    CPDFExternalContainerResult *container = [CPDFDetachedPKCS7ContainerOptions buildDetachedPKCS7ContainerWithOptions:containerOptions];
    if (!container.success) {
        NSLog(@"%@: %@", container.errorCode, container.errorMessage);
        return;
    }

    // 回填签名容器,回填阶段不需要打开文档
    CPDFFillSignatureOptions *fillOptions = [[CPDFFillSignatureOptions alloc] init];
    fillOptions.preparedSignature = imported.preparedSignature;
    fillOptions.signatureContents = container.containerDER;
    CPDFSignatureWorkflow *workflow = [[CPDFSignatureWorkflow alloc] init];
    CPDFFillSignatureResult *result = [workflow fillSignatureAtPath:signedFilePath options:fillOptions];
    NSLog(@"Signed: %d, output: %@", result.success, result.outputPath);
});

基于内存数据的二阶段签名

ComPDF SDK 3.0.2 起提供的 CPDFDeferredSignSession 同样支持二阶段签名,区别在于 PDF 以 NSData 形式输入和输出,全程不写入文件。prepareWithDocument:sourceData:options: 创建签名域并返回待签名的 hashToSign、准备好的 preparedData 和会话字符串 session;外部签名完成后,调用 fillSession:preparedData:containerDER:verifiesMessageDigest:(签名方返回完整 CMS 容器)或 fillSession:preparedData:signatureValue:options:(签名方只返回原始签名值)回填。对已存在且已设置外观的签名域,可改用 CPDFDocument 的 prepareDeferredSignForSignatureWidget:sourceData:digestAlgorithm:estimatedContentsSize: 完成准备阶段。使用时请注意:

  • 该流程只支持 SHA-256(CPDFSignatureDigestAlgorithmSHA256),传入其他摘要算法会以 sdk.invalid_argument 失败。
  • prepareWithDocument:sourceData:options: 只设置签名域区域,不生成签名外观。
  • hashToSign 已经是摘要值,请确认签名服务不会再次计算摘要;原始签名值回填不支持 RSA-PSS。
  • 准备阶段会修改内存中的 document,之后请勿保存该文档。