前言
本文是哈工大17级密码学原理与实践课程的实践部分(CA证书认证系统)实验报告,由于本实验代码中包含了数据库部分,每个人的电脑配置环境也不一样,所以,提供的参考代码不会直接运行成功,但给大家提供了写实验的一些思路。本实验报告是最终版,非常详尽。
本文仅供参考。希望各位学弟学妹认真对待实验,在学习时间充足的情况下,借此大大提高自己的编程能力。
代码下载地址:哈工大密码学实验(CA证书认证系统)
以下正文。
1. 背景与意义
CA认证系统,即CA证书颁发系统,是公钥基础设施(PKI)中的核心环节,是公钥加密过程中的第三方权威认证方,负责密钥和证书的产生、发布、管理、存储和撤销等功能,广泛用于电子商务等需要非对称加密的信息传输场景中。所有通过CA的信息传输方,都要无条件的信任CA的公正性,在消息传输的过程中,CA为信息传输的双方提供公私钥加密环境,提供身份认证、安全传输、不可否认性和数据完整性等功能。
公钥基础设施PKI( Publie Key Infrastrueture, 简称PKI)是利用公钥理论和技术建立的提供安全服务的基础设施, 是信息安全 的核心,也是电子商务安全的关键所在。PKI技术采用证书管理公钥, 通过第三方的可信任机构—认证中心CA(Certificate Authority),把用户的公钥和用户的其他标识信息(如名称、E-mail、身份证号等)捆绑在一起,在Internet上验证用户的身份(其中认证机构CA是PKI系统的核心部分)。目前, 通用的办法是采用建立在PKI基础之上的数字证书,通过把要传输的数字信息进行加密和签名,保证信息传输的机密性、真实性、完整性和不可否认性,从而保证信息的安全传输。
2. 国内外应用现状
美国是最早提出PKI概念的国家,并于1996年成立了美国联邦PKI筹委会。与PKI相关的绝大部分标准都有美国制定,其PKI技术在世界上处于领先地位。2000年6月30日,美国总统克林顿正式签署美国《全球及全国商业电子签名法》,给与电子签名、数字证书以法律上的保护,这一决定使电子认证问题迅速成为各国政府关注的热点。加拿大在1993年就已经开始了政府PKI体系雏形的研究工作,到2000年已经在PKI体系方面获得重要进展。加拿大与美国代表了发达国家PKI发展的主流。
欧洲在PKI基础建设方面也成绩显著。已颁布了93/1999EC法规,强调技术中立、隐私权保护、国内与国外相互认证以及无歧视等原则。为了解决各国PKI之间的协同工作问题,他采取了一系列策略:如积极自主相关研究所、大学和企业研究PKI相关技术;自主PKI互操作性相关技术研究,并建立CA网络及其顶级CA。并于2000年10成立了欧洲桥CA指导委员会,于2001年3月23日成立欧洲桥CA。
我国的PKI技术从1998年开始起步,由于政府和各有关部门近年来对PKI产业的发展给予了高度重视,2001年PKI技术被列为”十五“863计划信息安全主题重大项目,并于同年10月成立了国家863计划信息安全基础设施研究中心。国家计委也在制定新的计划来支持PKI产业的发展,在国家电子政务工程中明确提出了要构建PKI体系。目前,我国已全面推动PKI技术研究于应用。
1998年国内第一家以实体形式运营的上海CA中心(SHECA)成立。目前,国内的CA机构分为区域型、行业型、商业性和企业型四类;截至2002年底,前三种CA机构已有60余家,58%的省市建立了区域CA,部分部委建立了行业CA。其中全国型的行业CA中心有中国金融认证中心CFCA、CTCA中国电信认证中心等。区域型CA有一定地区性,也称地区CA,如上海CA中心、广东电子商务认证中心。
3. 需求分析
3.1 总体需求
(图片略)
3.2 功能需求
本次实验中,CA的功能需求主要有如下几点:
-
接收验证用户数字证书的申请
为了实现接收验证用户数字证书的申请,需要用户向CA提供自己的身份信息,并提供申请机构的相关有效信息。
-
生成证书
要生成一个证书,需要设计好证书的内容以及格式。
-
存储证书
要存储一个证书,需要设计好证书的存储形式,以及是否需要加密。
-
向申请者颁发(或拒绝颁发)数字证书
向申请者颁发数字证书,需要设计好如何想申请者颁发证书,颁发过程中是否需要加密传输。在这为了模拟CA的功能,将不实现拒绝颁发功能。
-
接收用户的数字证书的查询、撤销
需要设计好数字证书的查询和撤销需要哪些权限,用户需要提供哪些信息来进行查询和撤销。
-
产生和发布证书的有效期
需要设计好如何为用户产生证书的有效期,有效期的时长选择以及如何发布有效期。
-
数字证书的归档
需要设计好如何将数字证书归档,是以数据库的形式还是以文件的形式。
-
密钥归档
同数字证书的归档。
3.3 性能需求
本CA系统未考虑网站性能需求。
3.4 技术选型
- html+css+javascript
- jdbc+druid+mysql+beanutils
- jquery+jstl+standard
- dom4j
- jsencrypt.min.js+sha256-min.js
4. 概要设计
围绕着需求分析,将展开如下设计:
-
接收验证用户数字证书的申请
- 用户登录CA认证系统(前端主页),如有必要,进行注册
- 用户进入申请证书功能,输入组织结构、工商注册号、法人姓名、经办人姓名、经办人电话等信息,在“1年”、“2年”、“3年”中选择一项作为证书的有效期,并上传好本地的公钥文件后,点击提交按钮。
- 后台处理用户的登录信息,将这些信息归档后,生成证书。
-
生成证书
生成证书完全是后端的内容,用户在提交证书申请的相关信息后,由服务器在后台处理相关信息,并生成固定格式的
.mycer
证书文件。 -
存储证书
证书一律以文件的形式存储在
D:\DriveY\IntelliJ\cryptotw2\out\artifacts\cryptotw2_Web_exploded\download
路径下。在数据库中,证书以如下形式字段存储相关信息。
-
向申请者颁发(或拒绝颁发)数字证书
- 用户登录CA认证系统后,进入下载证书功能
- 用户输入要下载证书的序列号
- 服务器通过浏览器向用户发送一个证书文件,完成证书的颁发。
-
接收用户的数字证书的查询、撤销
- 查询
- 用户登录CA认证系统后,进入查看证书功能
- 用户可以查看所有组织机构的证书(包括已撤销的证书),并且可以下载
- 撤销
- 用户登录CA认证系统后,进入撤销证书功能
- 用户输入要查询证书的序列号和该证书登记时的用户密码
- 后台处理,验证无误后,该证书被成功撤销,撤销信息在CRL库中登记。
- 查询
-
产生和发布证书的有效期
证书的有效期为从证书制作时间起,1年/2年/3年止,时限由用户在申请证书时选择。
-
数字证书的归档
数字证书的归档一律以文件的形式在
D:\DriveY\IntelliJ\cryptotw2\out\artifacts\cryptotw2_Web_exploded\download
路径下。详细信息以字段形式保存在数据库中。 -
密钥归档
密钥(申请人的公钥)一律以文件的形式保存在
D:\DriveY\IntelliJ\cryptotw2\out\artifacts\cryptotw2_Web_exploded\upload
路径下。密钥内容不会在数据库中保存。
5. 细节设计
本部分,细节设计报告说明将以逐个功能、技术要点的形式呈现,逻辑实现请参见Java doc文档。
本实验使用的前端模板取自 大气黑色注册表单html5模板。包括首页在内,有相当部分的css代码经本人亲手改造后呈现。
由于某些界面不能够全部展示,将以一定比例的缩放给出界面贴图,可能存在贴图比例失衡的情况。
由于撰写报告时,仍有些功能正在调试,同一功能的前后贴图可能存在不一样的地方。
加密部分和数据结构部分将在小节中集中体现。
5.1 登陆/注册界面
登陆界面中有一个标题和一个container
面板,面板中左半部分是登录部分,右半部分是注册部分,上图中,“记住我“,”忘记密码?“和”其他方式登录“功能都是无效的。
5.1.1 注册
用户注册需要输入用户名、密码、身份证等信息,需要满足以下几个条件:
- 用户名必须为字母、数字、下划线、减号的组合,长度为6~16位
- 密码至少包括数字和字母,长度至少为6位,至多20位。
- 身份证信息必须满足中华人民共和国居民18位身份证号要求
在用户输入合法信息并点击注册后,相关信息会经过加密存入到数据库的ra_user
表中:
注意,此处为了演示,保留了一个用户名为shen
,密码为123
的简单用户,实际上这个用户的用户名和密码是非法的,不能被注册。
5.1.2 登录
用户登录需要输入用户名、密码信息,用户名和密码必须属于同一用户。后台将从数据库中提取注册密码密文和盐值,对用户输入的密码加密后与密文比对,如果匹配成功,则验证登陆成功。
5.2 主页面
主页面涉及两个部分,一个是置于页面顶端中间的用户登录状态,以及页面中间的container
面板。
5.2.1 用户登录状态(Filter)
其实最开始的时候没有做这个功能,后来才考虑到加进去的。记录用户登录状态的意义在于,一个是要保证所有访问本网站的用户,都必须登录后才能访问网站内的资源(主要指除了登录/注册页面的其它页面),另一个是记录此时用户的登录状态,将证书和申请时的登录用户关联,以便在撤销证书时,必须输入证书申请时的用户的登录密码,起到了验证身份的作用,限制了撤销权限。
后面的页面在这个位置都会有用户登录状态,不再赘述。
5.2.2 主操作面板
主操作面板中一共包括6个功能:
- 申请证书
- (下载)密钥生成器
- 下载证书
- 撤销证书
- 下载 CRL(证书撤销列表)
- 查看证书(包含下载功能)
5.3 申请证书
申请证书页面包括一个container
,其中有用户申请证书需要提交的表单。该表单包括:
- 组织机构
- 工商注册号
- 法人姓名
- 经办人姓名
- 经办人电话
- 有效期(1年/2年/3年)
- 密钥文件
其中,组织机构、工商注册号、法人姓名、经办人姓名、经办人电话,为了简单起见,都没有做拼写检查或合法性检查,有效期只能三选一,密钥文件从本地上传(该文件应该是本网站提供的密钥生成器genkey.exe
生成的公钥文件)。
5.4 密钥生成器
点击密钥生成器后,马上从浏览器获取一个genkey.exe
文件,运行该可执行程序将在与该文件所在路径同级的目录下生成两个文件pk.key
和sk.key
。前者为公钥文件,后者为私钥文件,两者共同组成一对 1024bits 的 RSA 公私密钥对。
5.5 下载证书
下载证书页面非常简单,只需要输入要下载的证书序列号,就可以从浏览器获取相应的证书文件。
5.6 撤销证书
撤销证书页面也非常简单,只需要输入要下载的证书序列号,并且输入相应的撤销身份验证密码,就可以成功撤销该证书。证书被撤销后不会从证书库中删除,但会加入到 CRL 库中。
撤销身份验证密码指的是证书在申请时,正在操作的用户的登录密码。这样设计虽然比较简单,但是限制了撤销权限,将撤销功能与用户登录状态相关联,由于证书序列号是(对所有人)可见的,所以限制请求者只有知道证书申请时的登录密码,才可以撤销成功。
其实大型公证CA系统的证书的申请和撤销都是比较严谨的工作,通常需要多方核实,并且在一到三个工作日内给出证书的申请和撤销反馈。本次实验中,申请和撤销都是自动化的,对于证书的申请没有加权限,证书的撤销也只加了登录密码的权限限定。
5.7 下载 URL
点击下载 CRL会直接从浏览器获取一个 CRL 文件,CRL文件的具体结构将在数据结构小节中介绍。
5.8 查看证书
查看证书功能将数据库certificate
表中的信息抽取出来,分页展示在前端网页。前端展示提供了证书的序列号、组织机构、工商注册号、证书有效期起、证书有效期止信息,并提供了一个下载链接,单机该链接可以直接获取到该证书文件。
此时数据库中只有两条记录,为了方便展示,所以在这里设计成每页只有一条记录。
5.9 数据结构设计
5.9.1 证书
证书的设计是本次实验CA系统的重头戏,现在互联网上比较流行的证书为X.509结构。
X.509是密码学里公钥证书的格式标准。X.509证书已应用在包括TLS/SSL在内的众多网络协议里,同时它也用在很多非在线应用场景里,比如电子签名服务。X.509证书里含有公钥、身份信息(比如网络主机名,组织的名称或个体名称等)和签名信息(可以是证书签发机构CA的签名,也可以是自签名)。对于一份经由可信的证书签发机构签名或者可以通过其它方式验证的证书,证书的拥有者就可以用证书及相应的私钥来创建安全的通信,对文档进行数字签名。
另外除了证书本身功能,X.509还附带了证书吊销列表和用于从最终对证书进行签名的证书签发机构直到最终可信点为止的证书合法性验证算法。
X.509是ITU-T标准化部门基于他们之前的ASN.1定义的一套证书标准。
证书组成结构标准用ASN.1(一种标准的语言)来进行描述. X.509 v3 数字证书结构如下:
- 证书
- 版本号
- 序列号
- 签名算法
- 颁发者
- 证书有效期
- 此日期前无效
- 此日期后无效
- 主题
- 主题公钥信息
- 公钥算法
- 主题公钥
- 颁发者唯一身份信息(可选项)
- 主题唯一身份信息(可选项)
- 扩展信息(可选项)
- …
- 证书签名算法
- 数字签名
——引用自维基百科
像csdn网站的证书文件如下:
参考X.509证书结构,设计自己的证书结构如下:
- 序列号(Serial Number: )——以当前时间为前缀的组织机构名称(如,20191222091200HIT)的16进制md5散列值。
- 签名算法(Sign Algorithm: )——sha1RSA
- 签名哈希算法(Encrypt Algorithm: )——sha256
- 颁发者——GothamCityTrust RSA CA 2019, www.tofushen.com, Gotham City Trust, CN
- 有效期从(Valid Time From: )——当前日期,如2019年12月21日 21:32:00
- 到(Valid Time To: )——当前日期向后(1年/2年/3年),如2022年12月21日 21:32:00
- 使用者(User: )——组织机构名
- 公钥(Public Key: )——公钥文件字串
- 签名(Sign: )——公钥文件字串的签名值,使用sha1RSA算法哈希,RSA算法加密,CA私钥加密。
样例如下:
证书文件内容为文本形式,文件格式为.mycer
。
因为证书的申请要和登录者信息相关联,所以在数据库中存储的证书申请信息如下:
- id
- 序列号
- 组织机构
- 工商注册号
- 文件路径(用于下载证书)
- 有效期起
- 有效期止
- 法人姓名
- 经办人姓名
- 经办人电话
- 申请人登录用户名
5.9.2 证书吊销列表(CRL)
在一些密码系统的运作中(一般情况下是公开密钥基础建设[注 1]的系统),证书吊销列表(英文:Certificate revocation list,缩写:CRL,或译作证书废止清册[参 1])是尚未到期就被证书颁发机构吊销的数字证书的名单。这些在证书吊销列表中的证书不再会受到信任。
当前,在线证书状态协议可以代替本列表实现证书状态检查。
——引用自维基百科
要从 CA 获得 CRL,请执行以下步骤:
- 从 CA 获取文件形式的 CRL。
- 转至管理控制台中的配置页面。
- 单击“证书”>“证书授权机构”选项卡。
- 单击“安装 CRL”按钮。
- 输入关联文件的完整路径名。
- 单击“确定”。
——引用自Oracle文档
从上述资料中可知,CRL首先是存储在CA中的一个列表形式的文件,该文件可以下载到用户主机上,并且可以通过Windows驱动安装到系统中,和浏览器建立联系,浏览器在浏览网站时,内置了许多互联网上网站的证书和CRL,浏览器检测根据证书内容和CRL验证证书的有效性,给用户提供可信提示。
CRL的发布存在一种博弈过程。如果CA频繁地更新CRL,会加大CA的维护负担,但是反而如果CA更新CRL过慢,又会导致用户不能及时获取CRL,可能存在一些证书已经被撤销了,而用户没有来得及得知,产生信赖风险。
因此,本次实验为了简化模型,模拟CRL的发布,给出如下设计:
- CRL为一个
.xml
文件,格式为
.xml
文件根标签为<crls>
,一级标签为<crl>
,每个一级标签内存放了撤销证书的信息,包括四个二级标签,分别表示撤销证书的序列号、组织机构、有效期起、有效期止。
- CRL以文件下载的形式提供给用户,用户需要自行到CA系统网站上下载(先登录)。
因此,本系统的CRL是实时更新的,但把信赖程度的实时保障责任推给了用户,如果用户没有及时的下载CRL,那么他将面临信任风险。
5.10 加密部分
本次实验涉及到的加密有非对称加密(1024bits RSA)和哈希函数(sha256),加密场景如下:
5.10.1 前端加密和签名(HTTP的传输安全)
前端加密的确是一个存在争议的问题。要保证HTTP的传输安全,现在流行的方式是采用HTTPS协议和建立SSL通道。因为HTTP是明文传输,所以不可避免地存在报文交换的过程中,被窃听的问题。攻击者可以窃听到传输报文,截获报文,修改报文内容转发,还可能实施重放攻击。信息传输的双方可以用加密的方式,保证报文的保密性,还可以用签名的方式,保证报文的完整性,还可以用时间戳的方式,抵御重放攻击。但是,没有一种有效的方式,可以保证HTTP传输的万无一失,因此,HTTPS协议和SSL证书通道被提出,改善HTTP传输的加密环境。
然而,既然没有一种方式可以保证HTTP报文不被攻击,那可不可以说对HTTP报文的传输进行加密,是无用功呢?不能说是无用功,只要是增大攻击者的攻击成本的加密方案都是好方案。
前端加密就存在这么一种讨论。因为前端加密是在前端页面中用JavaScript函数加密,而前端页面很容易就可以暴露在攻击者面前,甚至加密的密钥都是公开的,前端隐藏技术也不能完美的保证前端的安全性,所以,是否值得在前端做加密确实存在争议。
本系统中的前端加密采用RSA加密方案,前端公钥加密,后端私钥解密,公钥以明文形式定义在JavaScript函数中,私钥以明文的形式定义在后端代码中。后端可以成功获取前端传递的加密密文,并用对应的私钥解密获得明文。
本系统中用到前端加密的场景:登录/注册的用户名、密码、身份证信息;证书申请时的组织机构、工商注册号、法人姓名、经办人姓名、经办人电话信息;证书撤销时的序列号和身份验证密码信息。
为了提高HTTP传输的消息完整性,前端除了加密还用到了签名。对消息在前端用加密的同时,给消息做一个哈希处理(sha256),将消息的哈希摘要与密文同时发送到后端,在后端对解密得到的明文做同样的哈希处理(sha256),然后与前端传过来的哈希摘要比对,如果比对成功,则认为消息是完整的,否则向前端回复一个“消息可能被损坏”的信息,并作废这次提交。
之所以没有在前端签名的过程中,对摘要用RSA加密处理的原因是,在前端用RSA私钥对摘要加密是不安全的,因为私钥不应该被公开,而前端代码是不可避免被暴露的。而用公钥对摘要加密是没有意义的,任何人都可以用公钥伪造一个摘要。我认为,这里的签名验证的有效性,是建立在保密性之上的,如果保密性被破坏(明文被破解),那么签名验证也会很容易的被伪造。
5.10.2 签名
签名是信息传输中保证信息完整性的环节,大致流程为,发送方对明文做哈希处理得到一个摘要,对摘要用私钥加密,和消息一同传输给接收端,接收端用发送端的公钥解密得到摘要,再用同样的哈希算法对消息哈希处理得到摘要,比对两个摘要,如果两个摘要匹配成功,则认为该消息传输过程中完整性得到了保障。
这是因为发送方和接收方在处理摘要的加解密时,用的是发送方的公私钥,发送方用私钥加密,接收方用公钥解密,可以肯定的是,发送方的私钥是保密的,因此,签名是不可伪造的,如果消息被篡改,那么接收方可以通过比对两个摘要内容不匹配,断定消息的不完整性。
在CA给客户签发证书时,需要对证书中的(客户的公钥)做数字签名,用的是CA的私钥,在验证证书有效性时,要用CA的公钥,这样就可以来验证证书确实是CA颁发的。
5.10.3 RSA消息传输加密
由于在本系统中,包括电商和网银在内的消息传输还没有用到比较大的报文传输,所以为了简便,在消息传输的过程中没有使用DES加密方案,全部使用RSA加密方案(包括签名)。实际上消息传输应该遵照下列模型:
5.10.4 存储安全
本CA系统的存储安全只作用在用户数据库中ra_user
,因为考虑到CRL和证书库的信息都是公开的,所以就没有加密。
登录用户的登录密码和身份证都是隐私信息,应该加密存储。事实上,密码的安全性建立在数据库安全上。如果数据库被攻破,则密码的保密性将不复存在。
设计存储安全保密方案如下:
- 用户注册时,输入相关信息,在前端对用户名、密码、身份证号进行RSA加密和签名。
- 后台解密信息,并验证消息完整性。
- 后台随机生成强随机串(盐值),将解密信息(明文)与盐值混合,做sha256哈希处理,将哈希值和盐值存入数据库中。
- 用户登录时,先根据用户名获取数据库中的盐值,然后将明文与盐值混合,做sha256哈希处理,然后与数据库中的哈希密码比对,如果匹配,则登录成功;否则,登录失败。
5.10.5 验证码
验证码的作用是为了给恶意破解密码、反复自动提交表单攻击造成麻烦。本CA系统中需要用到验证码的部分有:登录/注册、申请证书、撤销证书。
5.10.6 前端密码隐藏显示
前端密码隐藏显示可以给偷窥攻击带来麻烦。
5.11 证书验证
严格意义上的证书验证,应该是对一个合法的X.509证书文件,利用Windows的证书相关程序和浏览器内置的驱动程序,将证书安装到浏览器上,每次访问网页时,由浏览器验证证书有效性,并给出相应的提示:
在本实验中,为了模拟上述功能,把证书验证封装好了一个工具类VerifyCerUtil
,该类中有一个方法verify
以证书文件(File
)作为参数或者以证书文件内容(String
)作为参数,返回一个boolean
值,验证有效则返回true
,无效则返回false
。
起初的证书验证是将该函数分配给电商和网银的两个服务器上,各自做本地验证。但是考虑到任何用户登录电商和网银都应该给出证书有效性的验证,所以,这个验证应该改到前端上验证。
为此,在验收前一天晚上将证书验证仓促改为一个模拟的小驱动程序,该驱动程序使用IE浏览器的ActiveX
技术,相当于给IE浏览器安装一个小插件,在JavaScript代码中启动驱动程序。
将对电商和网银的证书验证封装成两个.exe
驱动程序,放到指定目录下,还要求用户主机上该目录下应该有电商和网银的证书,以及CRL列表和CA认证机构的公钥文件。这个目录相当于在用户主机上,模拟了一个浏览器环境,每当用户使用浏览器(IE)访问电商和网银两个网站的时候,都会给出证书有效性提示如下:
6. 实现与测试
本节将对一部分代码和所有功能的实现测试给出说明。
6.1 登录/注册
6.1.1 注册
由于CSDN上传图片时出了点小问题,这部分的图都略了,参考价值不大。
用户名不合法
密码不合法
密码强度
身份证不合法
当用户iamjack
注册成功时
6.1.2 登录
登录验证码错误
输入用户jd
,或者用户iamjack
的密码错误,将显示用户名或密码错误。当用户的用户名或密码输入错误时,不应该显式地指出到底是用户名错误还是密码错误,防止攻击者对用户名或密码针对性的攻击。
用户iamjack
成功登录
6.2 申请证书
首先正确申请一个证书
6.3 密钥生成器
运行下载的genkey.exe
文件,将在同级目录下生成两个密钥文件pk.key
(公钥)和sk.key
(私钥)。
6.4 下载证书
验证码功能不再演示。
如果序列号不对的话,提示此证书不存在。
正确下载证书的结果:
6.5 撤销证书
验证码功能不再演示。
验证验证码正确后,会验证撤销身份验证密码:
验证撤销身份验证密码正确后,会验证证书是否存在,以及证书有效性:
成功撤销证书演示:
再次撤销该证书,提示该证书已失效:
6.6 下载CRL
点击下载CRL后开始下载。
CRL文件如下所示:
6.7 查看证书
证书查看列表如下(每页1条目):
下载链接有效:
7. 核心代码
7.1 后端
7.1.1 后端对前端的传输解密
String username = req.getParameter("username");String password = req.getParameter("password");String sign_username = req.getParameter("sign_username");String sign_password = req.getParameter("sign_password");PrivateKey privateKey = null;try {privateKey = // 私钥内容省略不写username = new String(Ende.decrypt((RSAPrivateKey) privateKey, Base64.getDecoder().decode(username)), "utf-8");password = new String(Ende.decrypt((RSAPrivateKey) privateKey, Base64.getDecoder().decode(password)), "utf-8");} catch (Exception e) {e.printStackTrace();}
7.1.2 后端对前端签名的验证
if (!SHADigest.getDigest(username).equalsIgnoreCase(sign_username)|| !SHADigest.getDigest(password).equalsIgnoreCase(sign_password)) {req.setAttribute("login_error", "登录失败!报文可能被损坏!请报警!");req.getRequestDispatcher("/index.jsp").forward(req, resp);return;}
7.1.3 用户注册加密
public boolean register(User registerUser) {String salt = UUID.randomUUID().toString(); // 生成盐值String username = registerUser.getUsername();String rawPwd = registerUser.getPassword();String idcard = registerUser.getIdcard();PasswordEncryptor passwordEncryptor = new PasswordEncryptor(salt, "sha-256"); // 生成加密器// 加盐加密String encPwd = passwordEncryptor.encode(rawPwd);String encId = passwordEncryptor.encode(idcard);Object[] a = {username};int u =template.queryForObject("SELECT COUNT(*) FROM ra_user WHERE username = ?", a, Integer.class);if (u >= 1) {return false;}Object[] insertArgs = {username, encPwd, encId, salt};String sql = "INSERT INTO ra_user(username, password, idcard, salt) VALUES (?,?,?,?)";template.update(sql, insertArgs);return true;}
7.1.4 用户登录的验证
public boolean verify(User user) {String username = user.getUsername();String rawPwd = user.getPassword();Object[] a = {username};String encPwd = null;String salt = null;try {encPwd =template.queryForObject("SELECT password FROM ra_user WHERE username = ?", a,String.class); // 获取数据库中用户的密码salt =template.queryForObject("SELECT salt FROM ra_user WHERE username = ?", a,String.class); // 获取盐值} catch (DataAccessException e) {e.printStackTrace();return false;}PasswordEncryptor passwordEncryptor = new PasswordEncryptor(salt, "sha-256"); // 生成加密器boolean isValid = passwordEncryptor.isPasswordValid(encPwd, rawPwd); // 判断密码正确性if (isValid) {return true;} else {return false;}}
7.1.5 Filter
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {HttpServletRequest request = (HttpServletRequest) req;String uri = request.getRequestURI();// 用户访问除了资源文件、主页、验证码servlet、登录servlet、注册servlet之外,都要进行登录状态检查if (uri.contains("/index.jsp") || uri.contains("/loginServlet") || uri.contains("/css/")|| uri.contains("/js/") || uri.contains("/images/") || uri.contains("/checkCodeServlet")|| uri.contains("/loginServlet") || uri.contains("/registerServlet")) {chain.doFilter(req, resp);} else {Object username = request.getSession().getAttribute("username");if (username != null) {chain.doFilter(req, resp);} else {request.setAttribute("login_msg", "您尚未登录,请登录");request.getRequestDispatcher("/index.jsp").forward(request, resp);}// chain.doFilter(req, resp);}}
7.1.6 Ende(加解密代码)
Ende
类中的代码取自参考文献[14].
7.1.7 RSASignature(签名算法)
RSASignature
类中的代码取自参考文献[14].
7.1.8 验证证书有效性(该方法被封装在驱动程序中)
package com.caiji.util;import com.caiji.domain.Mycrl;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;public class VerifyCerUtil {private static final String timePattern = "(\\d*年\\d*月\\d*日 \\d{2}:\\d{2}:\\d{2})";/*** 验证证书有效性* @param cer 证书内容字串* @return 如果有效,返回true;否则,返回false* @throws ParseException* @throws IOException* @throws NoSuchPaddingException* @throws NoSuchAlgorithmException* @throws IllegalBlockSizeException* @throws BadPaddingException* @throws InvalidKeyException* @throws InvalidKeySpecException*/public static boolean verify(String cer) throws ParseException, IOException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidKeySpecException {String serial_number = parseSerialNumber(cer);String validTimeFrom = parseValidTimeFrom(cer);String validTimeTo = parseValidTimeTo(cer);String sign = parseSign(cer);String publicKey = parsePublicKey(cer);// 验证证书有效期if (!judgeTime(validTimeFrom, validTimeTo)) {return false;}// 验证证书签名if (!RSASignature.doCheck(publicKey, sign, KeyUtil.loadPublicKeyByFile("D:\\DriveY"+ "\\IntelliJ\\cryptotw2\\out\\artifacts\\cryptotw2_Web_exploded\\download\\pk.key"), "utf-8")) {return false;}// 解析CRL,验证证书是否已被撤销List<String> mycrlList = getNode("D:\\DriveY\\IntelliJ\\cryptotw2\\out\\artifacts"+ "\\cryptotw2_Web_exploded\\download\\crl.xml");for (String deprecated : mycrlList) {if (deprecated.equals(serial_number)) {return false;}}return true;}/*** 验证证书有效性* @param certificate 证书文件* @return 如果有效,返回true;否则,返回false* @throws IOException* @throws ParseException* @throws NoSuchPaddingException* @throws NoSuchAlgorithmException* @throws IllegalBlockSizeException* @throws BadPaddingException* @throws InvalidKeyException* @throws InvalidKeySpecException*/public static boolean verify(File certificate) throws IOException, ParseException, NoSuchPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidKeySpecException {BufferedReader bufferedReader =new BufferedReader(new InputStreamReader(new FileInputStream(certificate), "GBK"));String cerLine = "";String cer = "";while ((cerLine = bufferedReader.readLine()) != null) {cer += cerLine + "\n";}bufferedReader.close();return verify(cer);}/*** 内部工具方法,解析crl.xml文件内容* @param url crl.xml文件url路径* @return 将xml文件解析为一个String列表,其中包含xml的每一条目信息*/private static List<String> getNode(String url) {List<String> mycrlList = new ArrayList<>();DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();try {DocumentBuilder builder = documentBuilderFactory.newDocumentBuilder();Document document = builder.parse(url);NodeList nodeList = document.getElementsByTagName("crl");for (int i = 0; i < nodeList.getLength(); i++) {Node node = nodeList.item(i);NodeList childNodes = node.getChildNodes();for (int j = 0; j < childNodes.getLength(); j++) {if (childNodes.item(j).getNodeType() == Node.ELEMENT_NODE&& childNodes.item(j).getNodeName().equals("serial_number")) {mycrlList.add(childNodes.item(j).getFirstChild().getNodeValue());}}}} catch (ParserConfigurationException e) {e.printStackTrace();} catch (SAXException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return mycrlList;}/*** 验证证书有效期* @param validTimeFrom 证书的有效期起字段* @param validTimeTo 证书的有效期至字段* @return 如果证书时间有效,返回true;否则,返回false* @throws ParseException*/private static boolean judgeTime(String validTimeFrom, String validTimeTo) throws ParseException {SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");Date from = simpleDateFormat.parse(validTimeFrom);Date to = simpleDateFormat.parse(validTimeTo);Date now = new Date();if (now.before(from) || now.after(to)) {return false;} else {return true;}}/*** 解析证书的序列号* @param cer 证书文件内容字串* @return 证书序列号字串*/private static String parseSerialNumber(String cer) {String serial_number = null;Pattern serialNumberPattern = Pattern.compile("Serial Number: " + "(\\w*)\\n");Matcher serialNumberMatch = serialNumberPattern.matcher(cer);while (serialNumberMatch.find()) {serial_number = serialNumberMatch.group(1);}return serial_number;}/*** 解析证书有效期起字串* @param cer 证书内容字串* @return 证书有效期起字串*/private static String parseValidTimeFrom(String cer) {String validTimeFrom = null;Pattern validTimeFromPattern = Pattern.compile("Valid Time From: " + timePattern);Matcher validTimeFromMatch = validTimeFromPattern.matcher(cer);while (validTimeFromMatch.find()) {validTimeFrom = validTimeFromMatch.group(1);}return validTimeFrom;}/*** 解析证书有效期至字串* @param cer 证书内容字串* @return 证书有效期至字串*/private static String parseValidTimeTo(String cer) {String validTimeTo = null;Pattern validTimeToPattern = Pattern.compile("Valid Time To: " + timePattern);Matcher validTimeToMatch = validTimeToPattern.matcher(cer);while (validTimeToMatch.find()) {validTimeTo = validTimeToMatch.group(1);}return validTimeTo;}/*** 解析证书公钥字串* @param cer 证书内容字串* @return 证书公钥字串*/private static String parsePublicKey(String cer) {String publicKey = null;Pattern publicKeyPattern = Pattern.compile("Public Key: " + "(.*)\\n");Matcher publicKeyMatch = publicKeyPattern.matcher(cer);while (publicKeyMatch.find()) {publicKey = publicKeyMatch.group(1);}return publicKey;}/*** 解析证书签名字串* @param cer 证书内容字串* @return 证书签名字串*/private static String parseSign(String cer) {String sign = null;Pattern signPattern = Pattern.compile("Sign: " + "(.*)\\n");Matcher signMatch = signPattern.matcher(cer);while (signMatch.find()) {sign = signMatch.group(1);}return sign;}
}
7.1.9 证书制作
private String makeCertificate(List<String> cerLines) {String cer_path = this.getServletContext().getRealPath("/download/");String cer_name =cerLines.get(0).substring(cerLines.get(0).indexOf(":") + 2, cerLines.get(0).length());try {BufferedWriter bufferedWriter =new BufferedWriter(new OutputStreamWriter(new FileOutputStream(new File(cer_path + cer_name))));for (int i = 0; i < cerLines.size(); i++) {System.out.println(cerLines.get(i));bufferedWriter.write(cerLines.get(i));bufferedWriter.write("\n");bufferedWriter.flush();}bufferedWriter.close();} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return cer_path + cer_name;}
7.1.10 证书下载
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String checkCode = request.getParameter("check_code");HttpSession session = request.getSession();String checkCode_session = (String) session.getAttribute("checkCode_session");session.removeAttribute("checkCode_session");if (request.getParameter("no_check_code") == null && (checkCode_session == null|| !checkCode_session.equalsIgnoreCase(checkCode))) {request.setAttribute("msg", "验证码错误!");request.getRequestDispatcher("download_cer.jsp").forward(request, response);return;}request.removeAttribute("no_check_code");String serial_number = request.getParameter("serial_number");System.out.println(serial_number);CertificateDao certificateDao = new CertificateDao();String filePath = certificateDao.getFilePath(serial_number);if (filePath == null) {request.setAttribute("msg", "此证书不存在!");request.getRequestDispatcher("download_cer.jsp").forward(request, response);return;}String filename = serial_number + ".mycer";ServletContext servletContext = this.getServletContext();// String mimeType = servletContext.getMimeType(file_path);response.setHeader("content-type", "application/octet-stream");response.setHeader("content-disposition", "attachment;filename=" + filename);FileInputStream fileInputStream = new FileInputStream(filePath);ServletOutputStream servletOutputStream = response.getOutputStream();byte[] buff = new byte[1024 * 8];int len = 0;while ((len = fileInputStream.read(buff)) != -1) {servletOutputStream.write(buff, 0, len);}fileInputStream.close();}
7.2 前端JavaScript
7.2.1 验证码加载
<script>window.onload = function () {document.getElementById("apply_check_img").onclick = function () {this.src = "/tw/checkCodeServlet?time=" + new Date().getTime();};}</script>
7.2.2 前端传输加密和签名
function apply_encrypt() {var publicKey ="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCLFxU/IBjwf0UjGsXy/dx6QiRH6pvvZoGvtUtDhGT1Wq0Vga2h7CUKwNazN8/l8YksKgz01JqIEh8NUAGOFY3tVMk/sNlMgYqCWyRw6QvMtspLDe3dzJn/83qAsMq3wr3Ooxgk143AXV6YlXhDvipYqwasP7luwRrbgdTANItl3wIDAQAB";var encrypt = new JSEncrypt();encrypt.setPublicKey(publicKey);var enc_organization = encrypt.encrypt(organization.value);var enc_registration_number = encrypt.encrypt(registration_number.value);var enc_juridical_person = encrypt.encrypt(juridical_person.value);var enc_charge_person = encrypt.encrypt(charge_person.value);var enc_charge_phone = encrypt.encrypt(charge_phone.value);var sign_organization = hex_sha256(organization.value);var sign_registration_number = hex_sha256(registration_number.value);var sign_juridical_person = hex_sha256(juridical_person.value);var sign_charge_person = hex_sha256(charge_person.value);var sign_charge_phone = hex_sha256(charge_phone.value);organization.value = enc_organization;registration_number.value = enc_registration_number;juridical_person.value = enc_juridical_person;charge_person.value = enc_charge_person;charge_phone.value = enc_charge_phone;this.sign_organization.value = sign_organization;this.sign_registration_number.value = sign_registration_number;this.sign_juridical_person.value = sign_juridical_person;this.sign_charge_person.value = sign_charge_person;this.sign_charge_phone.value = sign_charge_phone;}
7.2.3 证书内容(数据库前端)显示
<body>
<span style="text-align: right; font-family: 楷体; font-weight: bold; font-size: 20px; margin-top:
20px; margin-right: 20px; color: #FFFFFF"><%=request.getSession().getAttribute("username")%>,欢迎您</span>
<br>
<a href="/tw/logoutServlet"style="text-align: right; font-family: 楷体; font-weight: normal; font-size:20px; margin-top:20px; margin-right: 20px; color: #FFFFFF">退出登录</a>
<h1>哥谭市数字证书认证中心</h1><div class="container" style="width: 75%;"><h3>证 书 列 表</h3><table><tr class="tr-header"><th>序列号</th><th>组织机构</th><th>工商注册号</th><th>证书有效期起</th><th>证书有效期止</th><th>下载链接</th></tr><c:forEach items="${requestScope.certItems}" var="certItem" varStatus="s"><tr class="tr-body"><th>${certItem.serial_number}</th><th>${certItem.organization}</th><th>${certItem.registration_number}</th><th>${certItem.start_time}</th><th>${certItem.end_time}</th><th><ahref="/tw/downloadCerServlet?serial_number=${certItem.serial_number}&no_check_code=123">下 载</a></th></tr></c:forEach><tr class="tr-footer"><td colspan="3"style="text-align: right; padding-right: 20px; padding-top: 5px; padding-bottom:5px;">当前为第${page.currentPage}页,共${page.totalPage} 页</td><td colspan="3" style="text-align: left; padding-left: 20px; padding-top: 5px; padding-bottom:5px;"><c:choose><c:when test="${page.hasPrePage}"><a href="/tw/certificateInfoServlet?currentPage=1">首页</a> |<a href="/tw/certificateInfoServlet?currentPage=${page.currentPage-1}">上一页</a></c:when><c:otherwise>首页 | 上一页</c:otherwise></c:choose><c:choose><c:when test="${page.hasNextPage}"><a href="/tw/certificateInfoServlet?currentPage=${page.currentPage+1}">下一页</a> |</c:when><c:otherwise>下一页 | 尾页</c:otherwise></c:choose></td></tr></table><div class="clear"></div></div>
</body>
7.2.4 前端注册合法性检查
register_btn.addEventListener("click", function () {var uPattern = /^[a-zA-Z0-9_-]{6,16}$/;if (!uPattern.test(register_username.value)) {alert("用户名必须为字母、数字、下划线、减号的组合,长度为6-16!");return;}var pPattern = /^(?=.*[a-zA-Z])(?=.*\d)[^]{6,20}$/;if (!pPattern.test(register_password.value)) {alert("密码长度至少为6位,至多20位!并且至少包括数字和字母!");return;}var cP = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;if (!cP.test(register_idcard.value)) {alert("请输入合法的身份证!");return;}reg_encrypt();document.getElementById("register_form").submit();register_username.value = "";register_password.value = "";register_idcard.value = "";});
7.2.5 密码强度显示
function CheckIntensity(pwd) {var Mcolor, Wcolor, Scolor, Color_Html;var m = 0;//匹配数字if (/\d+/.test(pwd)) {debugger;m++;};//匹配字母if (/[A-Za-z]+/.test(pwd)) {m++;};//匹配除数字字母外的特殊符号if (/[^0-9a-zA-Z]+/.test(pwd)) {m++;};if (pwd.length <= 6) {m = 1;}if (pwd.length <= 0) {m = 0;}switch (m) {case 1:Wcolor = "pwd pwd_Weak_c";Mcolor = "pwd pwd_c";Scolor = "pwd pwd_c pwd_c_r";Color_Html = "弱";break;case 2:Wcolor = "pwd pwd_Medium_c";Mcolor = "pwd pwd_Medium_c";Scolor = "pwd pwd_c pwd_c_r";Color_Html = "中";break;case 3:Wcolor = "pwd pwd_Strong_c";Mcolor = "pwd pwd_Strong_c";Scolor = "pwd pwd_Strong_c pwd_Strong_c_r";Color_Html = "强";break;default:Wcolor = "pwd pwd_c";Mcolor = "pwd pwd_c pwd_f";Scolor = "pwd pwd_c pwd_c_r";Color_Html = "无";break;}document.getElementById('pwd_Weak').className = Wcolor;document.getElementById('pwd_Medium').className = Mcolor;document.getElementById('pwd_Strong').className = Scolor;document.getElementById('pwd_Medium').innerHTML = Color_Html;}
8. 调试过程中出现的几个问题
-
在做前端消息传输的签名,后端验证消息完整性的过程中,会遇到中文消息签名不匹配问题,究其原因是编码问题,将后端的字节数组转换改为
utf-8
编码解决。byte[] plaintext = message.getBytes("utf-8");
-
在证书查看列表中点击下载链接,将因为验证码不正确而导致下载失败。原始是下载的servlet中加了对验证码的判断,从列表中点击下载链接时,可以加上一个
no_check_code=123
的参数,在下载的servlet中,判断如果no_check_code
不为空,则不进行验证码的判断。
9. 结束语
经过了八周的密码学实验实践,自己动手,从设计构思,到一个个功能的不断实现,完成了一个具有申请、撤销、下载、查询等基本功能的CA认证系统。
在实践过程中感受到最大的困难,就是前期的框架构思。我们组花了两周的时间,去仔细调查了CA证书(认证机构)、网上银行和电子商务之间的联系,其中包括消息传输,加密规则,每个个体的作用等。在了解这些之后,画出了结构草图,并按照结构草图,开始逐步实现功能。
我自己开发的CA认证系统的功能实现模式是菜单模式,在前端创造一个菜单,并在上面逐步添加功能并实现。
边学习边开发的过程让我学到了很多。特别感谢哔哩哔哩上的视频教程:JavaWeb(放慢-脚步),逐步学到了HTML、CSS、JavaScript、xml、servlet、response、session、filter、jsp+el+stl等一套技术栈,并成功运用到了本次实验的项目中。
此外,特别感谢我的两位队友,王久金同学和韩啸同学,在开发过程中团队积极相互沟通,解决了很多困难,两位队友在各自的开发中也解决了很多难题,总体开发效率很高。
10. 参考文献
[1] https://blog.csdn.net/wangliang369/article/details/83792116 “jsp 实现分页操作”
[2] https://blog.csdn.net/yeyuwanfeng/article/details/81907856 “html a标签样式设置”
[3] http://www.cssmoban.com/cssthemes/6824.shtml “大气黑色登录注册表单html5模板”
[4] https://www.cnblogs.com/daizhongxing/p/11593137.html “常见密码正则表达式”
[5] https://www.cnblogs.com/raphael1982/p/8012634.html “用户名、密码等15个常用的js正则表达式”
[6] https://blog.csdn.net/weixin_36293343/article/details/85090852 “X509证书结构及解析”
[7] https://zh.wikipedia.org/wiki/X.509 “X.509,维基百科”
[8] https://docs.oracle.com/cd/E19146-01/820-0872/gdagx/index.html “管理证书撤销列表 (Certificate Revocation List, CRL)”
[9] https://baike.baidu.com/item/%E8%AF%81%E4%B9%A6%E6%92%A4%E9%94%80/747891?fr=aladdin(https://baike.baidu.com/item/证书撤销/747891?fr=aladdin) “证书撤销”
[10] https://www.jianshu.com/p/c65fa3af1c01 “PKI/CA工作原理及架构”
[11] https://blog.csdn.net/ayang1986/article/details/80810072 “CA认证简单介绍与工作流程”
[12] https://blog.csdn.net/chu_jian86a/article/details/83246960 “ActiveXObject对象使用整理(JS调用本地exe程序)”
[13] https://blog.csdn.net/xiao_cs/article/details/6262144 “WshShell.Run方法说明”
[14] https://www.cnblogs.com/demodashi/p/8458113.html “Java使用RSA加密解密签名及校验”
[15] https://space.bilibili.com/250181517?spm_id_from=333.788.b_765f7570696e666f.2 “Java Web教程”
[16] 江为强, 陈波. PKI/CA技术的起源、现状和前景综述[J]. 西南科技大学学报, 2003, 18(4):75-78.