当 curl 成功时,Java HTTPS 客户端失败 SSL 握手

Posted

技术标签:

【中文标题】当 curl 成功时,Java HTTPS 客户端失败 SSL 握手【英文标题】:Java HTTPS client fails SSL handshake while curl succeeds 【发布时间】:2018-12-06 17:09:52 【问题描述】:

先道歉,我对 SSL 编码还是很陌生。过去几天我一直在寻找答案,虽然我找到了很多建议,但到目前为止都没有任何效果。

我拥有的是在 Dropwizard 之上实现的服务器,它需要接受传入的 HTTPS 连接并使用附加的证书来唯一标识客户端。我目前正在使用所有自签名证书,而我正在开发中。服务器证书对是使用链 - 根对 -> 中间对 -> 服务器对创建的。服务器的 P12 是使用中间证书和服务器证书以及服务器私钥的串联创建的。然后它被添加到一个空的 JKS 并成为服务器的 KeyStore。

另外,我创建了两个客户端证书,一个使用相同的中间证书对作为基础证书,另一个作为纯独立证书对。这两个证书对的 x509 公钥部分被添加到 JKS 文件中,并成为服务器的 TrustStore。 Dropwizard 配置如下:

type: "https"
port: "9843"
keyStorePath: "keystore.jks"
keyStorePassword: "changeme"
keyStoreType: "JKS"
trustStorePath: "truststore.jks"
trustStorePassword: "changeme"
trustStoreType: "JKS"
allowRenegotiation: false
validateCerts: false
validatePeers: false
needClientAuth: true
wantClientAuth: true

我可以使用curl 和任一客户端证书对连接到服务器:

curl -v --cert client.pem --key client.key -k https://localhost:9843/v1/ld 

启用 SSL 调试后,服务器会记录以下内容:

*** CertificateRequest
Cert Types: RSA, DSS, ECDSA
Supported Signature Algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA256withDSA, SHA224withECDSA, SHA224withRSA, SHA224withDSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA
Cert Authorities:
<CN=*.me.com, O=Me, ST=Massachusetts, C=US>
<O=Internet Widgits Pty Ltd, ST=Massachusetts, C=US>
*** ServerHelloDone
dw-51, WRITE: TLSv1.2 Handshake, length = 3536
dw-44, READ: TLSv1.2 Handshake, length = 1047
*** Certificate chain
chain [0] = [
[
  Version: V3
  Subject: O=Internet Widgits Pty Ltd, L=North Reading, ST=Massachusetts, C=US
  Signature Algorithm: SHA1withRSA, OID = 1.2.840.113549.1.1.5

  Key:  Sun RSA public key, 2048 bits
  modulus: 23250299629324311533731283912176366463399376328149948822580485256237233115567136794461732268017120297060017586981907979910958857247642884566364833267711927344361604478514119965230314679194017013023991389216461419030751049820266939279047536006291610734616600760688907006770883510297954698233112783686968024400749969025850008781641616624298935923926427096257861170476293580684942956111432790304698635393966967864288730561678135798437678912431564767611000006312358137647455886578135011989168265295083928014176435879778838966450081419161406209555593636745048857672445188811541416453143809594265089422302064600885289819601
  public exponent: 65537
  Validity: [From: Wed Dec 05 13:52:49 EST 2018,
               To: Thu Dec 05 13:52:49 EST 2019]
  Issuer: O=Internet Widgits Pty Ltd, ST=Massachusetts, C=US
  SerialNumber: [    8174655c c8387da4]

Certificate Extensions: 3
...

到目前为止一切顺利。接下来,我尝试使用 Java 客户端连接到我的服务器,该客户端使用相同的证书对,组合在一个 P12 文件中。 Java 代码如下:

char[] password = "changeme".toCharArray();
KeyStore keystore = KeyStore.getInstance("PKCS12");
try (FileInputStream fileInputStream = new FileInputStream("client.p12")) 
    keystore.load(fileInputStream, password);


SSLContext sslContext = 
        SSLContexts.custom()
                   .loadKeyMaterial(keystore, password)
                   .loadTrustMaterial(null, (chain, authType) -> true)
                   .build();

return HttpClients.custom()
                  .setSSLContext(sslContext)
                  .setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
                  .build();

但是当这个客户端尝试连接服务器时,会记录以下内容:

*** CertificateRequest
Cert Types: RSA, DSS, ECDSA
Supported Signature Algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA256withDSA, SHA224withECDSA, SHA224withRSA, SHA224withDSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA
Cert Authorities:
<CN=*.me.com, O=Me, ST=Massachusetts, C=US>
<O=Internet Widgits Pty Ltd, ST=Massachusetts, C=US>
*** ServerHelloDone
Warning: no suitable certificate found - continuing without client authentication
*** Certificate chain
<Empty>
***

我还尝试使用使用 sslContext 初始化的 SSLConnectionSocketFactory,以及为“https”注册 socketFactory 的 Registry&lt;ConnectionSocketFactory&gt;。没有任何效果。我完全不知道为什么curl 接受证书颁发机构并发送客户端证书但 java httpClient 不接受。

编辑:

我尝试将服务器的公共证书添加到客户端请求中,但没有任何区别 - 我仍然看到相同的行为。代码更新如下:

KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
try (FileInputStream fileInputStream = new FileInputStream("server.cert.pem")) 
    try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(IOUtils.toByteArray(fileInputStream))) 
        CertificateFactory certificateFactory = CertificateFactory.getInstance("X509");
        Certificate certificate = certificateFactory.generateCertificate(byteArrayInputStream);
        trustStore.setCertificateEntry("server", certificate);
    


SSLContext sslContext =
        SSLContexts.custom()
                   .loadKeyMaterial(keystore, password)
                   .loadTrustMaterial(trustStore, (chain, authType) -> true)
                   .build();

【问题讨论】:

确保服务器发送一个它期望证书来自的 CA 列表,并且在这个列表中你有颁发客户端证书的 CA。看***.com/a/1710543/6368697 感谢@PatrickMevzek 的链接。根据调试日志,服务器正在发送客户端的 CA:&lt;O=Internet Widgits Pty Ltd, ST=Massachusetts, C=US&gt;。这是客户端用来连接的证书。我还尝试将服务器的公共证书添加到客户端的信任库中(请参阅编辑),但这似乎没有什么不同。 您的代码在我看来是正确的(至少对于定义为“接受任何服务器,即使它是假的”的“正确”)。在客户端尝试 javax.net.debug 并查看开头(握手之前),它应该显示至少使用 found key for: $alias / chain [0] = [ $certcontents ... ] ... 初始化的密钥管理器 - 确保这是您期望/想要的证书(和密钥条目)。 【参考方案1】:

所以我最终完全重新设计了我的证书的生成方式,并且我能够让事情正常进行,但需要注意的是:这取决于证书的生成方式。

适用于 Java 客户端、curl 和 Postman:

openssl genrsa -aes256 -out private/$SVRNAME.key.pem 2048
openssl req -config $CONFIGDIR/openssl.cnf \
      -new -x509 -days 7300 -sha256 -extensions v3_ca \
      -key private/$SVRNAME.key.pem \
      -out certs/$SVRNAME.cert.pem

适用于 curl 和 Postman,但不适用于 Java 客户端:

openssl req -newkey rsa:2048 -nodes \
      -keyout private/$CLINAME.key.pem \
      -x509 -days 365 -out certs/$CLINAME.cert.pem

不确定为什么“快速”证书会导致这么多问题,但至少它现在可以正常工作。感谢 Patrick 和 Dave 的帮助!

【讨论】:

以上是关于当 curl 成功时,Java HTTPS 客户端失败 SSL 握手的主要内容,如果未能解决你的问题,请参考以下文章

Azure:如何获得刷新令牌?当连接的输出仅提供访问令牌时使用 Curl

iOS https请求对自签名证书忽略

用户中止后在后台运行 PHP + Curl

Gitlab CI/CD 如何在管道中捕获 curl 响应

URL 错误 0:cURL 请求重试了 3 次,没有成功

怎么判断curl调用接口返回成功:响应码