简体   繁体   English

"如何使用相同的 TLS 会话通过数据连接连接到 FTPS 服务器?"

[英]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:我看到三种主要可能性:

  1. The server is not following the FTPS standard in requiring the same TLS session on the data port as on the control port.服务器不遵循 FTPS 标准,要求数据端口上的 TLS 会话与控制端口上的相同。 I can connect to the server fine (using the same host/user/password as I'm trying to use in my code) using FileZilla 3.13.1.我可以使用 FileZilla 3.13.1 很好地连接到服务器(使用与我尝试在我的代码中使用的相同的主机/用户/密码)。 The server identifies itself as "FileZilla Server 0.9.53 beta" upon login, so perhaps this is some sort of proprietary FileZilla way of doing things, and there's something odd I need to do to convince Java to use the same TLS session.服务器在登录时将自己标识为“FileZilla Server 0.9.53 beta”,所以这可能是某种专有的 FileZilla 做事方式,我需要做一些奇怪的事情来说服 Java 使用相同的 TLS 会话。
  2. The Apache Commons Net client doesn't actually follow the FTPS standard, and only allows some subset that doesn't allow for securing the data connections. Apache Commons Net 客户端实际上并不遵循 FTPS 标准,只允许某些不允许保护数据连接的子集。 This would seem odd, as it appears to be the standard way of connecting to FTPS from within Java.这看起来很奇怪,因为它似乎是从 Java 中连接到 FTPS 的标准方式。
  3. I'm completely missing something and misdiagnosing this.我完全错过了一些东西并误诊了这一点。

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服务器参考:


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: Cyber​​duck 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 Property jdk.tls.useExtendedMasterSecret to false 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM