Skip to content
Demo示例FAQ

PDF 生成模板编辑器

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

查看 GitHub

时间戳签名 ​

不带时间戳的数字签名,其签名时间来自签名设备的本地时钟,既可能被修改,也无法向验证方证明签名发生在证书有效期内。时间戳签名在签名时向时间戳服务器(TSA,Time Stamp Authority)请求一个符合 RFC 3161 标准的时间戳令牌并嵌入签名,由可信第三方证明该签名在此时间之前已经存在。即使签名证书之后过期,只要时间戳可信,验证方仍可确认签名是在证书有效期内完成的。

注: 本节及 签名工作流与验证报告、PAdES 与长期验证(LTV)和外部签名与二阶段签名 中的时间戳、签名工作流、PAdES 与撤销信息获取方法均为同步调用,执行期间会读写整个文件,并可能发起网络请求。请勿在主线程调用,应在后台队列中执行,完成后再回到主线程更新界面。

选择时间戳客户端

ComPDF SDK 通过 CPDFTSAClient 协议访问 TSA,并提供两种实现:

类说明
CPDFURLSessionTSAClient推荐使用。基于 NSURLSession 发送请求,遵循 App Transport Security(ATS)、系统代理与 VPN 配置;可替换 session 属性以实现证书固定或客户端证书认证。
CPDFHttpTSAClient不建议在上架的 App 中使用。由核心库内部发起阻塞式 HTTP 请求,绕过 NSURLSession,因此不受 ATS、系统代理及 App 自定义证书固定的约束。

如需通过自有网络层或离线服务获取时间戳,也可以自行实现 CPDFTSAClient 协议。

注: CPDFTSAClient 的 requestTimestamp: 会在 SDK 内部任务队列上被同步调用。自定义实现中请同步等待结果,且不要在其中向主队列 dispatch_sync,否则签名调用会死锁。

签名时添加时间戳

在 创建数字签名 的签名流程中,将 writeSignatureToURL:withWidget:PKCS12Cert:password:location:reason:isSetSignAp:permissions: 替换为带 tsaClient: 参数的版本,即可在写入签名的同时嵌入时间戳。tsaClient 传入 nil 时,与不带时间戳的方法行为一致。示例中的 widgetAnnotation 为按 创建数字签名 创建并设置好外观的签名域。

以下是创建带时间戳的数字签名的示例代码:

swift
// 创建时间戳客户端(推荐):请求经由 NSURLSession 发送,遵循 ATS、系统代理与 VPN 设置
let tsaClient = CPDFURLSessionTSAClient(url: URL(string: "https://tsa.example.com/tsr")!)
tsaClient.timeoutInterval = 30
tsaClient.retryCount = 1

// 签名与时间戳请求都是同步调用,请在后台线程执行
DispatchQueue.global(qos: .userInitiated).async {
    let isSuccess = document.writeSignature(to: URL(fileURLWithPath: writeFilePath),
                                            withWidget: widgetAnnotation,
                                            pkcs12Cert: certificatePath,
                                            password: "123",
                                            location: "<your signing location here>",
                                            reason: "I am the owner of the document.",
                                            isSetSignAp: false,
                                            permissions: .none,
                                            tsaClient: tsaClient)
    DispatchQueue.main.async {
        // 回到主线程更新界面
        print("Signed with timestamp: \(isSuccess)")
    }
}
objective-c
// 创建时间戳客户端(推荐):请求经由 NSURLSession 发送,遵循 ATS、系统代理与 VPN 设置
CPDFURLSessionTSAClient *tsaClient = [[CPDFURLSessionTSAClient alloc] initWithURL:[NSURL URLWithString:@"https://tsa.example.com/tsr"]];
tsaClient.timeoutInterval = 30;
tsaClient.retryCount = 1;

// 签名与时间戳请求都是同步调用,请在后台线程执行
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    BOOL isSuccess = [document writeSignatureToURL:[NSURL fileURLWithPath:writeFilePath]
                                        withWidget:widgetAnnotation
                                        PKCS12Cert:certificatePath
                                          password:@"123"
                                          location:@"<your signing location here>"
                                            reason:@"I am the owner of the document."
                                       isSetSignAp:NO
                                       permissions:CPDFSignaturePermissionsNone
                                         tsaClient:tsaClient];
    dispatch_async(dispatch_get_main_queue(), ^{
        // 回到主线程更新界面
        NSLog(@"Signed with timestamp: %d", isSuccess);
    });
});

读取签名的时间戳来源

CPDFSignature 的 timestampSource 属性表示签名时间的来源,hasEmbeddedTimestamp 表示签名是否内嵌 RFC 3161 时间戳令牌。

值说明
CPDFSignatureTimestampSourceNone签名不含时间信息。
CPDFSignatureTimestampSourceLocalClock签名时间来自签名设备的本地时钟。
CPDFSignatureTimestampSourceEmbedded签名内嵌 RFC 3161 时间戳令牌。
CPDFSignatureTimestampSourceDocument该签名本身是一个文档时间戳。

以下是读取签名时间戳来源的示例代码:

swift
let document = CPDFDocument(url: URL(fileURLWithPath: signedFilePath))

for signature in document?.signatures() ?? [] {
    switch signature.timestampSource {
    case .embedded:
        // 签名内嵌 RFC 3161 时间戳,签名时间由 TSA 证明
        print("\(signature.name ?? ""): embedded timestamp, token = \(signature.hasEmbeddedTimestamp)")
    case .document:
        // 该签名本身是一个文档时间戳
        print("\(signature.name ?? ""): document timestamp")
    case .localClock:
        // 签名时间来自签名设备的本地时钟,没有第三方证明
        print("\(signature.name ?? ""): local clock")
    default:
        // 签名不含时间信息
        print("\(signature.name ?? ""): no timestamp")
    }
}
objective-c
CPDFDocument *document = [[CPDFDocument alloc] initWithURL:[NSURL fileURLWithPath:signedFilePath]];

for (CPDFSignature *signature in [document signatures]) {
    switch (signature.timestampSource) {
        case CPDFSignatureTimestampSourceEmbedded:
            // 签名内嵌 RFC 3161 时间戳,签名时间由 TSA 证明
            NSLog(@"%@: embedded timestamp, token = %d", signature.name, signature.hasEmbeddedTimestamp);
            break;
        case CPDFSignatureTimestampSourceDocument:
            // 该签名本身是一个文档时间戳
            NSLog(@"%@: document timestamp", signature.name);
            break;
        case CPDFSignatureTimestampSourceLocalClock:
            // 签名时间来自签名设备的本地时钟,没有第三方证明
            NSLog(@"%@: local clock", signature.name);
            break;
        default:
            // 签名不含时间信息
            NSLog(@"%@: no timestamp", signature.name);
            break;
    }
}

如需验证时间戳本身(摘要是否匹配、令牌签名是否有效、TSA 证书链是否可信),请参考 签名工作流与验证报告 中 CPDFValidationCategoryTimestamp 类别的检查项。

单独获取时间戳令牌

如需为任意数据获取时间戳(例如为一段分离的哈希加盖时间戳,或在签名前测试 TSA 地址是否可用),可使用 CPDFTimestampService。它负责计算摘要、构造 RFC 3161 请求并通过 TSA 客户端发送,返回 DER 编码的时间戳令牌。为签名添加时间戳不需要使用该类。

以下是单独获取时间戳令牌的示例代码:

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

let options = CPDFTimestampOptions()
options.digestAlgorithm = .SHA256
options.includeNonce = true

DispatchQueue.global(qos: .userInitiated).async {
    // 计算摘要并向 TSA 请求时间戳令牌(同步网络请求)
    let response = service.timestampData(fileData, options: options)
    if response.isSuccess, let token = response.tokenDER {
        // token 为 DER 编码的 RFC 3161 TimeStampToken,可单独保存或存档
        print("Token size: \(token.count), genTime: \(response.genTime ?? "")")
    } else {
        // errorCode 为稳定的错误标识,例如 timestamp.transport.*
        print("Timestamp failed: \(response.errorCode ?? "") \(response.errorMessage ?? "")")
    }
}
objective-c
CPDFURLSessionTSAClient *tsaClient = [[CPDFURLSessionTSAClient alloc] initWithURL:[NSURL URLWithString:@"https://tsa.example.com/tsr"]];
CPDFTimestampService *service = [[CPDFTimestampService alloc] initWithTSAClient:tsaClient];

CPDFTimestampOptions *options = [[CPDFTimestampOptions alloc] init];
options.digestAlgorithm = CPDFSignatureDigestAlgorithmSHA256;
options.includeNonce = YES;

dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
    // 计算摘要并向 TSA 请求时间戳令牌(同步网络请求)
    CPDFTimestampResponse *response = [service timestampData:fileData options:options];
    if (response.isSuccess) {
        // tokenDER 为 DER 编码的 RFC 3161 TimeStampToken,可单独保存或存档
        NSLog(@"Token size: %lu, genTime: %@", (unsigned long)response.tokenDER.length, response.genTime);
    } else {
        // errorCode 为稳定的错误标识,例如 timestamp.transport.*
        NSLog(@"Timestamp failed: %@ %@", response.errorCode, response.errorMessage);
    }
});

加载 AATL 与 EUTL 信任列表

ComPDF SDK 支持加载 Adobe 认可信任列表(AATL)与欧盟可信列表(EUTL)。首次加载时调用 loadAATLAndEUTLWithAATLPath:EUTLPath:cachePath: 解析信任列表文件,并将其中的信任证书 SHA-1 列表写入缓存;之后启动时调用 loadAATLAndEUTLFromCachePath: 直接从缓存加载,无需重复解析。信任列表文件更新后,请重新调用前者刷新缓存。SDK 不随附信任列表文件,请自行从 Adobe 与欧盟官方渠道获取。

以下是加载 AATL 与 EUTL 信任列表的示例代码:

swift
let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0] + "/TrustList"

DispatchQueue.global(qos: .utility).async {
    // 优先从缓存加载;缓存尚未生成时解析原始信任列表并写入缓存
    if !CPDFSignature.loadAATLAndEUTL(fromCachePath: cachePath) {
        let isLoaded = CPDFSignature.loadAATLAndEUTL(withAATLPath: aatlPath, eutlPath: eutlPath, cachePath: cachePath)
        print("Trust lists loaded: \(isLoaded)")
    }
}
objective-c
NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"TrustList"];

dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
    // 优先从缓存加载;缓存尚未生成时解析原始信任列表并写入缓存
    if (![CPDFSignature loadAATLAndEUTLFromCachePath:cachePath]) {
        BOOL isLoaded = [CPDFSignature loadAATLAndEUTLWithAATLPath:aatlPath EUTLPath:eutlPath cachePath:cachePath];
        NSLog(@"Trust lists loaded: %d", isLoaded);
    }
});