摘要

阅读本文并探索
    - 如何突破Web程序无状态性这个让人抓狂的障碍实现自动显示签名结果和批量签名功能。
    - 如何将签名功能封装到一个实现了IHttpHandler接口的类库中,使Client端的代码尽可能的简单。
    - 使用数字签名API函数需要注意的几个问题。

本文介绍在Web程序中使用数字签名所遇到的特殊困难和解决方法,并给出一个超简单但相当实用的DEMO。

DEMO程序的效果

让我们先来看看实现之后的效果。



让Client端代码尽可能的简单

我们将数字签名操作的复杂性全部封装到一个命名空间为mylib.util.lnca的类库中,类库只暴露一个名为Signer的类。
Signer的Client (本例中的Default.aspx)的职责只有
    - 构造一个含有待签名的数据的Dictionary作为Signer的输入,然后调用Signer.do_sign()函数进行数字签名。
    - 在页面上放置一个专门用于取得并显示签名结果的按钮,并将这个按钮的ClientID传递给Signer,这样Signer在完成签名后就可以自动触发这个按钮。在将程序发布给最终用户时,要把这个按钮的top属性设为-10000,这样最终用户就看不到这个按钮了。

Default.aspx 的设计视图的截图



Default.aspx 的源代码如下

Default.aspx
Default.aspx.cs

由于Signer是一个HTTP 处理程序,所以需要在Web.config中添加一行对Signer.ashx的注册:
Web.config

有关HTTP处理程序的创建和应用,可以看《实战 HTTP 处理程序(HTTP Handler)系列》

由于我们把复杂性都放在了Signer.cs中,Signer.cs的代码有些长,我们会在后面讨论它的几个要点。

Signer.cs

Signer.cs的第227和228行的“Settings.Default.controls”和“Settings.Default.js”是需要发送给客户端浏览器用于回传签名结果的HiddenFields和执行签名操作的Javascript语句。我把它们放在了类库的配置文件里,它们的代码如下:
Settings.Default.controls
Settings.Default.js

自动显示签名结果

我们想要实现这样的交互效果:用户选定想要进行签名的数据后,只要按一个按钮就会自动弹出一个小窗体显示签名的进度;当签名结束后,可以自动显示签名结果,就像上面那个DEMO程序所展示的那样。
      如果我们开发的是WinForm的信息系统,实现这样的效果简直易如反掌。可是在Web程序中,我们却遇到了一点麻烦。

自动显示签名结果的困难

正如我们在第2篇所介绍的,为了防复制破解,我们是使用USB Key做数字签名。这个USB Key必须插在客户端的电脑上,我们在Server端无法直接控制它,只能通过在客户端浏览器上执行的javascript代码调用一个由辽宁CA认证中心开发的一个ActiveX控件操作USB Key进行签名,再将签名结果通过HiddenFields Post回Server端——不过这个Server端已经不是以前的Server端了,Web程序的这种无状态性没少让我们吃亏。


换句话说,我们没办法像下面这样写(伪码):
protected void do_sign_button_Click(object sender, EventArgs e)
{
1    Dictionary<string, string> sign_candidates = prepare_sign_candidates();
2    Dictionary<string, string> signed_datas = excute javascript at client browser to sign data, and return signed datas
3    signed_data_gridview.DataSource = signed_datas;
4    signed_data_gridview.DataBind(); 
}

实现自动显示签名结果

我们遇到的问题的实质是:准备签名数据(伪代码第1行)、显示签名结果(伪代码第3、4行)的操作在Server端进行;而使用USB Key进行签名的操作(伪代码第2行)必须在Client端的浏览器上执行,并且这两种操作是异步的!所以我们只能将显示签名结果的代码放到另一个函数中,在签名结束后以某种方法触发它。我们在Demo中所使用的方法是,将显示签名结果的代码放到“显示签名结果(自动)”按钮的Click事件中,在签名结束后,使用
javascript:opener.document.getElementById(show_signed_data_button.ClientID).click();
来触发这个按钮的Click事件。


思考题


我们使用一个“伪隐藏”的按钮可以简单地实现自动显示签名结果的效果,不过这种作法似乎有点土。你能否使用其它更“高级”的方法来实现同样的效果?

实现批量签名

我们需要让用户按一次按钮,就可以签名 n 条数据,可是数字签名API SignDataEx(sourcedata,...) 一次只能签名一条数据。我们需要遍历每条待签名数据,调用SignDataEx()进行签名。我们有两种选择:
    1. 在Server端进行遍历,每次传送一条数据给Client端进行签名。
    2. 将 n 条待签名数据一次全部传给Client端,在Client端使用javascript的for循环遍历待签名数据并进行签名。

我们在Demo程序中是使用了第1种方法。基于和“自动显示签名结果”一节所述的同样的困难,我们无法在Signer.cs的ProcessRequest()中这样写(伪码):
public void ProcessRequest(System.Web.HttpContext context)
{
    foreach (string key in sign_candidates.Keys)
    {
        string signed_data = excute javascript at client browser to sign data, and return signed data
    }
}

好在已经有大师发明了外部迭代器(external iterator),我们可以在第一次迭代之前,先创建一个待签名数据的一个外部迭代器,并把它保存在Session中。每次签名后,Client端PostBack回Server端,在Server端从Session中取出这个外部迭代器,调用sign_candidates_enumerator.MoveNext(),之后继续向Client端发送签名用的javascript语句,直至完成全部遍历,请参见Signer.cs的250~268行。下面的时序图表示批量签名3条数据的过程。



思考题

我们的DEMO实现了第1种方法,你能否实现第2种方法?这两种实现方法各有什么优缺点?

综合起来

我们把批量签名与自动显示签名结果的功能都放在Signer.cs中,可以用下面这个经过简化的时序图来表示。




附录 数字签名API简介

我们使用的是辽宁省数字认证中心发放的数字证书。他们还提供了两套数字签名API:一个是ActiveX控件;一个是COM组件。两套API都有完整、丰富的数字签名相关的函数,可以单独使用。如果是WinForm程序,直接使用COM组件即可。不过由于Web程序必须使用ActiveX控件,所以我们在作数字签名的时候使用ActiveX控件,在验证签名的时候使用COM组件。也许您手头的API和我们使用的API并不相同,不过您仍然可以下载这两套API的手册找找感觉。
LNCAToolkits 控件(通用版)程序员手册_v2.pdf  <- 这个是ActiveX控件的手册
LNCA-CryptoAPI-Com版程序员手册_v1.pdf  <- 这个是COM组件的手册

作数字签名的API函数是SignDataEx()。
函数声明: BSTR SignDataEx (BSTR szSrc,
                            BSTR sSignAlgo,
                            long IsAddSignCert,
                            long IsAddSrcData,
                            BSTR szInnerOid,
                            long IsAddTime,
                            BSTR pPin);
说明:进行签名数据操作(使用客户端证书)。
参数:
.. szSrc :原文数据
.. sSignAlgo:指定签名算法
szOID_OIWSEC_sha1 = “1.3.14.3.2.26”
szOID_RSA_MD5 = “1.2.840.113549.2.5”
szOID_RSA_MD2 = “1.2.840.113549.2.2”
.. IsAddSignCert 是否在结果中携带证书
0 = 不携带证书
1 = 携带证书
.. IsAddSrcData 是否在结果中携带原文
0 = 不携带原文
1 = 携带原文
.. szInnerOid:数据类型OID,(默认:NULL)
szOID_TSP_TSTInfo = "1.2.840.113549.1.9.16.1.4"
.. IsAddTime:是否添加签名时间
0: 不进行时间编码
1: 取当前系统时间,进行时间编码
2、从时间戳服务器取得时间,必须首先设置时间戳服务器URL。请参考7.14
章节的时间戳操作。
.. pPin:用户Key 口令
如果输入正确的口令,则不弹出输入口令窗口,直接签名数据。
如果输入错误的口令,则弹出输入口令窗口
其中:通过InputDataType 属性来指定原文数据格式
0 = 输入原文为二进制编码,此函数内部不进行转码
1 = 输入原文为BASE64 编码,此函数内部进行转码
返回值:成功时返回签名数据(BASE64 编码),
失败时返回空,由ErrorCode 属性中取错误码,由ErrorMessage 属性中取错误信
息。

需要说明的是IsAddSignCert这个参数。它指示是否在签名数据中携带证书
携带证书的优点:如果选择携带证书,在验证签名时就不用再向验证签名的函数显式传递一个证书。验证签名的函数会自动从签名数据中解析出证书,然后验证签名,这在编程上无疑是非常方便的!如果选择不携带证书,我们就必须将系统所有用户的证书保存在一个“证书表”中,再在含有签名数据字段的表中创建一个专门保存“证书表”ID的字段,在验证签名前要从“证书表”中取得证书,再验证签名。
携带证书的缺点:缺点是会使签名数据比较长,例如对“1234”签名的Base64编码会有1942个字符;而如果不携带证书只有560个字符。所以如果客户十分吝啬数据库的存储空间,就需要使用不携带证书的方式。不过我个人是十分喜欢携带证书的方式的(偶是懒人^_^)

还有就是IsAddSrcData这个参数应该指定为“携带原文”,这样在验证签名的时候(使用COM组件的VerifySign函数)就不用再给出原文了(SourceData参数设为null),而且VerifySign()函数在验证成功后会返回原文,这样还可以向用户显示这样的信息:“看,以前签名的是这个数据(VerifySign()函数返回的原文),现在被改成了这个数据(表中原文字段中的值),所以验证失败”。

本篇源代码下载

本篇源代码。运行本篇的DEMO需要预先安装签名用的ActiveX控件:ActiveX_bin_v2.8.6.0_20061130.rar,解压缩后运行reg_ActiveX.bat即可完成安装,还有你的 USB Key要支持这个ActiveX控件才行。

致谢

非常感谢辽宁省数字认证中心软件开发部项目经理张铁夫先生的指导和帮助。每次电话咨询都能得到他的耐心讲解,即使我们已经试用了半年多仍然没有购买一个数字证书^_^
感谢一直关注本系列的各位同仁,大家的鼓励和指导令我受益匪浅。感谢古巴、Clark Zheng菜菜灰在Http 处理程序方面对我的指导。感谢笑望人生蛙蛙池塘大石头aspnetx银河慢一拍游民一族yoyolion对本系列的补充和指正。

工具箱

本系列的所有流程图均使用Visio 2003绘制。
UML 时序图使用Dia v0.96.1绘制。
抓图软件使用的是SnigIt v7.1.1。图片上使用了手写字体方正静蕾简体
图片预览和格式转换使用了ACDSee v5.0。
文字部分使用Google 拼音输入法键入。

posted on 2007-10-08 10:30  1-2-3  阅读(12460)  评论(49编辑  收藏  举报