[英]How to connect to FTPS server with data connection using same TLS session?
Environment: I'm using Sun Java JDK 1.8.0_60 on 64-bit Windows 7, using Spring Integration 4.1.6 (which internally appears to use Apache Commons Net 3.3 for FTPS access).环境:我在 64 位 Windows 7 上使用 Sun Java JDK 1.8.0_60,使用 Spring Integration 4.1.6(内部似乎使用 Apache Commons Net 3.3 进行 FTPS 访问)。
I'm attempting to integrate with our application an automatic download from our client's FTPS server.我正在尝试与我们的应用程序集成,从我们客户的 FTPS 服务器自动下载。 I've done so successfully with SFTP servers using Spring Integration without any trouble for other clients without issues, but this is the first time a client has required us to use FTPS, and getting it to connect has been very puzzling.
我已经使用 Spring Integration 成功地使用 SFTP 服务器完成了此操作,对于其他客户端没有任何问题,但这是第一次客户端要求我们使用 FTPS,并且让它连接起来非常令人费解。 While in my real application I'm configuring Spring Integration using XML beans, to try to understand what's not working I'm using the following test code (though I'm anonymizing the actual host/username/password here):
在我的真实应用程序中,我正在使用 XML bean 配置 Spring Integration,为了尝试了解什么不起作用,我正在使用以下测试代码(尽管我在这里匿名化了实际的主机/用户名/密码):
final DefaultFtpsSessionFactory sessionFactory = new DefaultFtpsSessionFactory();
sessionFactory.setHost("XXXXXXXXX");
sessionFactory.setPort(990);
sessionFactory.setUsername("XXXXXXX");
sessionFactory.setPassword("XXXXXXX");
sessionFactory.setClientMode(2);
sessionFactory.setFileType(2);
sessionFactory.setUseClientMode(true);
sessionFactory.setImplicit(true);
sessionFactory.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
sessionFactory.setProt("P");
sessionFactory.setProtocol("TLSv1.2");
sessionFactory.setProtocols(new String[]{"TLSv1.2"});
sessionFactory.setSessionCreation(true);
sessionFactory.setCipherSuites(new String[]{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"});
final FtpSession session = sessionFactory.getSession();
//try {
final FTPFile[] ftpFiles = session.list("/");
logger.debug("FtpFiles: {}", (Object[]) ftpFiles);
//} catch (Exception ignored ) {}
session.close();
I'm running this code with -Djavax.net.debug=all
to get all the TLS debugging information printed.我正在使用
-Djavax.net.debug=all
运行此代码以打印所有 TLS 调试信息。
The main "control" connection to the FTPS server works fine, but when it tries to open the data connection for the list (or any other data connection I've tried), I get a javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
, caused by java.io.EOFException: SSL peer shut down incorrectly
.到 FTPS 服务器的主要“控制”连接工作正常,但是当它尝试打开列表的数据连接(或我尝试过的任何其他数据连接)时,我得到一个
javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
,由java.io.EOFException: SSL peer shut down incorrectly
closed wrongly 引起。 If I uncomment the swallowing-exceptions catch block around the session.list
command, then I can see (though the javax.net.debug output) that the server sent the following message after rejecting the data connection SSL handshake:如果我取消注释
session.list
命令周围的吞咽异常捕获块,那么我可以看到(尽管 javax.net.debug 输出)服务器在拒绝数据连接 SSL 握手后发送了以下消息:
main, READ: TLSv1.2 Application Data, length = 129
Padded plaintext after DECRYPTION: len = 105
0000: 34 35 30 20 54 4C 53 20 73 65 73 73 69 6F 6E 20 450 TLS session
0010: 6F 66 20 64 61 74 61 20 63 6F 6E 6E 65 63 74 69 of data connecti
0020: 6F 6E 20 68 61 73 20 6E 6F 74 20 72 65 73 75 6D on has not resum
0030: 65 64 20 6F 72 20 74 68 65 20 73 65 73 73 69 6F ed or the sessio
0040: 6E 20 64 6F 65 73 20 6E 6F 74 20 6D 61 74 63 68 n does not match
0050: 20 74 68 65 20 63 6F 6E 74 72 6F 6C 20 63 6F 6E the control con
0060: 6E 65 63 74 69 6F 6E 0D 0A nection..
What appears to be happening (and this is my first time dealing with FTPS, though I've dealt with plain FTP before) is that the way the server ensures authentication and encryption over both the control and data connections is that after a "normal" TLS connection to establish the control connection and authentication happens there, each data connection requires the client to connect with the same TLS session.似乎正在发生的事情(这是我第一次处理 FTPS,尽管我之前处理过普通 FTP)是服务器确保对控制和数据连接进行身份验证和加密的方式是在“正常”之后TLS连接建立控制连接和身份验证发生在那里,每个数据连接都要求客户端连接相同的TLS会话。 This makes sense to me as how it's supposed to be work, but the Apache Commons Net FTPS implementation doesn't seem to be doing that.
这对我来说是有意义的,因为它应该是如何工作的,但是 Apache Commons Net FTPS 实现似乎并没有这样做。 It seems to be trying to establish a new TLS session, and so the server is rejecting the attempt.
它似乎正在尝试建立一个新的 TLS 会话,因此服务器拒绝了该尝试。
Based on this question about resuming SSL sessions in JSSE , it appears that Java assumes or requires a different session for each host/post combination.基于这个关于在 JSSE 中恢复 SSL 会话的问题,Java 似乎为每个主机/帖子组合假定或要求不同的会话。 My hypothesis is that since the FTPS data connection is on a different port than the control connection, it's not finding the existing session and is trying to establish a new one, so the connection fails.
我的假设是,由于 FTPS 数据连接与控制连接位于不同的端口上,因此它没有找到现有会话并试图建立一个新会话,因此连接失败。
I see three main possibilities:我看到三种主要可能性:
I'd appreciate any direction you can provide as to how to connect to this kind of FTPS server.我很感激你能提供关于如何连接到这种 FTPS 服务器的任何指导。 Thank you.
谢谢你。
Indeed some FTP(S) servers do require that the TLS/SSL session is reused for the data connection.事实上,一些 FTP(S) 服务器确实要求将 TLS/SSL 会话重用于数据连接。 This is a security measure by which the server can verify that the data connection is used by the same client as the control connection.
这是一种安全措施,服务器可以通过它来验证数据连接是否由与控制连接相同的客户端使用。
Some references for common FTP servers:一些常见的FTP服务器参考:
NoSessionReuseRequired
directive) NoSessionReuseRequired
指令) What may help you with the implementation is that Cyberduck FTP(S) client does support TLS/SSL session reuse and it uses Apache Commons Net library: Cyberduck FTP(S) 客户端确实支持 TLS/SSL 会话重用,并且它使用 Apache Commons Net 库,这可能对您的实施有所帮助:
https://github.com/iterate-ch/cyberduck/issues/5087 – Reuse Session key on data connection https://github.com/iterate-ch/cyberduck/issues/5087 - 在数据连接上重用会话密钥
See its FTPClient.java
code (extends Commons Net FTPSClient
), particularly its override of _prepareDataSocket_
method :请参阅它的
FTPClient.java
代码(扩展 Commons Net FTPSClient
),特别是它对_prepareDataSocket_
方法的覆盖:
@Override protected void _prepareDataSocket_(final Socket socket) { if(preferences.getBoolean("ftp.tls.session.requirereuse")) { if(socket instanceof SSLSocket) { // Control socket is SSL final SSLSession session = ((SSLSocket) _socket_).getSession(); if(session.isValid()) { final SSLSessionContext context = session.getSessionContext(); context.setSessionCacheSize(preferences.getInteger("ftp.ssl.session.cache.size")); try { final Field sessionHostPortCache = context.getClass().getDeclaredField("sessionHostPortCache"); sessionHostPortCache.setAccessible(true); final Object cache = sessionHostPortCache.get(context); final Method putMethod = cache.getClass().getDeclaredMethod("put", Object.class, Object.class); putMethod.setAccessible(true); Method getHostMethod; try { getHostMethod = socket.getClass().getMethod("getPeerHost"); } catch(NoSuchMethodException e) { // Running in IKVM getHostMethod = socket.getClass().getDeclaredMethod("getHost"); } getHostMethod.setAccessible(true); Object peerHost = getHostMethod.invoke(socket); putMethod.invoke(cache, String.format("%s:%s", peerHost, socket.getPort()).toLowerCase(Locale.ROOT), session); } catch(NoSuchFieldException e) { // Not running in expected JRE log.warn("No field sessionHostPortCache in SSLSessionContext", e); } catch(Exception e) { // Not running in expected JRE log.warn(e.getMessage()); } } else { log.warn(String.format("SSL session %s for socket %s is not rejoinable", session, socket)); } } } }
It seems that the _prepareDataSocket_
method was added to Commons Net FTPSClient
specifically to allow the TLS/SSL session reuse implementation:似乎
_prepareDataSocket_
方法被添加到 Commons Net FTPSClient
,专门用于允许 TLS/SSL 会话重用实现:
https://issues.apache.org/jira/browse/NET-426 https://issues.apache.org/jira/browse/NET-426
A native support for the reuse is still pending:对重用的本机支持仍然悬而未决:
https://issues.apache.org/jira/browse/NET-408 https://issues.apache.org/jira/browse/NET-408
You will obviously need to override the Spring Integration DefaultFtpsSessionFactory.createClientInstance()
to return your custom FTPSClient
implementation with the session reuse support.您显然需要覆盖 Spring Integration
DefaultFtpsSessionFactory.createClientInstance()
以返回具有会话重用支持的自定义FTPSClient
实现。
The above solution does not work on its own anymore since JDK 8u161.自 JDK 8u161 以来,上述解决方案不再单独工作。
According to JDK 8u161 Update Release Notes (and the answer by @Laurent ):根据JDK 8u161 Update Release Notes (以及@Laurent 的回答):
Added TLS session hash and extended master secret extension support
添加了 TLS 会话哈希和扩展的主密钥扩展支持
......
In case of compatibility issues, an application may disable negotiation of this extension by setting the System Propertyjdk.tls.useExtendedMasterSecret
tofalse
in the JDK如果出现兼容性问题,应用程序可以通过在 JDK 中将系统属性
jdk.tls.useExtendedMasterSecret
设置为false
来禁用此扩展的协商
Ie, you can call this to fix the problem (you still need to override the _prepareDataSocket_
):即,您可以调用它来解决问题(您仍然需要覆盖
_prepareDataSocket_
):
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
Though this should be a considered a workaround only.虽然这应该只是一种解决方法。 I do not know a proper solution.
我不知道一个合适的解决方案。
An alternative implementation is here:另一种实现在这里:
https://issues.apache.org/jira/browse/NET-408 https://issues.apache.org/jira/browse/NET-408
There's a separate question about problems in 1.8.0_161:关于 1.8.0_161 中的问题有一个单独的问题:
SSL Session reuse in Apache FTPS client in JDK 8u161 JDK 8u161 中 Apache FTPS 客户端中的 SSL 会话重用
I actually had the same problem in the past (just in C++/OpenSSL, I do not do Java), so I knew what to google for.实际上,我过去也遇到过同样的问题(只是在 C++/OpenSSL 中,我不使用 Java),所以我知道该用谷歌搜索什么。
You can use this SSLSessionReuseFTPSClient class :您可以使用此 SSLSessionReuseFTPSClient 类:
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.Locale;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSessionContext;
import javax.net.ssl.SSLSocket;
import org.apache.commons.net.ftp.FTPSClient;
public class SSLSessionReuseFTPSClient extends FTPSClient {
// adapted from:
// https://trac.cyberduck.io/browser/trunk/ftp/src/main/java/ch/cyberduck/core/ftp/FTPClient.java
@Override
protected void _prepareDataSocket_(final Socket socket) throws IOException {
if (socket instanceof SSLSocket) {
// Control socket is SSL
final SSLSession session = ((SSLSocket) _socket_).getSession();
if (session.isValid()) {
final SSLSessionContext context = session.getSessionContext();
try {
final Field sessionHostPortCache = context.getClass().getDeclaredField("sessionHostPortCache");
sessionHostPortCache.setAccessible(true);
final Object cache = sessionHostPortCache.get(context);
final Method method = cache.getClass().getDeclaredMethod("put", Object.class, Object.class);
method.setAccessible(true);
method.invoke(cache, String
.format("%s:%s", socket.getInetAddress().getHostName(), String.valueOf(socket.getPort()))
.toLowerCase(Locale.ROOT), session);
method.invoke(cache, String
.format("%s:%s", socket.getInetAddress().getHostAddress(), String.valueOf(socket.getPort()))
.toLowerCase(Locale.ROOT), session);
} catch (NoSuchFieldException e) {
throw new IOException(e);
} catch (Exception e) {
throw new IOException(e);
}
} else {
throw new IOException("Invalid SSL Session");
}
}
}
}
And With openJDK 1.8.0_161 :并使用 openJDK 1.8.0_161 :
We must set :我们必须设置:
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
according to http://www.oracle.com/technetwork/java/javase/8u161-relnotes-4021379.html根据http://www.oracle.com/technetwork/java/javase/8u161-relnotes-4021379.html
Added TLS session hash and extended master secret extension support添加了 TLS 会话哈希和扩展的主密钥扩展支持
In case of compatibility issues, an application may disable negotiation of this extension by setting the System Property jdk.tls.useExtendedMasterSecret to false in the JDK如果出现兼容性问题,应用程序可以通过在 JDK 中将系统属性 jdk.tls.useExtendedMasterSecret 设置为 false 来禁用此扩展的协商
To make Martin Prikryl's suggestion work for me I had to store the key not only under socket.getInetAddress().getHostName()<\/code> but also under
socket.getInetAddress().getHostAddress()<\/code> .
为了让马丁Prikryl的建议工作,为我,我有存储不仅在关键
socket.getInetAddress().getHostName()<\/code>也下
socket.getInetAddress().getHostAddress()<\/code>
(Solution stolen from here<\/a> .)
(从
这里<\/a>偷来的解决方案。)
"
@Martin Prikryl's answer helped me. @Martin Prikryl 的回答帮助了我。
According to my practice, it is worth mentioning that If you used根据我的实践,值得一提的是,如果你使用
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
and it doesn't work, you can try JVM parameters of the same function:而且它不起作用,您可以尝试相同功能的JVM参数:
-Djdk.tls.useExtendedMasterSecret=false
. -Djdk.tls.useExtendedMasterSecret=false
。
Hope to help you.希望能帮到你。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.