昨天在公司的代码中,看见一个VC项目的Post Build Step会用signtool给项目生成的exe添加数字签名,而exe的某块代码会调用WinVerifyTrust这个Windows API来验证exe自己的数字签名。正好这几天稍微闲暇一些,在好奇心的驱使下便开始捣鼓起数字签名来。经过一天终于有点收获,大致理解数字签名是怎么一回事,也对公司代码的那两块地方比较理解了,于是做个小结。


  首先推荐读读《算法导论》31.7节The RSA public-key crptosystem的第一小节:Public-key cryptosystems,对公钥密码系统的框架做个了解,当然也可以到网上搜类似的介绍性文章。这里不用知道任何数学公式,但要理解整个系统的逻辑流程和设计思想,尤其要理解私钥、公钥的概念,并且要知道:一个消息经公钥和私钥两次编码还是消息本身,且既可以用公钥先编码,也可以用私钥先编码。在这里我还弄明白了:虽然加密和数字签名这两种手段都可以基于同样的公钥密码系统,但它们的目的和工作流程是不一样的——前者是防窃听,后者是验真伪。


  接下来就是捣腾Windows上的数字签名工具,实践一下。signtool是Windows SDK自带的命令行工具,用于对文件进行数字签名,也可用于验证文件和时间戳文件中的签名。公司的那个VC项目使用了如下命令对exe进行数字签名:


signtool /a <生成的exe>


  不过我用同样的命令在自己家电脑上给一个exe签名,却总是得到“SignTool Error: No certificates were found that met all the given criteria”的错误。捣鼓了半天才知道,如果是自己测试的话,要先用makecert、cert2spc和pvk2pfx制作一个同时包含私钥和公钥的pfx格式的证书文件,然后用pfx文件给exe签名,或者将pfx导入证书库中,让signtool自己去搜可用的证书。可参考《makecert制作数字证书》这篇文章。为了模拟公司项目,我采用了导入证书库的做法,具体来说是这样:


  1、用makecert制作自己的根证书:

E:\Temp>makecert -n "CN=ZzxiangRoot" -r -sky signature -sv ZzxiangRoot.pvk ZzxiangRoot.cer
Succeeded

因为我明确是要给二进制文件制作签名,所以要加上-sky signature选项。于是E:\Temp目录下就回生成两个文件:私钥证书ZzxiangRoot.pvk和公钥证书ZzxiangRoot.cer。

2、用cert2spc将公钥证书转换为软件发布者证书,即spc文件:

E:\Temp>cert2spc ZzxiangRoot.cer ZzxiangRoot.spc
Succeeded

3、用pvk2pfx将公钥证书和私钥证书合并成一个PFX格式的证书文件:

E:\Temp>pvk2pfx -pvk ZzxiangRoot.pvk -spc ZzxiangRoot.spc -pfx ZzxiangRoot.pfx

4、双击ZzxiangRoot.pfx将其导入证书库中。在“证书导入向导”中,一路点击“下一步”,直到“证书存储”步骤,选择“将所有的证书放入下列存储”:

固件签名公私钥对 私钥签名失败 51670_Test

点击“浏览”,在弹出的对话框中选择“个人”->“确定”。

固件签名公私钥对 私钥签名失败 51670_数字签名_02

为什么要选择“个人”呢?可以参见signtool的sign子命令的/s选项的说明:“指定要在搜索证书时打开的存储区。 如果未指定该选项,则打开My存储。”这里的“My”就是“个人”。

然后就可以一路“下一步”到导入完成了。我们可以到计算机的管理控制台确认。在开始菜单中搜索并运行mmc。在mmc界面中,选择“文件”->“添加删除管理单元”。在弹出的“添加删除管理单元”对话框中,在左边的“可用的管理单元”中选择“证书”:

固件签名公私钥对 私钥签名失败 51670_Test_03

点击中间的“添加”按钮,在弹出的对话框中选择“我的用户帐户”或“计算机用户帐户”,再点击“完成”:

固件签名公私钥对 私钥签名失败 51670_Windows_04

就将“证书”节点添加到“所选管理节点”中了:

固件签名公私钥对 私钥签名失败 51670_Windows_05

点击“确定”,回到管理控制台主界面中,在左边的树控件中展开“证书 - 当前用户”->“个人”,选择“证书”节点,就可以看见已经导入的ZzxiangRoot证书。

双击ZzxiangRoot,可以看见证书对话框里写着“您有一个与该证书对应的私钥”。对话框里还写着“此CA根目录证书不受信任。要启用信任,请将该证书安装到‘受信任的根证书颁发机构’存储区”。后面会讲到这一点。

5、现在可以使用signtool命令给exe签名了:

E:\Temp>signtool sign /a Test.exe
 Done Adding Additional Store
 Successfully signed: Test.exe

可以加上/v查看更细致的输出:

E:\Temp>signtool sign /a /v Test.exe
 The following certificate was selected:
     Issued to: ZzxiangRoot
     Issued by: ZzxiangRoot
     Expires:   Sun Jan 01 07:59:59 2040
     SHA1 hash: 3361BBBD366687FD80B201F1346561C6E4936263


 Done Adding Additional Store
 Successfully signed: Test.exe


 Number of files successfully Signed: 1
 Number of warnings: 0
 Number of errors: 0

  接下来就是要验证exe的签名。按照公钥密码系统的设计思路,这一步可以在任意机器上进行。公司代码用的是WinVerifyTrust函数。其实也可以继续用signtool工具,命令格式是

signtool verify /pa <需要验证的exe>

  注意必须要加上/pa选项,否则signtool会使用针对Windows驱动程序的签名验证策略。

  不过现在直接用signtool verify /pa Test.exe的话,会得到“SignTool Error: A certificate chain processed, but terminated in a root certificate which is not trusted by the trust provider”的错误。这是因为验证用的计算机没有将ZzxiangRoot公钥证书添加到之前提到的“受信任的根证书颁发机构”。要做这一步的话,需要将公钥证书ZzxiangRoot.cer拷贝到验证机上,然后双击该cer文件,在弹出的证书对话框中选择“安装证书”。接下来和之前的导入证书操作一样,只是在“证书存储”这一步,需要选择将证书存储在“受信任的根证书颁发机构”存储区,而不是之前的“个人”存储区中。

  现在可以进行验证了:

E:\Temp>signtool verify /pa Test.exe

Successfully verified: Test.exe

也可以加上/v查看更细致的输出:

E:\Temp>signtool verify /pa /v Test.exe


Verifying: Test.exe
 Hash of file (sha1): 8D3C56FBE8BB11FB760B729FF8F801DDBA7C3B59


 Signing Certificate Chain:
     Issued to: ZzxiangRoot
     Issued by: ZzxiangRoot
     Expires:   Sun Jan 01 07:59:59 2040
     SHA1 hash: 3361BBBD366687FD80B201F1346561C6E4936263


 File is not timestamped.


 Successfully verified: Test.exe


 Number of files successfully Verified: 1
 Number of warnings:

0
Number of errors: 0

  使用WinVerifyTrust函数进行验证的方法可以参见MSDN的这个例子。这个例程可以直接拷贝下来用的,只是需要注意,要将它作为C++文件来编译,而不是纯C文件,否则会编译不过。

  说起来真是挺搞笑。虽然我的大学宣称自己的特色是信息安全,但大学毕业后五年来,这还是我第一次认真研究信息安全相关的技术。

上面提到的几个命令都是系统中的,打开cmd之后输入命令默认是找不到路径的。

需要执行脚本设置:

A:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\vcvars32.bat
A:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\amd64\vcvars64.bat
A:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\vcvarsall.bat


可以在当前目录下建一个init.bat内容如下:


"A:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\vcvars32.bat"