本文不涉及 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
类,来解决这些问题。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于