#pdf-file #signed #signature #pdf-document #modification #verify #validating

trust_pdf

验证已签名的PDF与原始文档的一致性,检查隐秘的修改并验证签名

1个不稳定版本

0.1.0 2024年8月25日

#3#signed

MIT 许可证

155KB
799

Trust PDF

一个库,用于验证签名的PDF是否有效,并且与未签名的原始文档匹配。

问题

想象一下这个场景:你生成一个包含你的条款和条件的PDF文件,并将其发送给客户或业务伙伴进行签字。对方签字后将其上传到你的服务器。

根据你对数字文档和签名的了解,你的服务器应该能够轻松地验证签名的有效性,并确认提交的签字文档确实是你发送的,对吗?

错误!

判断你收到的已签名PDF文件的内容是否与未签名的版本一致是非常困难的!似乎没有建立任何真正经过测试的协议或标准来做到这一点,而且PDF标准的制定者似乎也没有考虑这个问题。

这在某种程度上是可以理解的,因为PDF标准假设有一个真实的GUI,有真实的人阅读文件,并且在签字并发送回文件后,这个人会再次阅读它,并查看嵌入的PDF视频,执行嵌入的JavaScript。但是,由于你正在自动化这个过程,你应该处理的是包含嵌入PDF的PKCS #7文件,而不是反过来(如果是这样,任务确实很简单)。

但在现实世界中,我们有政府运行在线工具,为公民生成具有法律约束力的数字签名,唯一的输出是签名PDF,它已经成为数字签名的实际标准,我们必须找到一种自动处理它们的方法。

这个库是我尝试解决这个问题的尝试,只要签名以特定的和受限制的方式添加,它就会起作用。

难度

问题是PDF是一个复杂的格式,有很多组成部分,嵌入的签名只是其内部结构中的另一个对象。事实上,PDF将其视为一个表单字段(有时是一个注释,用户认为这是实际的签名,但实际上不是)。因此,要包含一个签名,你必须以非平凡的方式更改PDF文件,有无限的选择和自由度。

据说Adobe Acrobat会将所有PDF对象反序列化到内存中,添加新的签名对象,然后以全新的方式重新序列化它。因此,任何简单的字节到字节的比较都失去了意义。

然而,在修改PDF文件的一种替代方法中,即增量更新中,出现了一丝希望。我测试过的PDF签名者实际上会使用这种方法,如果原始PDF文件正确设置了/SigFlags,即使是Acrobat也会使用它。它的工作原理是直接使用原始PDF的字节,并附加新内容。原始文件成为新文件的序言。

有人可能会想,只需检查原始文件是否为签名文件的序言,以确保未对其进行修改,但这将是错误的。增量更新可以完全改变PDF文件的所有内容。它实际上必须为PDF对象提供新的索引,这些索引可能可选地使用原始文件中的对象,但这不是必须的。以这种方式包含的签名不仅会签名原始文件范围,还会签名一切,包括增量更新本身,因此您不能让律师争辩说所签的是前缀文档,而不是修改后的文档。

策略

为了解决这个问题,这个库列出了在文档中添加签名可以执行的操作

  • 已签名的文档必须以未签名的文档开始;
  • 每个添加的签名都必须有自己的增量更新;
  • 每个增量更新都必须保持前一个增量中所有未修改的对象,除了严格必要的更改对象:/Catalog/AcroForm和最多一个/Page
  • /Catalog中,只有当缺少时,可以添加/AcroForm和其他几个字段;
  • /AcroForm中,必须添加一个额外的/FT /Sig表单字段;
  • 最多在一页中,可以添加一个/Subtype /Widget注释;
  • 签名必须是有效的,并覆盖从文档开始到其增量更新结束的部分;
  • 最终签名必须覆盖整个文档;
  • 可能还有一些其他限制。

我希望这足以防止对渲染的PDF进行任何内容修改,除了签名注释,这可能会被制作并放置在某个地方以有意义地改变内容的解释。为了减轻这一点,这个库还将返回每个签名注释的页面和矩形,以便应用程序进行进一步的授权。

用法

以下是如何验证一个已签名的PDF文件与一个未签名的参考文件的最小完整程序。

// Load the PDF files
let reference_file_i_wrote_and_trust =
    std::fs::read("test_data/valid_modification/unsigned.pdf").unwrap();
let signed_file_i_received =
    std::fs::read("test_data/valid_modification/signed-visible.pdf").unwrap();

// Create the OpenSSL verifier
let trust_store = trust_pdf::openssl::load_ca_bundle_from_dir("test_data/trusted_CAs")
    .unwrap()
    .build();
let intermediaries = openssl::stack::Stack::new().unwrap();
let digital_signature_verifier =
    trust_pdf::openssl::OpenSslVerifier::new(trust_store, intermediaries);

// Verify the signed file with the original as reference
if trust_pdf::verify_from_reference(
    reference_file_i_wrote_and_trust,
    signed_file_i_received,
    &digital_signature_verifier,
)
.is_ok()
{
    println!("Everything looks alright!");
} else {
    println!("Someone is trying to trick you!");
}

可选功能

  • openssl:这个功能使用openssl crate提供对Pkcs7Verifier trait的实现,这是验证PDF文件中嵌入的PKCS #7签名所需的。但是,您可以提供自己的实现,因此对OpenSSL的依赖是可选的。

安全性

这是一个尽最大努力的尝试,目前它是由一个人在他的假期和业余时间编写的,没有经过独立的审查,没有实际使用,几乎没有任何测试。同时,注意许可证中的免责声明。

话虽如此,如果您真的想用它来保护某些内容,那么我建议,作为一个备份计划,在您的未签名文档中放入法律语言,说明文档中的任何数字签名仅适用于PDF文档的第一个、未修改的版本,并且通过增量更新引入的修改不具有法律约束力。请与您的律师评估这种情况在法庭上站得住脚的可能性。

最后,万一有人感兴趣资助它,这个项目将受益于专业的安全审计和漏洞赏金计划。

许可证

本项目采用MIT许可证发布。

依赖项

~20MB
~316K SLoC