使用 TLS PSK 加密时如何正确检测流的结尾?
How to detect an end of stream properly, when TLS PSK encryption is used?
我已经根据 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()
应该在流的末尾 return -1 而不会抛出异常。
当使用 TLS PSK 加密时,如何正确检测流结束?
从长远来看,我想将我的测试用例扩展到在嵌入式 Jetty 前面充当反向 PSK TLS 代理的程序 - 并且不希望依赖异常来检测客户端是否已完成读取或写入.
EOFException 被抛出(自 v1.56 起),因为未收到所需的 close_notify 警报。这意味着TLS层不能排除应用数据被截断的可能性。
截断意味着您到目前为止收到的数据已正确传输(根据活动密码套件),但可能还有更多数据您没有收到。截断可能是意外的或恶意的。对于许多应用程序,后面的数据可能会影响前面数据的含义,因此截断可能会任意改变语义。
对于某些应用程序协议,可以确定没有实际截断(即只是缺少 close_notify)- 考虑 HTTP Content-Length header,或者某些或者所有被截断的数据可能仍然被有效地接受 - 考虑 self-delimiting 个独立消息流。这不能在 TLS 层本身完成;或者更确切地说,它是通过要求 close_notify!
来完成的
因此,EOFException 被引发为“[信号] 在输入过程中意外到达文件末尾或流末尾”。此时,应用程序应保守地假设数据已被截断,但 application-specific 机制可能仍允许接受部分或全部数据,如上所述。
从(尚未发布)v1.57 开始,我们添加了 TlsNoCloseNotifyException 作为 EOFException 的子类,在这种特定情况下将 only/always 抛出,希望允许更简单的应用程序代码。
我已经根据 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()
应该在流的末尾 return -1 而不会抛出异常。
当使用 TLS PSK 加密时,如何正确检测流结束?
从长远来看,我想将我的测试用例扩展到在嵌入式 Jetty 前面充当反向 PSK TLS 代理的程序 - 并且不希望依赖异常来检测客户端是否已完成读取或写入.
EOFException 被抛出(自 v1.56 起),因为未收到所需的 close_notify 警报。这意味着TLS层不能排除应用数据被截断的可能性。
截断意味着您到目前为止收到的数据已正确传输(根据活动密码套件),但可能还有更多数据您没有收到。截断可能是意外的或恶意的。对于许多应用程序,后面的数据可能会影响前面数据的含义,因此截断可能会任意改变语义。
对于某些应用程序协议,可以确定没有实际截断(即只是缺少 close_notify)- 考虑 HTTP Content-Length header,或者某些或者所有被截断的数据可能仍然被有效地接受 - 考虑 self-delimiting 个独立消息流。这不能在 TLS 层本身完成;或者更确切地说,它是通过要求 close_notify!
来完成的因此,EOFException 被引发为“[信号] 在输入过程中意外到达文件末尾或流末尾”。此时,应用程序应保守地假设数据已被截断,但 application-specific 机制可能仍允许接受部分或全部数据,如上所述。
从(尚未发布)v1.57 开始,我们添加了 TlsNoCloseNotifyException 作为 EOFException 的子类,在这种特定情况下将 only/always 抛出,希望允许更简单的应用程序代码。