使用 TLS PSK 加密时如何正确检测流的结束?

Posted

技术标签:

【中文标题】使用 TLS PSK 加密时如何正确检测流的结束?【英文标题】:How to detect an end of stream properly, when TLS PSK encryption is used? 【发布时间】:2016-11-16 16:13:28 【问题描述】:

我根据 Bouncy Castle 的 MockPSKTlsClient 准备了 a simple TLS PSK client test case。

在我调用的main方法中:

public static void main(String[] args) throws IOException 
    SecureRandom random      = new SecureRandom();
    TlsPSKIdentity identity  = new BasicTlsPSKIdentity("Client_identity", Hex.decode("1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A"));
    Socket socket            = new Socket(InetAddress.getLocalHost(), 12345);
    TlsClientProtocol proto  = new TlsClientProtocol(socket.getInputStream(), socket.getOutputStream(), random);
    MockPSKTlsClient client  = new MockPSKTlsClient(null, identity);
    proto.connect(client);

    OutputStream clearOs = proto.getOutputStream();
    InputStream clearIs = proto.getInputStream();
    clearOs.write("GET / HTTP/1.1\r\n\r\n".getBytes("UTF-8"));
    Streams.pipeAll(clearIs, System.out);   // why is java.io.EOFException thrown?

如你所见,我向openssl服务器发送了一个GET / HTTP/1.1字符串,启动为:

# openssl s_server \
        -psk 1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A \
        -psk_hint Client_identity\
        -cipher PSK-AES256-CBC-SHA \
        -debug -state -nocert -accept 12345 -tls1_2 -www

之后我调用Streams.pipeAll()方法,它只是:

public static void pipeAll(InputStream inStr, OutputStream outStr)
    throws IOException

    byte[] bs = new byte[BUFFER_SIZE];
    int numRead;
    while ((numRead = inStr.read(bs, 0, bs.length)) >= 0) // Why is EOFException thrown?
    
        outStr.write(bs, 0, numRead);
    

这会将openssl s_server 的答案复制到屏幕上,并且令人惊讶地在最后抛出一个EOFException

TLS-PSK client negotiated TLS 1.2
Established session: 68e647e3276f345e82effdb7cc04649f6872d245ae01489c08ed109c5906dd16
HTTP/1.0 200 ok
Content-type: text/html

<HTML><BODY BGCOLOR="#ffffff">
<pre>

s_server -psk 1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A1A -psk_hint Client_identity -cipher PSK-AES256-CBC-SHA -debug -state -nocert -accept 12345 -tls1_2 -www 
Secure Renegotiation IS supported
Ciphers supported in s_server binary
TLSv1/SSLv3:PSK-AES256-CBC-SHA       
---
Ciphers common between both SSL end points:
PSK-AES256-CBC-SHA
Signature Algorithms: RSA+SHA1:RSA+SHA224:RSA+SHA256:RSA+SHA384:RSA+SHA512:DSA+SHA1:DSA+SHA224:DSA+SHA256:DSA+SHA384:DSA+SHA512:ECDSA+SHA1:ECDSA+SHA224:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512
Shared Signature Algorithms: RSA+SHA1:RSA+SHA224:RSA+SHA256:RSA+SHA384:RSA+SHA512:DSA+SHA1:DSA+SHA224:DSA+SHA256:DSA+SHA384:DSA+SHA512:ECDSA+SHA1:ECDSA+SHA224:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512
---
New, TLSv1/SSLv3, Cipher is PSK-AES256-CBC-SHA
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : PSK-AES256-CBC-SHA
    Session-ID: 68E647E3276F345E82EFFDB7CC04649F6872D245AE01489C08ED109C5906DD16
    Session-ID-ctx: 01000000
    Master-Key: B023F1053230C2938E1D3FD6D73FEB41DEC3FC1068A390FE6DCFD60A6ED666CA2AD0CD1DAD504A087BE322DD2C870C0C
    Key-Arg   : None
    PSK identity: Client_identity
    PSK identity hint: Client_identity
    SRP username: None
    Start Time: 1479312253
    Timeout   : 7200 (sec)
    Verify return code: 0 (ok)
---
  13 items in the session cache
   0 client connects (SSL_connect())
   0 client renegotiates (SSL_connect())
   0 client connects that finished
  14 server accepts (SSL_accept())
   0 server renegotiates (SSL_accept())
  13 server accepts that finished
   0 session cache hits
   0 session cache misses
   0 session cache timeouts
   0 callback cache hits
   0 cache full overflows (128 allowed)
---
no client certificate available
</BODY></HTML>

TLS-PSK client raised alert: fatal(2), internal_error(80)
> Failed to read record
java.io.EOFException
    at org.bouncycastle.crypto.tls.TlsProtocol.safeReadRecord(Unknown Source)
    at org.bouncycastle.crypto.tls.TlsProtocol.readApplicationData(Unknown Source)
    at org.bouncycastle.crypto.tls.TlsInputStream.read(Unknown Source)
    at de.afarber.tlspskclient2.Main.pipeAll(Main.java:52)
    at de.afarber.tlspskclient2.Main.main(Main.java:44)
Exception in thread "main" java.io.IOException: Internal TLS error, this could be an attack
    at org.bouncycastle.crypto.tls.TlsProtocol.failWithError(Unknown Source)
    at org.bouncycastle.crypto.tls.TlsProtocol.safeReadRecord(Unknown Source)
    at org.bouncycastle.crypto.tls.TlsProtocol.readApplicationData(Unknown Source)
    at org.bouncycastle.crypto.tls.TlsInputStream.read(Unknown Source)
    at de.afarber.tlspskclient2.Main.pipeAll(Main.java:52)
    at de.afarber.tlspskclient2.Main.main(Main.java:44)

我的问题是:为什么会抛出EOFException

通常InputStream.read() 应该在流结束时返回 -1 而不会抛出异常。

从长远来看,我想将我的测试用例扩展到在嵌入式 Jetty 前充当反向 PSK TLS 代理的程序 - 并且不希望依赖异常来检测客户端是否已完成读取或写入。

【问题讨论】:

它应该在流结束时返回 -1,而不是零。这对于 BouncyCastle 来说无疑是糟糕的设计。它应该抛出一个SSLException,因为他可能会发生截断攻击。 请注意,它不是“在 TLS 解码流的末尾”。这是一个截断。查看堆栈跟踪。 你说得对,InputStream.read()应该返回-1。我已经更新了我的问题,谢谢。我仍然想知道如何正确检测 TLS 流的结束。 正在正确检测到它。这里的问题是可能的截断攻击。这就是您需要调查的内容。 这里有一些最初的混淆,所以我想强调一下,BouncyCastle (Java) 确实用 -1 表示普通的流结束,并且总是这样。特别是,EOFException(从 1.57 开始,TlsNoCloseNotifyException)从不用于普通的流结束,但仅在未收到预期的 close_notify 警报时使用。 【参考方案1】:

EOFException 被抛出(从 v1.56 开始),因为没有收到所需的 close_notify 警报。这意味着 TLS 层不能排除应用数据被截断的可能性。

截断表示您目前收到的数据是正确传输的(根据活动密码套件),但您可能还有更多数据未收到。截断可能是偶然的或恶意的。对于许多应用程序来说,后面的数据可能会影响前面数据的含义,因此截断可能会任意改变语义。

对于某些应用程序协议,可以确定没有实际截断(即只是缺少 close_notify) - 考虑 HTTP Content-Length 标头,或者部分或全部截断的数据可能仍被有用地接受 -考虑一个自定界的、独立的消息流。这不能在 TLS 层本身中完成;或者更确切地说,它是通过要求 close_notify 来完成的!

因此,EOFException 被引发为“[信号] 在输入期间意外到达文件结尾或流结尾”。此时,应用程序应保守地假设数据已被截断,但特定于应用程序的机制可能仍允许接受部分或全部数据,如上所述。

从(尚未发布)v1.57 开始,我们添加了 TlsNoCloseNotifyException 作为 EOFException 的子类,它只会/总是在这种特定情况下被抛出,希望允许更简单的应用程序代码。

【讨论】:

这肯定是openssl s_server的bug? 在我看来这是一个错误,是的,但大概只存在于 s_server 实用程序的细节中,不一定是“真正的”基于 openssl 的服务器的特性。

以上是关于使用 TLS PSK 加密时如何正确检测流的结束?的主要内容,如果未能解决你的问题,请参考以下文章

OpenSSL TLS/DTLS PSK

[skill][https][ssl/tls] HTTPS相关知识汇总

是否有公共 psk 服务器来测试 tls 握手?

TLS中PSK的简要介绍

如何在 Java 中使用 PSK 通过 MQTT 建立安全通信?

OpenSSL 1.1.1 PSK TLS1.3 - TLS_256_GCM_SHA384 密码套件没有合适的签名算法错误