Skip to content
Demo示例FAQ

PDF 生成模板编辑器

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

查看 GitHub

PAdES 与长期验证(LTV) ​

PAdES(PDF Advanced Electronic Signatures,ETSI EN 319 142)是欧盟 eIDAS 法规认可的 PDF 高级电子签名格式。CPDFPadesSigner 可以生成以下四个基线级别的签名,每个级别都包含前一级别的全部内容:

级别说明输出文件
CPDFPadesBaselineProfileB基础签名,包含签名证书引用。signedPath
CPDFPadesBaselineProfileTBaseline-B 加可信签名时间戳,需要 TSA。signedPath
CPDFPadesBaselineProfileLTBaseline-T 加写入 DSS / VRI 的证书链与 OCSP / CRL 撤销信息,签名证书过期后仍可验证。signedPath、ltvPath
CPDFPadesBaselineProfileLTABaseline-LT 加覆盖长期验证材料的文档时间戳,可定期续期。signedPath、ltvPath、ltaPath

每个级别写入单独的文件,各路径不能相同。最终文档是最后写入的文件(LTA 为 ltaPath,LT 为 ltvPath,其余为 signedPath),之前的文件是中间状态,可作为审计记录保留,也可删除。

注: CPDFPadesSigner 与 CPDFLtvService 的实例持有核心库状态,同一实例只能在一个线程中使用,即使处理的是不同文档也是如此。

生成 PAdES-B / PAdES-T 签名

CPDFPadesCreateOptions 的 signatureOptions 描述内层签名(签名身份、签名域、外观等),其 profile 会根据基线级别自动设置,无需填写。T 及以上级别的签名时间戳来自 signatureOptions 的 tsaClient。

在返回的 CPDFPadesCreateResult 中,signatureResult.success 表示签名已写入,signatureResult.subFilter 为 ETSI.CAdES.detached 表示已生成 PAdES 签名;success 表示请求的级别已完整达成。

以下是生成 PAdES-T 签名的示例代码:

swift
let tsaClient = CPDFURLSessionTSAClient(url: URL(string: "https://tsa.example.com/tsr")!)

// 内层签名:签名身份、签名域与签名时间戳
let signatureOptions = CPDFSignatureCreateOptions()
signatureOptions.signer = CPDFSigningIdentity(pkcs12Path: certificatePath, password: "123")
signatureOptions.tsaClient = tsaClient
signatureOptions.fieldName = "Signature1"
signatureOptions.pageIndex = 0
signatureOptions.bounds = CGRect(x: 28, y: 420, width: 150, height: 70)

let padesOptions = CPDFPadesCreateOptions()
padesOptions.profile = .T
padesOptions.signatureOptions = signatureOptions
padesOptions.trustedCertPath = trustedRootsPath

DispatchQueue.global(qos: .userInitiated).async {
    let signer = CPDFPadesSigner(document: document)
    let result = signer.sign(toPath: signedPath, ltvPath: nil, ltaPath: nil, options: padesOptions)
    if result.signatureResult.success {
        // ETSI.CAdES.detached 表示已生成 PAdES 签名
        print("Signed: \(result.signedPath), subFilter: \(result.signatureResult.subFilter)")
    }
    for item in result.errors {
        print("\(item.code): \(item.message)")
    }
}
objective-c
CPDFURLSessionTSAClient *tsaClient = [[CPDFURLSessionTSAClient alloc] initWithURL:[NSURL URLWithString:@"https://tsa.example.com/tsr"]];

// 内层签名:签名身份、签名域与签名时间戳
CPDFSignatureCreateOptions *signatureOptions = [[CPDFSignatureCreateOptions alloc] init];
signatureOptions.signer = [CPDFSigningIdentity identityWithPKCS12Path:certificatePath password:@"123"];
signatureOptions.tsaClient = tsaClient;
signatureOptions.fieldName = @"Signature1";
signatureOptions.pageIndex = 0;
signatureOptions.bounds = CGRectMake(28, 420, 150, 70);

CPDFPadesCreateOptions *padesOptions = [[CPDFPadesCreateOptions alloc] init];
padesOptions.profile = CPDFPadesBaselineProfileT;
padesOptions.signatureOptions = signatureOptions;
padesOptions.trustedCertPath = trustedRootsPath;

dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    CPDFPadesSigner *signer = [[CPDFPadesSigner alloc] initWithDocument:document];
    CPDFPadesCreateResult *result = [signer signToPath:signedPath ltvPath:nil ltaPath:nil options:padesOptions];
    if (result.signatureResult.success) {
        // ETSI.CAdES.detached 表示已生成 PAdES 签名
        NSLog(@"Signed: %@, subFilter: %@", result.signedPath, result.signatureResult.subFilter);
    }
    for (CPDFValidationReportItem *item in result.errors) {
        NSLog(@"%@: %@", item.code, item.message);
    }
});

在 iOS 上写入长期验证材料(LT / LTA)

LT 与 LTA 需要签名证书链及 OCSP / CRL 撤销信息。CPDFPadesCreateOptions 的 autoCollectLTVValidationInfo 默认为 YES,此时由核心库自行发起同步 HTTP 请求下载撤销信息;该请求不经过 NSURLSession,不受 ATS、系统代理与超时设置的约束。在 iOS 上,建议由 App 自行获取撤销信息,再通过 ltvOptions 传入,步骤如下:

  1. 使用 CPDFPadesSigner 生成 PAdES-T 签名(见上文)。

  2. 使用 CPDFCertificateChainBuilder 在本地构建签名证书链,并通过 CPDFRevocationService 的 discoverSourcesInCertificateDER: 读取证书中的 OCSP / CRL 地址。

  3. 使用 App 自己的 NSURLSession 下载 CRL 或 OCSP 响应,再调用 CPDFRevocationService 的 validateCRL: 或 validateOCSPResponse: 在本地验证,这两个方法不会联网。

  4. 将证书链与撤销信息放入 CPDFLtvValidationInfo,调用 prolongLTVFromPath:ltvPath:ltaPath:options: 写入 DSS / VRI。appendDocumentTimestamp 为 YES 时会同时追加文档时间戳,得到 LTA 级别的文档。

CPDFLtvValidationInfo 的 signatureContentsDER 必须是签名 /Contents 的完整原始字节(包含预留的填充字节),而不是裁剪后的 CMS 容器,否则写入的 VRI 条目无法被验证程序找到。请使用 CPDFSignatureCoverageChecker 的 rawSignatureContentsForSignature:atPath: 获取。

以下是在 iOS 上写入长期验证材料并追加文档时间戳的示例代码(以 CRL 为例):

swift
let tsaClient = CPDFURLSessionTSAClient(url: URL(string: "https://tsa.example.com/tsr")!)

DispatchQueue.global(qos: .userInitiated).async {
    // 1. 从已签名文件中取出签名及其证书(签名者证书位于数组末尾)
    guard let signedDocument = CPDFDocument(url: URL(fileURLWithPath: signedPath)),
          let signature = signedDocument.signatures()?.first,
          let certificates = signature.signers?.first?.certificates,
          let leafDER = certificates.last?.derData() else { return }

    // 2. 在本地构建证书链(不联网),链按叶证书在前排列
    let store = CPDFCertificateStore()
    store.trustedCertificatePath = trustedRootsPath
    for certificate in certificates.dropLast() {
        if let der = certificate.derData() {
            store.addIntermediateCertificateDER(der)
        }
    }
    let chain = CPDFCertificateChainBuilder().buildChain(forCertificateDER: leafDER, store: store, allowPartialChain: true)
    guard chain.success, chain.chainDER.count >= 2 else { return }

    // 3. 读取证书中的 CRL 分发点,用 App 自己的 URLSession 下载 CRL,再在本地验证
    let revocation = CPDFRevocationService()
    let sources = revocation.discoverSources(inCertificateDER: leafDER)
    guard let crlURL = sources.crlURLs.first.flatMap({ URL(string: $0) }),
          let crlData = downloadData(from: crlURL) else { return }

    let input = CPDFRevocationCheckInput()
    input.subjectCertificateDER = leafDER
    input.issuerCertificateDER = chain.chainDER[1]
    input.crlDER = crlData
    let check = revocation.validateCRL(input)
    guard check.success, check.status == .good else { return }

    // 4. 组装 LTV 材料;签名的 /Contents 必须是包含填充字节的原始内容
    let info = CPDFLtvValidationInfo()
    info.signatureContentsDER = CPDFSignatureCoverageChecker().rawSignatureContents(for: signature, atPath: signedPath)
    info.certificatesDER = chain.chainDER
    info.crlsDER = [crlData]

    let ltvOptions = CPDFLtvAddOptions()
    ltvOptions.validationInfo = [info]

    // 5. 写入 DSS / VRI(LT),并追加文档时间戳(LTA)
    let prolongOptions = CPDFPadesProlongOptions()
    prolongOptions.ltvOptions = ltvOptions
    prolongOptions.autoCollectLTVValidationInfo = false
    prolongOptions.appendDocumentTimestamp = true
    prolongOptions.ltaTSAClient = tsaClient
    prolongOptions.trustedCertPath = trustedRootsPath

    let result = CPDFPadesSigner().prolongLTV(fromPath: signedPath, ltvPath: ltvPath, ltaPath: ltaPath, options: prolongOptions)
    print("LTV written: \(result.ltvResult.success), LTA file: \(result.ltaPath)")
}
objective-c
CPDFURLSessionTSAClient *tsaClient = [[CPDFURLSessionTSAClient alloc] initWithURL:[NSURL URLWithString:@"https://tsa.example.com/tsr"]];

dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    // 1. 从已签名文件中取出签名及其证书(签名者证书位于数组末尾)
    CPDFDocument *signedDocument = [[CPDFDocument alloc] initWithURL:[NSURL fileURLWithPath:signedPath]];
    CPDFSignature *signature = [signedDocument signatures].firstObject;
    NSArray<CPDFSignatureCertificate *> *certificates = signature.signers.firstObject.certificates;
    NSData *leafDER = [certificates.lastObject DERData];
    if (!leafDER) {
        return;
    }

    // 2. 在本地构建证书链(不联网),链按叶证书在前排列
    CPDFCertificateStore *store = [[CPDFCertificateStore alloc] init];
    store.trustedCertificatePath = trustedRootsPath;
    for (NSUInteger i = 0; i + 1 < certificates.count; i++) {
        NSData *der = [certificates[i] DERData];
        if (der) {
            [store addIntermediateCertificateDER:der];
        }
    }
    CPDFCertificateChainBuildResult *chain = [[[CPDFCertificateChainBuilder alloc] init] buildChainForCertificateDER:leafDER store:store allowPartialChain:YES];
    if (!chain.success || chain.chainDER.count < 2) {
        return;
    }

    // 3. 读取证书中的 CRL 分发点,用 App 自己的 URLSession 下载 CRL,再在本地验证
    CPDFRevocationService *revocation = [[CPDFRevocationService alloc] init];
    CPDFRevocationSources *sources = [revocation discoverSourcesInCertificateDER:leafDER];
    NSString *crlURLString = sources.crlURLs.firstObject;
    NSData *crlData = crlURLString ? downloadData([NSURL URLWithString:crlURLString]) : nil;
    if (!crlData) {
        return;
    }

    CPDFRevocationCheckInput *input = [[CPDFRevocationCheckInput alloc] init];
    input.subjectCertificateDER = leafDER;
    input.issuerCertificateDER = chain.chainDER[1];
    input.crlDER = crlData;
    CPDFRevocationCheckResult *check = [revocation validateCRL:input];
    if (!check.success || check.status != CPDFRevocationStatusGood) {
        return;
    }

    // 4. 组装 LTV 材料;签名的 /Contents 必须是包含填充字节的原始内容
    CPDFLtvValidationInfo *info = [[CPDFLtvValidationInfo alloc] init];
    info.signatureContentsDER = [[[CPDFSignatureCoverageChecker alloc] init] rawSignatureContentsForSignature:signature atPath:signedPath];
    info.certificatesDER = chain.chainDER;
    info.crlsDER = @[crlData];

    CPDFLtvAddOptions *ltvOptions = [[CPDFLtvAddOptions alloc] init];
    ltvOptions.validationInfo = @[info];

    // 5. 写入 DSS / VRI(LT),并追加文档时间戳(LTA)
    CPDFPadesProlongOptions *prolongOptions = [[CPDFPadesProlongOptions alloc] init];
    prolongOptions.ltvOptions = ltvOptions;
    prolongOptions.autoCollectLTVValidationInfo = NO;
    prolongOptions.appendDocumentTimestamp = YES;
    prolongOptions.ltaTSAClient = tsaClient;
    prolongOptions.trustedCertPath = trustedRootsPath;

    CPDFPadesSigner *signer = [[CPDFPadesSigner alloc] init];
    CPDFPadesProlongResult *result = [signer prolongLTVFromPath:signedPath ltvPath:ltvPath ltaPath:ltaPath options:prolongOptions];
    NSLog(@"LTV written: %d, LTA file: %@", result.ltvResult.success, result.ltaPath);
});

如果您的使用场景可以接受核心库直接联网,也可以将 profile 设为 CPDFPadesBaselineProfileLT 或 CPDFPadesBaselineProfileLTA,同时传入 ltvPath、ltaPath 并设置 ltaTSAClient,由 signToPath:ltvPath:ltaPath:options: 一次完成签名与长期验证材料的收集。

注: CPDFRevocationService 的 fetchOCSPResponse:、fetchCRL: 与 fetchAndValidateBestAvailable: 同样由核心库通过 OpenSSL 发起阻塞式网络请求,绕过 ATS 与系统代理,不建议在 iOS 上使用。

为存档文档续期

prolongLTVFromPath:ltvPath:ltaPath:options: 也用于为已签名的存档文档续期:在已有撤销信息或文档时间戳的证书过期之前,重新获取最新的撤销信息并追加新的文档时间戳。该方法不需要打开文档,也适用于其他软件签署的 PDF。注意每次追加都会重新嵌入 DSS 中已有的证书与 CRL,文件会随之增大。

检查文档的 LTV 状态

CPDFLtvService 只读取文档中的 DSS / VRI,不会联网。inspectDSSInDocument: 汇总 DSS 中的内容;assessReadinessOfDocument: 判断文档中的每个签名是否都具备完整的长期验证材料,未就绪时,missingMaterials 会列出缺少的材料,可据此决定重新获取撤销信息还是重新签名。

以下是检查文档 LTV 状态的示例代码:

swift
DispatchQueue.global(qos: .utility).async {
    guard let document = CPDFDocument(url: URL(fileURLWithPath: ltaPath)) else { return }

    // CPDFLtvService 只读取文档结构,不联网;每个线程使用独立实例
    let service = CPDFLtvService()

    let dss = service.inspectDSS(in: document)
    if dss.success {
        print("DSS: \(dss.dssInfo.hasDSS), VRI entries: \(dss.dssInfo.vriEntriesCount), CRLs: \(dss.dssInfo.crlsCount)")
    }

    let readiness = service.assessReadiness(of: document)
    if readiness.success {
        let info = readiness.readinessInfo
        print("LTV ready: \(info.ltvReady), hint: \(info.profileHint)")
        // 未就绪时,missingMaterials 说明缺少哪些材料
        for missing in info.missingMaterials {
            print("Missing: \(missing)")
        }
    }
}
objective-c
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
    CPDFDocument *document = [[CPDFDocument alloc] initWithURL:[NSURL fileURLWithPath:ltaPath]];

    // CPDFLtvService 只读取文档结构,不联网;每个线程使用独立实例
    CPDFLtvService *service = [[CPDFLtvService alloc] init];

    CPDFLtvInspectResult *dss = [service inspectDSSInDocument:document];
    if (dss.success) {
        NSLog(@"DSS: %d, VRI entries: %ld, CRLs: %ld", dss.dssInfo.hasDSS, (long)dss.dssInfo.vriEntriesCount, (long)dss.dssInfo.crlsCount);
    }

    CPDFLtvReadinessResult *readiness = [service assessReadinessOfDocument:document];
    if (readiness.success) {
        CPDFLtvReadinessInfo *info = readiness.readinessInfo;
        NSLog(@"LTV ready: %d, hint: %@", info.ltvReady, info.profileHint);
        // 未就绪时,missingMaterials 说明缺少哪些材料
        for (NSString *missing in info.missingMaterials) {
            NSLog(@"Missing: %@", missing);
        }
    }
});