本文不涉及SSL通信相关的知识,只介绍一下最近通过我的调研,了解到的关于SSL认证相关的东西。
- JSSE Reference Guide
一个关于SSL的比较专业的介绍,来自JSSE(Java Secure Socket Extension)
什么是密钥
要说证书,需要先说一下密钥
是怎么回事。
目前的加密算法,分为对称加密和非对称加密两类,而密钥,可以把它想像成一把钥匙:
对称加密
: 使用同一把钥匙,即可实现对一段信息的加密和解密非对称加密
:加密和解密信息,需要使用不同的钥匙
对称加密的优点就是速度快,所以可以用来加密大段的信息,但缺点也很明显,因为通信双方需要提前交换密钥,那么密钥本身就存在泄露的风险。
而非对称加密,由于加密和解密使用不同的密钥,所以只需要将其中一个密钥发送给对方(传说中的公钥
),而另一个密钥由自己保管(也就是私钥
),这样就避免了在传输环节丢失的风险。
不过,非对称加密算法非常复杂,所以性能比对称加密要低很多,而且对处理的信息长度有限制。所以在实际使用中,信息仍然以对称加密算法进行加密,而对称加密算法使用的密钥,由非对称加密算法进行加密传输。
常见的加密算法:
- 对称加密:
DES
3DES
AES
- 非对称加密:
RSA
DSA
ECC
什么是签名
签名
其实就是通过某种计算,将一段信息生成一个摘要,比如我们常见的MD5
算法,就是干这个的。此外,常用的还有SHA
算法。
签名并不是完美的
一般来讲,签名都是固定长度的,比如MD5生成的就是一个
128
位的二进制串。
不论源信息是一个字符,还是一整部小说,通过MD5算法生成的签名长度都是固定的,而固定长度的签名能表达的信息量是有限的,这也就导致了信息在转换过程中必定是有损且不可逆的。
另一方面,由于签名只能表达有限个数的信息,而自然界中的信息数量则是无穷尽的,所以,必定导致不同的信息签名是会重复的。话虽如此,想要找到重复的签名并没有那么容易,比如MD5能够表达
2^128
种信息,想在这么多信息中找到一个重复的,想必也没那么容易。在术语中,将这种重复称之为冲突
或碰撞
。
什么是证书
证书本身和密钥是两个平行的概念。证书存在的目的,是对某个东西的认证。比如,你当前访问的网站的真实性、电子合同的有效性等等。而证书存在的形式,大体上就是由一个认证机构标识加一个由它签发的签名组成。
认证机构——我们称之为CA (Certificate Authority)
。只有受信的CA颁发的证书,才被信任。而一个CA是否被信任,则是由验证一方自己决定的。比如,操作系统会有一个受信的CA列表,在系统中的软件想要验证一个CA是否可信时,就可以把这个工作交给系统来完成。当然,也有自立门户的,比如JRE,自己有一个受信的CA列表,完全无视操作系统的CA认证。
一些浏览器,比如Chrome,也有自己的CA列表,不过他们应该是将自己的列表和系统的进行了一个合并。
这样就出现了如下情况:某些机构的证书在一个系统中好使,在另一个系统中不好使,或者不同浏览器表现也不一样。而Java从版本7开始,好似大幅减少了受信的CA列表,导致像沃通、StartSSL的证书都不在列表中,所以在Java7中进行SSL握手时,对果对方是这两个机构的证书的话,默认是不可信的,也就导致了握手失败。
沃通是国内做得比较大的CA机构,之前我们服务器的证书都是从沃通申请的;StartSSL是国际上用户量比较大的一个CA。
在某个时间,沃通收购了StartSSL,又在某个时间,Mozilla宣布暂时将沃通和StartSSL移出受信列表,原因是他们使用不再安全的SHA1算法且恶意欺骗浏览器,来龙动脉可以关注知乎话题 如何看待 Mozilla 决定停止信任沃通 (WoSign) 和 StartCom 颁发的证书?
后来,Safari和Chrome也跟随Mozilla取消了沃通和StartSSL的受信。不过,据说再后来的版本已经相继又恢复了这两个CA的受信。江湖实在险恶,我等凡夫俗子看个热闹就好。
我们的服务器现在用的是LetsEncrypt颁发的证书,通过测试,这个证书在各类客户端中兼容性都还不错,不过在Java7中还是不认识这个证书。
如何证明“我就是我”
刚才说的那套证书认证体系,要想工作得很好,就必须解决两个问题:
- 如何知道一个证书就是由某个CA签发的,而不是由别人伪造的
- 如何验证某个证书所认证的内容是否发生了改变
解决第1个问题
这就要用到之前说的非对称加密
技术了,流程大概是这个样子的:
- CA生成自己的一套证书,其中包含
公钥
和私钥
- CA将
私钥
放进自己的保险库,并雇佣黑帮7*24小时看守 - CA将
公钥
公之于众 - 验证者通过公开信息获取到这个CA的
公钥
,并将此CA列入受信列表 - CA用自己的
私钥
签发一个证书 - 验证者拿到证书后,根据证书上的标识找到这个CA,发现其在自己的受信列表中,于是找到这个CA的
公钥
,并使用公钥
来尝试解密证书内容 - 如果解密成功,则证书验证通过,否则验证失败
解决第2个问题
这就要用到之前说的签名
了。
每个证书除了包含对自己的身份验证信息外,还包含了对所认证内容的签名。验证者将需要验证的内容以相同的算法做签名,然后将此签名和证书上记录的签名做对比,如果签名相同,则表示内容无误,否则说明内容已经被修改了。
证书链(Certificate Chain)
在实际使用过程中,一个SSL证书很可能是多级认证的,也就是说A机构认证了B,B又认证了C,而证书最终是由C签发的。一般来讲,在这整个环节中,只有A是受信的CA,而B和C有可能是A自己生成的分级证书,或是A的合作机构。
下面是一张画得非常棒的图示,很好的说明了证书链的工作模式:
图片来自 http://blog.csdn.net/shen_guo/article/details/49891459
可以使用openssl
命令来查看某个网站的证书信息:
其中的0/1/2
就是证书链信息。
解决Java高版本中的证书问题
前文提到,Java从版本7开始,取消了大量的受信CA,很不幸我们自己的证书CA(LetsEncrypt)
也被取消了。好在各种前端系统还是认我们的证书的,所以这只是对我们后台程序之间互相通信产生了影响。
那如何让Java信任我们的CA呢?就是将我们的CA手动加入到信任列表中。有两种办法可以做到:
- 将CA根证书导入Java的受信CA列表,这样所有的Java程序都将信任此CA,但缺点是需要在所有机器上进行手动导入。
- 在程序中将CA根证书加入信任列表,这样只有在当前会话中,该CA才被认可,不对其它程序产生影响,且无需手动修改机器配置。
我决定使用第2种方式来实现我们的需求。
在zlhcommon
库中,包com.zhilehuo.server.zlhcommon.ssl
下面有两个类:CompositeX509TrustManager
和CompositeX509TrustManagerAgent
。其中,CompositeX509TrustManagerAgent
在CompositeX509TrustManager
的基础上,可以快速将沃通、StartSSL、LetsEncrypt证书添加至受信列表,也可以方便的将其它证书添加至受信列表。
此外,com.zhilehuo.server.zlhcommon.httptools.ZlhHttpClientBuilder
这个类,可以快速生成基于CompositeX509TrustManagerAgent
的CloseableHttpClient
,并结合SimpleRequest
类,进行HTTPS请求。
Java中自定义受信CA的坑
- 默认情况下,Java在设置了自定义CA后,系统就不再认识原有的CA列表,这就要求我们在请求之前,先判断目标站点是否是受信站点,如果不是,再加载我们自己的证书,这样比较麻烦
- 默认情况下,自定义CA每次只允许设置一个证书文件,无法同时将多个CA证书导入,这样实际情况下也会遇到一些麻烦
所以,我们才在
zlhcommon
库中,集成了CompositeX509TrustManager
类,来解决这些问题。