上周同事丢给我一份签过字的电子协议,说附件太大发不出去,让我顺手压一下。我手都放到压缩工具上了才想起签名这回事。压完的文件签名还认不认?换个工具会不会不一样?我当时没敢压,让他换了个能传大附件的渠道。这周我专门造了一份测试文件,拿五个常见的压缩和重写工具挨个跑了一遍。结论先放这儿。五个工具压完签名全部失效,其中两个干脆把签名删了,拿验签工具去查连错都不报。
先造一份能验过的签名件
真实合同不能拿来测,我就自己造了一份单页的虚构协议。页面上写着 fictional, for testing only,里面放了一张 1800×1200 的合成图片,整个文件 931,980 字节。证书也是自己用 openssl 建的:一个测试根证书加一个虚构的签名人。签名用 pyHanko 打上去。验签用两个工具,一个是 poppler 的 pdfsig,一个是 pyHanko 自带的校验,两边都只信任我自建的根证书。原文件两边都验过了。pdfsig 报 Signature is Valid,pyHanko 报 INTACT:TRUSTED,UNTOUCHED。
签名覆盖的范围写在签名字典的 ByteRange 里。这份文件是 0 923749 931275 705。翻成人话就是文件开头的一大截加上末尾的一小截。中间空出来的那一截放签名值本身。两段加起来正好到文件末尾。我是中文系转行的,看到这里才算搞懂签名在验什么。它不管页面长什么样,只认这两段字节有没有变过一个比特。
五个工具压完分成两类
同一份 signed.pdf 我依次交给 qpdf(普通重写和对象流打包两种)、Ghostscript 的 /ebook、iLovePDF 推荐档、PDF24 默认档,还有图映的电子书档。图映这一项我用的是图映 ImgIng(imging.cn/)的 PDF 压缩,跑在一台 M4 Mac 的 Chromium 149 里。拿回来的文件我测了两遍:pdfsig 一遍,pyHanko 一遍。结果很整齐地分成了两类。
| 工具 | 输出字节 | 签名字典 | pdfsig | pyHanko |
|---|---|---|---|---|
| qpdf 重写 | 930,748 | 还在 | ByteRange 非法 | INVALID |
| qpdf 对象流 | 930,178 | 还在 | ByteRange 非法 | INVALID |
| Ghostscript /ebook | 39,876 | 整个没了 | 没有签名 | 无签名可验 |
| 图映 电子书档 | 64,903 | 还在 | ByteRange 非法 | INVALID |
| iLovePDF 推荐 | 37,513 | 整个没了 | 没有签名 | 无签名可验 |
| PDF24 默认 | 50,877 | 还在 | ByteRange 非法 | INVALID |
第一类是 qpdf、图映和 PDF24。签名字典还留在文件里,ByteRange 的数字原样照抄,可文件内容已经变了。图映那份最直观。ByteRange 还写着到 931,980 字节为止,实际文件只剩 64,903 字节。pdfsig 直接报 Illegal values in ByteRange array。pyHanko 的说法更具体,原话是 The signature does not cover the entire file,最后判 INVALID。这类文件好歹留着签名的壳,验签时会明明白白报错,至少你知道它坏了。
我又看了每份输出的结构。五个工具的产物都只剩一个 %%EOF,开头的字节也和原文件不一样了。我猜它们都是把整份文件读进来再从头写一遍。删掉没用的对象、把图片换成更小的编码,压缩效果全靠这一步。签名覆盖的那两段字节也就跟着变了。
删掉签名的文件和没签过的一样
第二类是 Ghostscript 和 iLovePDF。它们把整个签名域都删了,iLovePDF 那份连 SigFlags 都没留下。pdfsig 的输出是 does not contain any signatures,pyHanko 那边干脆无签名可验。这句输出很平静,读起来不像出了错,可原文件明明是签过名的。
我觉得这一类比第一类麻烦得多。我翻了 Ghostscript 那份的字节,/ByteRange、/Type /Sig、/SigFlags 一个都搜不到,按结构读也没有表单字典。拿去跟两份从来没签过的公开文件比,一本 1887 年的扫描字典和一份美国地质调查局的火山报告,结果一模一样。也就是说,光拿着压完的文件,谁也看不出它原来签过名。收到的人手里要是没有原件,根本没法发现。有件事我没做,压完的文件我没拿 Acrobat 或福昕打开看签名面板,它们会怎么显示我不知道,我能保证的只有 pdfsig 和 pyHanko 这两个工具的结论。
签名件先压后签
这事说到底不怪哪个工具。压缩就是要动字节,签名就是要锁字节,两者天生冲突。我现在的顺序是先压后签,签好的文件就别再进任何压缩流程,附件太大就换个能传大文件的渠道。签过名的原件我会单独留一份,压出来的东西哪怕看着一切正常,也不拿它当正本。拿到别人发来的 PDF,我会先用 pdfsig 跑一下,命令行就一句 pdfsig 文件名.pdf。报 Signature is Valid 就原样转发,报 does not contain any signatures 的时候,我会多问一句对方手里有没有签过名的版本。