引子。说说我们公司吧,以二代测序技术和生物信息学为核心,从事肿瘤个体化精准诊疗和伴随诊断。粗暴一点来讲,卖报告的。报告以PDF文档为载体,纸质文件会加盖公章。
需求:使用证书给PDF签名,防止篡改
签名的这东西,在 https 标配化的今天,已经很普遍了。大概意思,比如我(王大力)发布一个文档,我用我的签名文件(即数字证书)给文档加一个签名,让阅读文档的人知道,哦,这个文档是大力的。一旦别人坏人修改了内容,则大力的签名即消息。因为别人是没有我的签名的,签名不能被模仿。
其中主要的技术环节,主要是有大型的机构纷发、核验签名证书。PDF 阅读软件对签名的检查和提示。最重要的是我必须保护好自己的签名,不能让坏人得到。否则,坏人就可以冒充我发布内容,而让我背锅…
引子。说说我们公司吧,以二代测序技术和生物信息学为核心,从事肿瘤个体化精准诊疗和伴随诊断。粗暴一点来讲,卖报告的。报告以PDF文档为载体,纸质文件会加盖公章。
需求:使用证书给PDF签名,防止篡改
签名的这东西,在 https 标配化的今天,已经很普遍了。大概意思,比如我(王大力)发布一个文档,我用我的签名文件(即数字证书)给文档加一个签名,让阅读文档的人知道,哦,这个文档是大力的。一旦别人坏人修改了内容,则大力的签名即消息。因为别人是没有我的签名的,签名不能被模仿。
其中主要的技术环节,主要是有大型的机构纷发、核验签名证书。PDF 阅读软件对签名的检查和提示。最重要的是我必须保护好自己的签名,不能让坏人得到。否则,坏人就可以冒充我发布内容,而让我背锅…
案例:
貌似离我最近的案例就是车险的电子保单。有一个公章。在 Macos 下使用预览程序打开 PDF 文件,看不到公章。而使用 Adobe Reader 可以看到公章。点击公章,能看到签名信息,以及最重要的,文档是否被修改过。
我们的目标大概也是这样。但到目前为止(技术探索阶段)没有搞明白公章是怎么消失的。所以我们的 PDF 文件,暂时目标就是依靠阅读器,去检查是否被修改过,能显示签名信息即可。
方法论:
公司生成 PDF 文件的工具,是购买的 prince 软件,授权费适中,即来之,则安之。首先想到就是该程序在生成是,把签名加上,万事大吉。当然,这款软件也是牛B,好几年前有人给留言类似的功能,他们就是不加。这里说一个他们的方向:H52Pdf。一个方向上做好了,也是够吃了。
不过在去搞代码前,我花了大半天的时间,在安装各类PDF阅读器,编辑器。通过编辑工具加签名,偷偷编辑,查看效果。这个过程也是相当消耗精力啊。
PDF Expert 记录一下,这个工具还可以。
领导给了个链接,使用 TCPDF 这个库,有签名案例 ,这个 Demo 我也是佩服,一语中的,直击要害,差点我就以为我完成工作了。看到网站的第一感觉,这也太老土了;再看,确实老土,而且类库给维护也比较旧的。(核心功能稳定了嘛,毕竟 php 也这么多年了)。
等把类库下载下来,开始测试的时候,傻了。这个类库,集生成与签名于一体。对已有文件加签,是拒绝的。然后就开始搜吧,网上大多数 php 操作 pdf 都会有这个类库的影子。
在它的文章中找到了关键线索,我也整理一个表格
prince | 将html+css生成PDF文档的程序 | https://www.princexml.com/ |
---|---|---|
tcpdf | php 生成 pdf文档的类库,支持加签名 | https://tcpdf.org/docs/ |
FPDI | 可以用来载入一个已存在的PDF 档案,给tcpdf使用 | https://www.setasign.com/products/fpdi/about/ |
最后说一下FPDI的母公司,https://www.setasign.com/,有多款产品,其中 FPDI是免费的…
万事俱备,开始实践喽
实践过程
安装类库
1
2
3compser require tecnickcom/tcpdf
compser require setasign/fpdi
compser require setasign/fpdi-tcpdftcpdf example 001 主要是了为找找感觉,忘记代码从哪里弄过来的了
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
require_once("vendor/autoload.php");
// create new PDF document
$pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false);
// set document information
$pdf->SetCreator(PDF_CREATOR); //设置创建者
$pdf->SetAuthor('Nicola Asuni'); //设置作者
$pdf->SetTitle('TCPDF Example 001'); //设置文件的title
$pdf->SetSubject('TCPDF Tutorial'); //设置主题
$pdf->SetKeywords('TCPDF, PDF, example, test, guide'); //设置关键词
// set default header data
$pdf->SetHeaderData(PDF_HEADER_LOGO, PDF_HEADER_LOGO_WIDTH, PDF_HEADER_TITLE . ' 001', PDF_HEADER_STRING, array(0, 64, 255), array(0, 64, 128)); //设置头部,比如header_logo,header_title,header_string及其属性
$pdf->setFooterData(array(0, 64, 0), array(0, 64, 128));
// set header and footer fonts
$pdf->setHeaderFont(Array(PDF_FONT_NAME_MAIN, '', PDF_FONT_SIZE_MAIN)); //设置页头字体
$pdf->setFooterFont(Array(PDF_FONT_NAME_DATA, '', PDF_FONT_SIZE_DATA)); //设置页尾字体
// set default monospaced font
$pdf->SetDefaultMonospacedFont(PDF_FONT_MONOSPACED); //设置默认等宽字体
// set margins
$pdf->SetMargins(PDF_MARGIN_LEFT, PDF_MARGIN_TOP, PDF_MARGIN_RIGHT); //设置margins 参考css的margins
$pdf->SetHeaderMargin(PDF_MARGIN_HEADER); //设置页头margins
$pdf->SetFooterMargin(PDF_MARGIN_FOOTER); //设置页脚margins
// set auto page breaks
$pdf->SetAutoPageBreak(TRUE, PDF_MARGIN_BOTTOM); //设置自动分页
// set image scale factor
$pdf->setImageScale(PDF_IMAGE_SCALE_RATIO); //设置调整图像自适应比例
// set some language-dependent strings (optional) 设置一些与语言相关的字符串
if (@file_exists(dirname(__FILE__) . '/lang/eng.php')) {
require_once(dirname(__FILE__) . '/lang/eng.php');
$pdf->setLanguageArray($l);
}
// ---------------------------------------------------------
// set default font subsetting mode
$pdf->setFontSubsetting(true); //设置默认字体子集模式
// Set font
// dejavusans is a UTF-8 Unicode font, if you only need to
// print standard ASCII chars, you can use core fonts like
// helvetica or times to reduce file size.
$pdf->SetFont('dejavusans', '', 14, '', true); //设置字体
// Add a page
// This method has several options, check the source code documentation for more information.
$pdf->AddPage(); //增加一个页面
// set text shadow effect 设置文字阴影效果
$pdf->setTextShadow(array('enabled' => true, 'depth_w' => 0.2, 'depth_h' => 0.2, 'color' => array(196, 196, 196), 'opacity' => 1, 'blend_mode' => 'Normal'));
// Set some content to print
$html = <<<EOD
<h1>Welcome to Genecast <a href="http://www.tcpdf.org" style="text-decoration:none;background-color:#CC0000;color:black;"> <span style="color:black;">TC</span><span style="color:white;">PDF</span> </a>!</h1>
<i>This is the first example of TCPDF library.</i>
<p>This text is printed using the <i>writeHTMLCell()</i> method but you can also use: <i>Multicell(), writeHTML(), Write(), Cell() and Text()</i>.</p>
<p>Please check the source code documentation and other examples for further information.</p>
<p style="color:#CC0000;">TO IMPROVE AND EXPAND TCPDF I NEED YOUR SUPPORT, PLEASE <a href="http://sourceforge.net/donate/index.php?group_id=128076">MAKE A DONATION!</a></p>
EOD;
// Print text using writeHTMLCell()
$pdf->writeHTMLCell(0, 0, '', '', $html, 0, 1, 0, true, '', true); //使用writeHTMLCell打印文本
// ---------------------------------------------------------
// Close and output PDF document
// This method has several options, check the source code documentation for more information.
$v = $pdf->Output('/Users/apple/Desktop/example_001.pdf', 'F'); //I输出在浏览器上制作证书。仅用于自签,生产时需要向厂商购买。自己给自己签名,没有公信力啦
1
2
3
4
5# 复制 tcpdf的 demo喽
openssl req -x509 -nodes -days 365000 -newkey rsa:1024 -keyout tcpdf.crt -out tcpdf.crt
openssl pkcs12 -export -in tcpdf.crt -out tcpdf.p12
# 第二步的密码,在php代码里需要使用的哦
使用代码,给已存在的 pdf 文件签名
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
require_once("vendor/autoload.php");
use setasign\Fpdi\TcpdfFpdi;
use setasign\Fpdi\PdfReader;
$pdf = new TcpdfFpdi();
$pdf->setPrintHeader(false);
$pdf->setPrintFooter(false);
// 不自动换行,很重要噢
$pdf->SetAutoPageBreak(false);
$pageCount = $pdf->setSourceFile('/Users/apple/Desktop/14.pdf');
// 让旧的 pdf 所有页面载入到新的pdf中
for($i = 1; $i <= $pageCount; $i++) {
$pageId = $pdf->ImportPage($i);
$s = $pdf->getTemplatesize($pageId);
$pdf->AddPage($s['orientation'], $s);
$pdf->useImportedPage($pageId);
}
$certificate = 'file:///Users/apple/webapps/pdf/tcpdf.crt';
$info = array(
'Name' => '臻和',
'Location' => 'Office',
'Reason' => '臻和出品',
'ContactInfo' => 'http://www.genecast.com.cn',
);
// openssl 第二个命令输入的密码,这里需要噢
$password = "123456";
$pdf->setSignature($certificate, $certificate, $password, '', 2, $info);
// 在第一页加签名
$pdf->setPage(1);
// *** set signature appearance ***
// 签名展示的图片,可以用其它内容代替
$pdf->Image('images.png', 130, 263, 38, 30, 'PNG');
// 真实的签名块,不可见,盖在图片上边
$pdf->setSignatureAppearance(130, 263, 38, 30);
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// *** set an empty signature appearance ***
//$pdf->addEmptySignatureAppearance(130, 260, 38, 16);
$pdf->Output('/Users/apple/Desktop/example_002.pdf', 'F'); //I输出在浏览器上查看效果喽, 博客刚建立 ,图片整的还不利索,这里不截图了。使用 Adobe Reader 可以看到完整的效果。特意用第三方编辑工具编辑再保存时,阅读器可以提示文档被修改过。
总结
这里其实不知道写啥,但感觉要有一个,小时候写作文留下的阴影吗。在编码阶段, composer 安装,并没有这么顺利,最开始没有找对类名。
另外,说一下FPDI的母公司,www.setasign.com ,商业公司网站的制作,果然是比 tcpdf 高出一个档次。
而且,他们家就有一个产品,PDF-Singer,对 PDF签名。
如果最后没有找到现在的方案,我肯定会向领导申请购买的。文档,Demo,一应俱全,虽然全是英文。唯一不爽的就是没有购买授权,我没有找着测试类库在哪里。
好吧,技术探索完成,后续在生产中再有心得,再补上。我是王大力,谢谢您的观看~~
后记1
到了生产环节,发现一个很严重的Bug。
提供PDF签名证书的厂商,产品是通地 UsbKey 发放给客户(也就是我们),在 windows 下,通过 adobe 的工具,对 pdf 文件进行签名。
逆天了,法律规定他们不能提供文件形式的软证书。
或者使用他们提供的在线API,大体意思就是把文件传给厂商,他们帮你签你的名字。(这样安全吗?)
厂商的客服跟我说:这个就像纸质文件,需要盖公章一样,公章也是需要专人保管,一份一份地盖。
我默默的认同了。
后记2
找到一家可以提供软证书的公司,http://www.cfca.com.cn/ , 需要将 pfx 证书转换格式,命令请参考:
1
openssl pkcs12 -in cfca.pfx -out tcpdf.crt -nodes
setasign 的 PDF-Singer 申请了测试license,下载了代码,由于需要使用 ioncube 而恶心到到了我(主要是自动安装失败),放弃。
已签名的文档,在 adobe 的阅读器里,显示 “文档证书的有效性未知。无法验证作者。” 那么你需要如下操作: 首选项–>信任管理器->自动AATL更新–>立即更新。 更新需要一段时间,更新后重新验证即可正常显示签名信息。
各种生成,各种猜测,各种阅读器查看效果,心好累,比写代码都累。(最近比较困~~~)
正式上线后,又出现一个问题。一台win7的电脑,无法验证证书。厂家给出的解释是win7部分系统不能自动寻找证书链路,所以无法验证。厂家给出的解决方案是把中级证书和用户证书合并成新的证书,签名时把中级证书签进去。 但合并证书程序会报错。
最终采用了 Java 外挂程序的方案 java -jar JSignPdf/JSignPdf.jar ,使用 pfx 和密码 直接隐藏式签名。测试大部分机器都能正常。此事告一段落
后记3
文件签名的效果得到领导的认可,于是产生了要把旧的文件都转换成签名形式。之前我怎么没有想到这个延展需求呢。
干呗。先找一款去PDF密码的工具(当然,我是知道密码的)。 PDFBox 的命令行工具非常好用。然后说一下工作流:
- 从数据库中导出相关的数据。每条数据代表一个文件。
- 从阿里云下载这个文件
- 对文件实施移除密码、增加签名操作
- 在阿里上备份这个文件。
- 将本的文件覆盖阿里旧的文件。
一次性的工具类,使用GO语言去编写,本以为很简单的,结果还是搞了3天才转换完成。使用带缓存的 chan 去做pipeline 控制。耗时点:
- log 选择。选择综合证啊。 zero/log ,这事以后不做了
- exec comand。主要是在判断正常远行,还是异常费了点功夫。不得不说GO语言在这块的处理还是挺好的。关键字: cmd.ProcessState
- 串流测试,本地下载文件较慢,debug一次需要等待
- 增加中断、继续执行的能力。因为整体跑下了,跑了三个多小时。