抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

!! 本文代码已上传Github 使用GPL-3.0协议开源 !!

有天我打开网上的PDF文件时,看到这样一行提示:

此文档已经过数字签名

我大惊,我平时会用GPG对文件进行签名,但得到的也只是一个单独的签名文件。难道,GPG创建的签名可以嵌入PDF?我想起以前在Windows上用PDF Reader时见过“签名”菜单,但是那看起来只是手绘了个图形贴上去,看上去好像也不高级,背后好像也没有特别的算法撑腰。这个“数字签名”却有证书,有颁发日期,到期时间,SHA-256指纹,和签名算法,看起来很专业,就像访问HTTPS站点时的SSL证书。

我决定要给自己的PDF也加上这样的数字签名,最好能有个很有信服力的图章,上面写着“signed by Pengbo”。

我在网上展开搜索,却发现linux下,好像少有这样的进行签名的工具,KDE的PDF查看器Okular有这个功能,但它找不到我的证书(恼

那就只能另寻办法啦。

我在网上搜查到的信息告诉我PDF签名需要S/MIME证书,一般由权威CA颁发,价格在……

等等!我签名自己的文件还要付钱?不可能!

CA创建S/MIME证书总要用一些工具吧,我能不能用这样的工具创建自签名证书

这一工具就是openssl,自己建过网站的人应该对它不陌生,处理HTTPS时多少会碰到。OpenSSL 是一个用于实现安全通信的软件包,它由一组密码学函数库组成。它的主要目标是通过使用公开的密码学算法来保护数据的机密性、完整性和身份验证。它支持对称加密、非对称加密、数字签名、证书管理等功能。

背景知识了解的差不多了,动手吧!

生成

如果你没有openssl这个软件包,请安装它。

首先生成自己的私钥(不知道什么意思的话可以查查非对称加密

1
openssl genrsa -aes128 -out myself.key 2048

-aes128 表示用AES128算法加密私钥,可选的还有-aes192,-aes2562048表示私钥长度,越长越安全,但也越慢,我一般用4096位的。

1
openssl req -new -days 365 -key myself.key -out myself.csr

这个命令创建证书请求,正常情况下,你应该把这个证书请求发送给CA的工作人员,但今天我们要自己签发,这个待会儿再说。

-days 表示证书有效期,我太懒了,这辈子都不想换,疯狂加0,现在过期时间在34世纪。(这辈子用不完了

别学我,这样不太“正规”,我给你的示例,有效期是1年。

你可能会问:有效期只有一年,那一年以后数字签名是不是就不能验证了呢?别怕,后面我们利用时间戳解决这个问题。

1
openssl x509 -in myself.csr -out myself.crt -req -signkey myself.key -days 365

这一步本该由CA做的,可是贫穷提高了我们的计算机水平,我们现在要自己签发。signkey后面就是我们自己的私钥,如果你像让证书看起来更逼真,可以先自建CA,再用自建CA的私钥签发自己的私钥创建的证书请求,你可以在网上找到教程。

-days参数依然是有效期。

这一步后,你得到了一个x509证书,你可以用它来加密,签名,但是,我们用于PDF签名的证书需要是 PKCS#12 格式的,所以再转下格式。

1
openssl pkcs12 -export -out myself.pfx -inkey myself.key -in myself.crt

没什么特别的。

至此,myself.pfx已经可以用于PDF签名了,你可以将这一pfx证书导入Kleopatra(需另外安装),然后打开Okular,在设置>配置后端程序>PDF>签名后端程序选中GnuPG,然后就可以用工具>数字签名了。(当然这是我后来才发现的)

签名

经过艰苦的搜查,我尝试了pdftkpdfsig等工具,都不太行。

最后我找到了pyhanko,一个专门用于签章pdf的python包,先安装它。

可以用pip安装:

1
pip install pyhanko[opentype]

[opentype]表示安装opentype可选项,因为我们要生成可见签章,需要处理字体问题。

然后,上大招:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import os
from pyhanko import stamp
from pyhanko.pdf_utils import text
from pyhanko.pdf_utils.font import opentype
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
from pyhanko.sign import fields, signers
from pyhanko.sign import timestamps
from pyhanko.sign.fields import SigSeedSubFilter
import uuid

pfx = '/home/micraow/sec/cert.pfx' # 证书位置
passwd = b'填到这里' # 密码
output_dir = '/home/micraow/Documents/signed/' # 签名后的存放处,确保存在!
signer_name = 'Pengbo' # 签章人姓名
url = 'https://pengs.top' # 主页地址
email = 'peng@pengs.top' # 邮箱地址
extra_info = 'GPG fingerprint: \n6B5314F24B17198DD24B\n5B47062C2BFFBDCBB303\n' # 另外的想打印的信息

signer = signers.SimpleSigner.load_pkcs12(
pfx_file=pfx, passphrase=passwd
)

timestamper = timestamps.HTTPTimeStamper(
url='https://freetsa.org/tsr'
)

in_file = input("Location of input pdf: ").strip()

target = input('Who is this document given to? (Myself by default) ').strip()

if target == '':
target = 'Myself'

purpose = input(
'The reason why you want to sign the document?(Not Specified by default) ').strip()

if purpose == '':
purpose = 'Not Specified'

desc = input('Any other information you want to give? If not, hit Enter.').strip()

filename = os.path.basename(in_file)

with open(in_file, 'rb') as inf:
w = IncrementalPdfFileWriter(inf, strict=False)
fields.append_signature_field(
w, sig_field_spec=fields.SigFieldSpec(
# x1,y1,x2,y2 left to right, bottom to top
'Signature', box=(395, 750, 580, 830)
)
)

meta = signers.PdfSignatureMetadata(field_name='Signature', md_algorithm='sha256',
subfilter=SigSeedSubFilter.PADES, embed_validation_info=False, use_pades_lta=True,)
pdf_signer = signers.PdfSigner(
meta, timestamper=timestamper, signer=signer, stamp_style=stamp.QRStampStyle(
# Let's include the URL in the stamp text as well
stamp_text='Signed by: %(signer)s\nEmail: %(email)s\nTime: %(ts)s\nURL: '+ url + \
'\nUUID: \n%(uuid)s\n' + extra_info,
text_box_style=text.TextBoxStyle(
font=opentype.GlyphAccumulatorFactory(
'/home/micraow/.local/Rajdhani-Medium.ttf') # 字体位置
),
),
)

id = uuid.uuid1()

info = "如果你看到这段文字,它意味着这个文件经过了数字签名。\nIf you see this text, it means the document has been digitally signed.\n" + \
"签名人 The signer: " + signer_name + '\n'+'原始文件名 Original filename: ' + \
filename+'\n'+'文档接收方 Target: ' + target + \
'\n'+'文档签名原因 Reason: ' + purpose + '\n'

if desc != '':
info = info + '更多信息 More information: ' + desc+'\n'

info = info + '这个文档使用了由Pengbo提供的工具进行签名,有关该签名的细节你可以在https://pengs.top/pdf-sign/ 看到。\n This document is signed using the tools provided by Pengbo, details of which can be found at https://pengs.top/pdf-sign/.'
with open(output_dir+"signed_"+filename, 'wb') as outf:
# with QR stamps, the 'url' text parameter is special-cased and mandatory, even if it
# doesn't occur in the stamp text: this is because the value of the 'url' parameter is
# also used to render the QR code.
pdf_signer.sign_pdf(
w, output=outf,
appearance_text_params={
'signer': signer_name, 'email': email, 'url': info, 'uuid': str(id)}
)

print("File at: "+output_dir+"signed_"+filename)


有一些需要注意的:

import下面一堆信息根据自己实际情况改

如果附加信息太多,二维码可能不好扫。

运行前确保output_dir存在,不存在请新建文件夹,

Windows上路径需要转义,你需要在路径中每个反斜杠后再加一个反斜杠。

路径可用相对路径或绝对路径,

linux上,可以将文件拖到终端窗口中快捷输入路径。

对了,关于上面提到的的时间戳

时间戳通常包含以下几个关键要素:

  1. 具体的时间信息:精确到秒甚至更高精度的时间点,明确标识了签名动作发生的时刻。

例如,时间戳可能记录为“2024 年 7 月 3 日 15 时 30 分 25 秒”。

  1. 数字签名:用于证明时间信息的完整性和准确性,防止被篡改。

这确保了记录的时间是真实和可信的。

  1. 权威认证:时间戳通常由可信赖的第三方时间戳服务器生成和提供。

这个服务器具有高度准确的时钟和安全机制,以确保其提供的时间信息是准确和不可伪造的。

举例来说,当对一份 PDF 文档进行签名时,时间戳就像一个公证员在旁边记录下您签名的精确时间,并给这个时间记录加上了一个无法篡改的印章,以证明这个时间的真实性和有效性。

在法律和合规性方面,时间戳可以作为重要的证据,证明签名操作在特定时间完成,具有不可否认性和权威性。

以上代码中,使用https://freetsa.org/tsr ,你可以用别的时间戳服务器。

效果

demo

去繁就简,我觉得挺好看的,很有正式文件的感觉。

另外写到这儿我突然想到之前流浪地球里出现的字体,挺酷的,我把链接贴这儿,你可以下下来用,签名效果如下:

demo

比较有科幻感。

Alist链接

扫描后结果:

demo

总结

自己动手,丰衣足食。

另外,这是我第一次尝试将人工智能真正用于问题解决,由于pdf签名这一块,网上的信息很少,很杂,大多数是广告或windows上的软件,因此我使用了豆包这个免费的人工智能模型,速度和逻辑性都不错,可联网搜索,只是有的时候就在编造了。它给我的pdfsig的命令参数都是它自己编的,害我困惑了好久,后来还是我自己找到解决方案的。

评论

留下神评妙论