一次SSL握手的历史记录

哈Ha!

最近,我不得不将SSL与相互身份验证一起固定到Spring Reactive Webclient。 看来这很简单,但是它导致JDK源代码中的游荡结果出乎意料。 整篇文章都积累了经验,这可能对工程师的日常任务或准备面试很有用。


问题陈述


客户端有一个通过HTTPS运行的REST服务。
您必须从客户端Java应用程序访问它。

在此任务中,我得到的第一件事是2个扩展名为.pem的文件-客户端证书和私钥。 我使用Postman检查了它们的性能:我在设置中指定了它们的路径,并在拉出请求后确保服务器以200 OK响应和有意义的响应正文进行响应。 另外,我检查了没有客户端证书的情况,服务器返回的HTTP状态为500,并在响应的正文中返回一条短消息,说明发生了“安全异常”,并带有特定代码。

接下来,您应该正确配置客户端Java应用程序。

对于REST请求,我使用了具有非阻塞I / O的Spring Reactive WebClient。
该文档提供了一个示例,说明如何通过向其扔一个SslContext对象来自定义该对象,该对象仅存储证书和私钥。

我在简化版本中的配置几乎是从文档中复制粘贴的:

SslContext sslContext = SslContextBuilder .forClient() .keyManager(…) /*  ,   .pem      (, , ). PEM      . /    openssl .     KeyManagerFactory. */ .build(); ClientHttpConnector connector = new ReactorClientHttpConnector( builder -> builder.sslContext(sslContext)); WebClient webClient = WebClient.builder() .clientConnector(connector).build(); 

遵循TDD的原理,我还编写了一个使用WebTestClient而不是WebClient的测试,该测试显示了大量调试信息。 第一个断言是这样的:

 webTestClient .post() .uri([  ]) .body(BodyInserters.fromObject([ ,   ,  ])) .exchange() .expectStatus().isOk() 

这个简单的测试没有立即通过:服务器返回500的正文,就像Postman没有指定客户证书一样。

另外,我注意到在调试过程中,我打开了“不检查服务器证书”选项,即,我将InsecureTrustManagerFactory实例作为SslContext的TrustManager传递了。 这项措施是多余的,但可以肯定地排除了一半的选择。

测试中的调试信息并未说明问题,但看起来在SSL握手阶段出了点问题,因此我决定更详细地比较两种情况下连接的发生方式:对于Postman和Java客户端。 使用Wireshark可以看到所有这些信息-这是一种非常流行的网络流量分析器。 同时,我了解了双向身份验证如何进行SSL握手,可以说是实时的(他们喜欢在采访中问这个问题):

  • 首先,客户端发送一个客户端问候消息,其中包含元信息,例如协议版本和它支持的加密算法列表
  • 作为响应,服务器立即发送以下消息的包: 服务器Hello,证书,服务器密钥交换,证书请求,服务器Hello完成
    服务器问候表明服务器(密码套件)选择的加密算法。 证书中包含服务器证书。 服务器密钥交换根据选择的算法携带一些加密所需的信息(我们现在对详细信息不感兴趣,因此我们假定这只是一个公共密钥,尽管这是不正确的!)。 同样,在双向身份验证的情况下,服务器在“ 证书请求”消息中请求客户端证书,并说明其支持的格式以及所信任的颁发者。
  • 客户端收到此信息后,将验证服务器的证书,并在以下消息中发送其证书,“公钥”和其他信息: 证书,客户端密钥交换,证书验证 。 最后一个是ChangeCipherSpec消息,指示所有进一步的通信将以加密形式进行
  • 最后,经过所有这些改组之后,服务器将检查客户的证书,如果一切正常,它将给出答案。

在经过15分钟的流量后,我注意到Java客户端由于响应服务器的证书请求 ,由于某种原因没有发送其证书,这与Postman客户端不同。 即,有一个证书消息,但它为空。

接下来,我需要首先查看TLS协议规范 ,该协议的字面含义如下:
如果证书请求消息中的certificate_authorities列表是非空的,则证书链中的证书之一应由列出的CA之一颁发。
我们正在谈论服务器发出的“ 证书请求”消息中指定的certificate_authorities列表。 客户证书(至少一个链)必须由此列表中列出的发行者之一签名。 我们称其为X检查

我不知道这种情况,是在调试时达到JDK的深度时才发现的(我有JDK9)。 Netty的HttpClient是Spring WebClient的基础,默认情况下使用JDK中的SslEngine。 另外,您可以通过添加必要的依赖项将其切换到OpenSSL提供程序,但是我最终不需要它。
因此,我在sun.security.ssl.ClientHandshaker类和serverHelloDone消息的处理程序中设置了断点,发现未通过的X检查:客户端证书链中的所有颁发者都不在服务器信任的颁发者列表中(来自服务器的证书请求消息)。

我求助于客户以获取新证书,但客户反对一切对他都正常,并向Python提交了脚本,他通常使用脚本检查证书的功能。 该脚本只不过使用Requests库发送HTTPS请求,并返回200 OK。 当好旧的卷发也返回200 OK时,我终于感到惊讶。 我立刻想起了一个笑话:“整个公司步履蹒跚,中尉步履蹒跚。”

Curl当然是一个著名的实用程序,但是TLS标准也不是卫生纸。 我不知道要检查什么,我漫无目的地爬到curl文档上,在Github上发现了这样一个著名的bug。

记者准确地描述了X测试:在带有默认后端(OpenSSL)的curl中,它没有运行,与带有GnuTLS后端的curl不同。 我不太懒惰,使用--with-gnutls选项从源代码收集了curl,并发送了一个令人痛苦的请求。 最后,除了JDK之外,另一个客户端还返回了HTTP状态500和“安全异常”!

我根据“ Well,curl works”这一论点写给客户,并收到了新证书,重新生成并整齐地安装在服务器上。 有了它,我对WebClient的配置工作正常。 快乐的结局。

SSL史诗花了两个多星期的时间处理所有信件(包括研究详细的Java日志,挑选另一个对客户有用的项目的代码,以及讨好它)。

除了客户端行为的差异之外,很长一段时间以来困扰我的是,服务器的配置方式要求证书但不进行验证。 但是,在规范中对此有解释:
同样,如果证书链的某些方面是不可接受的(例如,它不是由已知的,受信任的CA签名的),则服务器可以自行决定继续握手(考虑客户端未经身份验证)或发送致命警报。

Source: https://habr.com/ru/post/zh-CN414909/


All Articles