PDF 生成模板编辑器
开源可视化 PDF 生成引擎,支持自定义模板,并提供面向开发者的 API,灵活构建文档生成流程。
当私钥保存在智能卡、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 提供签名证书与中间证书。以下是使用外部私钥签名的示例代码:
// 实现 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)")
}
}
}// 实现 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 要求的容器。
以下是使用外部签名容器签名的示例代码:
// 签名设备直接返回完整的 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)")
}// 签名设备直接返回完整的 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);
});二阶段签名
二阶段签名适用于签名需要异步完成,或需要在另一个进程、另一台机器上完成的场景,步骤如下:
准备阶段:调用 prepareSignatureToPath:options:,写出一个包含真实 ByteRange 与空 /Contents 占位的 PDF,并返回待签名的摘要 documentDigest。
外部签名:将摘要交给持有私钥的一方签名。
回填阶段:使用 CPDFDetachedPKCS7ContainerOptions 将签名值组装为分离式 PKCS#7 容器,再调用 fillSignatureAtPath:options: 写入占位。
跨进程执行时,在准备阶段通过 exportSessionMetadata 或 saveSessionMetadataToPath: 保存状态,在回填端通过 importSessionMetadata: 或 loadSessionMetadataFromPath: 恢复。会话元数据是带版本号的 ASCII 文本,可以存入文件、数据库或通过网络传输。准备好的 PDF 在回填之前不能修改;占位大小由 estimatedContentsSize 决定,签名容器超出占位时回填会失败,需要重新执行准备阶段。
以下是准备阶段的示例代码:
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)
}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);
});以下是回填阶段的示例代码:
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)")
}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: 完成准备阶段。使用时请注意:
CPDFSignatureDigestAlgorithmSHA256),传入其他摘要算法会以 sdk.invalid_argument 失败。prepareWithDocument:sourceData:options: 只设置签名域区域,不生成签名外观。hashToSign 已经是摘要值,请确认签名服务不会再次计算摘要;原始签名值回填不支持 RSA-PSS。document,之后请勿保存该文档。