PDF 生成模板编辑器
开源可视化 PDF 生成引擎,支持自定义模板,并提供面向开发者的 API,灵活构建文档生成流程。
不带时间戳的数字签名,其签名时间来自签名设备的本地时钟,既可能被修改,也无法向验证方证明签名发生在证书有效期内。时间戳签名在签名时向时间戳服务器(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 为按 创建数字签名 创建并设置好外观的签名域。
以下是创建带时间戳的数字签名的示例代码:
// 创建时间戳客户端(推荐):请求经由 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)")
}
}// 创建时间戳客户端(推荐):请求经由 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 | 该签名本身是一个文档时间戳。 |
以下是读取签名时间戳来源的示例代码:
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")
}
}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 编码的时间戳令牌。为签名添加时间戳不需要使用该类。
以下是单独获取时间戳令牌的示例代码:
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 ?? "")")
}
}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 信任列表的示例代码:
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)")
}
}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);
}
});