【问题标题】:SSL Renegotiation with Client Certificate causes Server Buffer Overflow与客户端证书的 SSL 重新协商导致服务器缓冲区溢出
【发布时间】:2012-12-26 05:45:11
【问题描述】:

我编写了一个 Java 客户端应用程序,该应用程序使用客户端证书通过 HTTPS 连接到 Apache Web 服务器,并执行文件的 HTTP PUT 到服务器。它适用于小文件,但适用于大文件。

Apache 服务器日志显示如下:

...
OpenSSL: Handshake: done
...
Changed client verification type will force renegotiation
...
filling buffer, max size 131072 bytes
...
request body exceeds maximum size (131072) for SSL buffer
could not buffer message body to allow SSL renegotiation to proceed
...    
OpenSSL: I/O error, 5 bytes expected to read on BIO
(104)Connection reset by peer: SSL input filter read failed.
(32)Broken pipe: core_output_filter: writing data to the network
Connection closed to child 20 with standard shutdown

客户端的响应是:

java.io.IOException: Server returned HTTP response code: 401 for URL

我不熟悉这个过程,所以我不确定这里是否需要重新谈判,或者我是否可以采取一些措施来阻止它。或者我可以让客户端等到重新协商完成后再发送应用程序数据?这是客户端代码的摘录(已删除错误处理):

        URL url = new URL("my url goes here");
        con = (HttpsURLConnection) url.openConnection();
        con.setSSLSocketFactory(getMyCustomClientCertSocketFactory());
        con.setRequestMethod("PUT");
        con.setDoOutput(true);
        con.connect();
        writer = new OutputStreamWriter(con.getOutputStream());
        writer.write(xml);
        writer.close();

        parseServerResponse(con.getInputStream());

我在想也许我需要使用 SSLSocket 等较低级别的 API 并利用 HandshakeCompletedListener?

我还想知道 Apache SSLVerifyDepth 指令是否与重新协商发生的原因有关。我在每个目录上下文(只有一个上传目录)中得到了值为 2 的指令,Apache 手册对此进行了说明:

在每个目录的上下文中,它会强制与 读取 HTTP 请求后重新配置客户端验证深度 但在发送 HTTP 响应之前。

这里要求的是 Java 调试输出:

keyStore is : 
keyStore type is : jks
keyStore provider is : 
init keystore
init keymanager of type SunX509
trustStore is: C:\Program Files\Java\jdk1.6.0_35\jre\lib\security\cacerts
trustStore type is : jks
trustStore provider is : 
init truststore
adding as trusted cert:
 ...
trigger seeding of SecureRandom
done seeding SecureRandom
***
found key for : key-alias
chain [0] = [
[
...
]
***
trigger seeding of SecureRandom
done seeding SecureRandom
Allow unsafe renegotiation: false
Allow legacy hello messages: true
Is initial handshake: true
Is secure renegotiation: false
%% No cached client session
*** ClientHello, TLSv1
RandomCookie:  ...
Session ID:  {}
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA, SSL_DHE_DSS_WITH_DES_CBC_SHA, SSL_RSA_EXPORT_WITH_RC4_40_MD5, SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA, TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
Compression Methods:  { 0 }
***
main, WRITE: TLSv1 Handshake, length = 75
main, WRITE: SSLv2 client hello message, length = 101
main, READ: TLSv1 Handshake, length = 81
*** ServerHello, TLSv1
RandomCookie:  ...
Session ID:  ...
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA
Compression Method: 0
Extension renegotiation_info, renegotiated_connection: <empty>
***
%% Created:  [Session-1, TLS_RSA_WITH_AES_128_CBC_SHA]
** TLS_RSA_WITH_AES_128_CBC_SHA
main, READ: TLSv1 Handshake, length = 4392
*** Certificate chain
chain [0] = [
[
...
Certificate Extensions: 8
[1]: ObjectId: 1.3.6.1.5.5.7.1.1 Criticality=false
AuthorityInfoAccess [
  [
   accessMethod: ...
   accessLocation: URIName: ...
   accessMethod: ...
   accessLocation: URIName: ...
]

[2]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
...
]
]
[3]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
  CA:false
  PathLen: undefined
]
[4]: ObjectId: 2.5.29.31 Criticality=false
CRLDistributionPoints [
  [DistributionPoint:
     [URIName: ...
]]
[5]: ObjectId: 2.5.29.32 Criticality=false
CertificatePolicies [
  [CertificatePolicyId: ...
[PolicyQualifierInfo: [
  qualifierID: ...
  qualifier: ...
]]  ]
]
[6]: ObjectId: 2.5.29.37 Criticality=false
ExtendedKeyUsages [
  serverAuth
  clientAuth
]
[7]: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
  DigitalSignature
  Key_Encipherment
]
[8]: ObjectId: 2.5.29.17 Criticality=false
SubjectAlternativeName [
  DNSName: ...
]
]
  Algorithm: [SHA1withRSA]
  Signature:
...
]
...
***
main, READ: TLSv1 Handshake, length = 4
*** ServerHelloDone
*** ClientKeyExchange, RSA PreMasterSecret, TLSv1
main, WRITE: TLSv1 Handshake, length = 518
SESSION KEYGEN:
PreMaster Secret:
...
CONNECTION KEYGEN:
Client Nonce:
...
Server Nonce:
...
Master Secret:
...
Client MAC write Secret:
...
Server MAC write Secret:
...
Client write key:
...
Server write key:
...
Client write IV:
...
Server write IV:
...
main, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished
verify_data:  { 18, 162, 18, 251, 82, 111, 87, 133, 53, 240, 114, 155 }
***
main, WRITE: TLSv1 Handshake, length = 48
main, READ: TLSv1 Change Cipher Spec, length = 1
main, READ: TLSv1 Handshake, length = 48
*** Finished
verify_data:  { 46, 206, 8, 40, 63, 252, 99, 190, 251, 183, 110, 201 }
***
%% Cached client session: [Session-1, TLS_RSA_WITH_AES_128_CBC_SHA]
main, WRITE: TLSv1 Application Data, length = 256
main, WRITE: TLSv1 Application Data, length = 32
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 16416
...
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 512
main, READ: TLSv1 Application Data, length = 304 

这里要求的是 getMyCustomClientCertSocketFactory 源(从 PEM 文件中获取证书和密钥):

public static SSLSocketFactory getMyCustomClientCertSocketFactory(String pemPath,
        boolean verifyPeer)
        throws NoSuchAlgorithmException, FileNotFoundException, IOException,
        KeyStoreException, CertificateException, UnrecoverableKeyException,
        KeyManagementException, InvalidKeySpecException {
    SSLContext context = SSLContext.getInstance("TLS");

    byte[] certAndKey = IOUtil.fileToBytes(new File(pemPath));
    byte[] certBytes = parseDERFromPEM(certAndKey,
            "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
    byte[] keyBytes = parseDERFromPEM(certAndKey,
            "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");

    X509Certificate cert = generateX509CertificateFromDER(certBytes);
    RSAPrivateKey key = generateRSAPrivateKeyFromDER(keyBytes);

    KeyStore keystore = KeyStore.getInstance("JKS");
    keystore.load(null);
    keystore.setCertificateEntry("cert-alias", cert);
    keystore.setKeyEntry("key-alias", key, "changeit".toCharArray(),
            new Certificate[]{cert});

    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(keystore, "changeit".toCharArray());

    KeyManager[] km = kmf.getKeyManagers();

    TrustManager[] tm = null;

    if (!verifyPeer) {
        tm = new TrustManager[]{new TrustyTrustManager()};
    }

    context.init(km, tm, null);

    return context.getSocketFactory();
}

【问题讨论】:

  • 如果我使用 UNIX 实用程序“curl”,我可以毫无意外地传输大文件,所以我想知道它有什么不同...
  • 这是一个讨论该问题的错误报告:bugzilla.redhat.com/show_bug.cgi?id=491763。在使用 HttpsUrlConnections 时,在 PUT 之前在上传目录上发出 HTTP GET(或 HEAD 或 OPTIONS)似乎不起作用 - 我猜 keep-alive 没有得到尊重?
  • 使用 -Djavax.net.debug=ssl,handshake 运行您的客户端并将输出发布到您的答案中。
  • 调试输出过于冗长,无法粘贴到评论中,但基本上看起来应用程序数据在服务器甚至请求重新协商之前就已发送,到那时为时已晚,因为套接字关闭。有没有办法可靠地预测重新谈判? curl 有什么作用?
  • 将其编辑到您的问题中。在您这样做之前,没有人可以提供帮助。

标签: java apache ssl jsse


【解决方案1】:

java.io.IOException:服务器返回 HTTP 响应代码:URL 的 401

这是一个应用程序错误。它不是由 SSL 层引起的。我不知道为什么你会得到401 Unauthorized 以获得更大的文件,但你也忽略了getMyCustomClientCertSocketFactory()
你也尝试过另一种方法,例如POST?你有同样的问题吗?

【讨论】:

  • 是的,Apache 在重新协商期间缓冲区溢出时会发送 401。只要请求很大,使用 GET、POST、PUT 等都无关紧要——问题是如果应用程序数据大于缓冲区可以容纳的大小,服务器缓冲区就会溢出。是的,我可以增加缓冲区,问题就消失了,但这是拒绝服务的漏洞,并不理想。我没有提供 getMyCustomClientCertSocketFactory 的源代码,因为它只是切线相关的 - 为了使用带有 HttpsUrlConnection 的客户端证书,您必须设置自己的 SocketFactory。
  • I can bump the buffer up and the problem goes away, but this is a vulnerability for a denial of service and not ideal 所以你说问题出在 Apache 的缓冲区大小上?那么你应该增加它,否则它很容易受到 DoS 攻击。我的意思是即使你从客户端进行一些更改,如果 DoS 是您关心的问题,该问题仍然存在于服务器中
  • 我需要支持至少 64MB 的文件上传。默认情况下,Apache 中的缓冲区是相当抗 DoS 的 128KB。如果我将它提升到 64MB,那么让我的服务器停机将非常容易(普通用户可能会意外完成)。同样,真正的问题是 Java 客户端不够智能,无法防止服务器泛滥,而一些客户端(例如 curl)更智能,不会在重新协商期间淹没服务器。
  • 不,这不是应用程序错误,这是 Apache 发送的身份验证错误。
  • @EJP: 但是HTTP 401 是一个HTTP 错误并且HTTP 在SSL 之上运行。那么HTTP 响应是如何发送的握手问题?如果你能写一个答案来解释这一点,我会投赞成票,我相信其他人会觉得它有用。
【解决方案2】:

基于现在提供的所有额外信息,我认为您应该将 XML 编写成多个块而不是一个块。目前您正在编写一个块,它将被 SSL 分块成 16k 块,由于某种原因(它不应该)阻塞 Apache。我会尝试不大于 4k 的块大小。调整块大小直到它起作用。

一旦您解决了这个问题,您很可能会发现客户端证书问题。不要气馁,这证明你至少解决了这个问题。

【讨论】:

  • 我不明白这个答案。您是如何得出 Apache 在 16k 块上窒息的结论的?如果这是问题,那么我该如何更改块大小?
【解决方案3】:

Sun Java 中内置的 HttpsUrlConnection 工具似乎无法以服务器友好的方式处理带有客户端证书的大型 HTTP PUT 方案(即不会溢出服务器 SSL 重新协商缓冲区)。

我检查了 curl 所做的事情以了解“服务器友好的含义”,结果发现有一个名为“Expect”的 HTTP 1.1 标头,curl 发送的值为“100-continue”(参见规范http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20) .这个标头本质上是说“我有一个巨大的有效载荷,但在我发送它之前,请告诉我你是否可以处理它”。这使端点有时间在发送有效负载之前重新协商客户端证书。

在 Sun HttpUrlConnection 实现中,似乎不允许使用此标头,实际上它位于受限标头列表中;这意味着即使您使用 HttpUrlConnection.setRequestProperty 方法设置它,标头也不会实际发送到服务器。您可以使用系统属性 sun.net.http.allowRestrictedHeaders 覆盖受限制的标头,但是由于 Sun 实现不知道如何处理协议的这一部分,因此客户端只会因套接字异常而崩溃。

有趣的是,Java 的 OpenJDK 实现似乎确实支持此标头。此外,Apache HTTP 客户端库支持此标头 (http://hc.apache.org/);我已经使用 Apache HTTP 客户端库实现了一个测试程序,它可以使用客户端证书和 Expect 标头成功执行大文件的 HTTP PUT 请求。

总结一下,解决方案是:

  1. 将 Apache SSLRenegBufferSize 指令设置为一个巨大的数字(如 64MB)。默认值为 128K。此解决方案可能会产生拒绝服务风险
  2. 配置一个始终需要客户端证书的主机,而不是只有少数目录需要它的主机。这样可以避免重新谈判。在我的场景中,这不是一个好的选择,因为大多数用户都是匿名的或用户名/密码经过身份验证的。只有一个上传目录用于以编程方式上传文件。我们必须为这个目录创建一个具有自己 SSL 证书的新虚拟主机。
  3. 使用支持 HTTP 1.1 Expect 标头的客户端。不幸的是,Sun Java 不支持这个开箱即用。必须使用第三方(例如 Apache HTTP 组件客户端库)或使用 Java 套接字 API 推出您自己的解决方案。
  4. 利用 HTTP 1.1 持久连接(使用 keep-alive 进行管道传输),首先发出一个没有大负载但会导致重新协商的 HTTP 请求,然后将连接重用于 HTTP PUT。从理论上讲,客户端应该能够在上传目录上发出 HTTP HEAD 或 OPTIONS,然后重用相同的连接来执行 PUT。为了使其工作,持久连接池可能只需要包含一个连接,以避免“启动”一个连接,然后为 PUT 发出另一个连接。但是,HttpUrlConnection 类似乎不会保留/重用涉及客户端证书或 SSL 的持久连接,因为我无法让这个解决方案正常工作。请参阅 (HttpsUrlConnection and keep-alive)。

【讨论】:

  • 看起来 Java 7 实际上支持 Expect 标头。在我的测试中,我让它与 ChunkedStreamingMode 一起工作。所以有两行新代码: con.setChunkedStreamingMode(0);和 con.setRequestProperty("Expect", "100-Continue");
猜你喜欢
  • 2013-01-07
  • 2019-03-11
  • 2015-10-20
  • 2016-08-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-09-05
相关资源
最近更新 更多