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

环境:我在64位Windows 7上使用Sun Java JDK 1.8.0_60,使用Spring Integration 4.1.6(内部似乎使用Apache Commons Net 3.3进行FTPS访问)。

我正在尝试与我们的应用程序集成,从客户端的FTPS服务器自动下载。 我已经使用Spring Integration成功完成了使用Spring Integration的SFTP服务器而没有任何问题,但这是客户第一次要求我们使用FTPS,并且连接它一直非常令人费解。 虽然在我的实际应用程序中,我正在使用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(); 

我正在使用-Djavax.net.debug=all运行此代码以打印所有TLS调试信息。

与FTPS服务器的主“控制”连接工作正常,但当它尝试打开列表的数据连接(或我尝试过的任何其他数据连接)时,我得到一个javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake ,由java.io.EOFException: SSL peer shut down incorrectly引起java.io.EOFException: SSL peer shut down incorrectly 。 如果我取消注释session.list命令周围的swallowing-exceptions catch块,那么我可以看到(虽然是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.. 

似乎正在发生的事情(这是我第一次处理FTPS,虽然我以前处理过普通FTP)是服务器确保对控制和数据连接进行身份validation和加密的方式是在“正常”之后建立控制连接和身份validation的TLS连接在那里发生,每个数据连接都要求客户端连接相同的TLS会话。 这对我来说是有意义的,因为它应该如何工作,但Apache Commons Net FTPS实现似乎并没有这样做。 它似乎试图建立一个新的TLS会话,因此服务器拒绝该尝试。

基于关于在JSSE中恢复SSL会话的这个问题 ,似乎Java假定或要求每个主机/post组合使用不同的会话。 我的假设是,由于FTPS数据连接位于与控制连接不同的端口上,因此它找不到现有会话并且正在尝试建立新会话,因此连接失败。

我看到三种主要可能性:

  1. 服务器不遵循FTPS标准,要求在数据端口上使用与控制端口上相同的TLS会话。 我可以使用FileZilla 3.13.1连接到服务器(使用与我在我的代码中尝试使用相同的主机/用户/密码)。 服务器在登录时将自己标识为“FileZilla Server 0.9.53 beta”,因此这可能是某种专有的FileZilla操作方式,而且我需要做些奇怪的事情来说服Java使用相同的TLS会话。
  2. Apache Commons Net客户端实际上并不遵循FTPS标准,只允许某些子集不允许保护数据连接。 这看起来很奇怪,因为它似乎是从Java内部连接到FTPS的标准方式。
  3. 我完全错过了一些东西并且误解了这一点。

我很欣赏你可以提供的关于如何连接到这种FTPS服务器的任何方向。 谢谢。

实际上,一些FTP(S)服务器确实需要将TLS / SSL会话重用于数据连接。 这是一种安全措施,服务器可以通过该安全措施validation数据连接是否由与控制连接相同的客户端使用。

常见FTP服务器的一些参考:

  • vsftpd: https : //scarybeastsecurity.blogspot.com/2009/02/vsftpd-210-released.html
  • FileZilla服务器: https ://svn.filezilla-project.org/filezilla ? view = revision & revision = 6661
  • ProFTPD: http : //www.proftpd.org/docs/contrib/mod_tls.html#TLSOptions ( NoSessionReuseRequired指令)

可以帮助您实现的是Cyber​​duck FTP(S)客户端确实支持TLS / SSL会话重用,它使用Apache Commons Net库:

  • https://trac.cyberduck.io/ticket/5087 – 在数据连接上重用Session键

  • 查看其FTPClient.java代码(扩展Commons Net FTPSClient ),特别是它覆盖_prepareDataSocket_方法 :

     @Override protected void _prepareDataSocket_(final Socket socket) throws IOException { 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 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) { // 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)); } } } } 
  • 似乎将_prepareDataSocket_方法专门添加到Commons Net FTPSClient以允许TLS / SSL会话重用实现:
    https://issues.apache.org/jira/browse/NET-426

    仍在等待对重用的本机支持:
    https://issues.apache.org/jira/browse/NET-408

  • 您显然需要覆盖Spring Integration DefaultFtpsSessionFactory.createClientInstance()以返回具有会话重用支持的自定义FTPSClient实现。


自JDK 8u161以来,上述解决方案不再适用。

根据JDK 8u161更新发行说明 (以及@Laurent的回答 ):

添加了TLS会话哈希和扩展主密钥扩展支持

如果出现兼容性问题,应用程序可能会通过在JDK中将系统属性jdk.tls.useExtendedMasterSecret设置为false来禁用此扩展的协商

即,你可以调用它来解决问题:

 System.setProperty("jdk.tls.useExtendedMasterSecret", "false"); 

虽然这应该只是一种解决方法。 我不知道一个合适的解决方案。

您可以使用此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"); } } } } 

并使用openJDK 1.8.0_161:

我们必须设定:

 System.setProperty("jdk.tls.useExtendedMasterSecret", "false"); 

根据http://www.oracle.com/technetwork/java/javase/8u161-relnotes-4021379.html

添加了TLS会话哈希和扩展主密钥扩展支持

如果出现兼容性问题,应用程序可能会通过在JDK中将系统属性jdk.tls.useExtendedMasterSecret设置为false来禁用此扩展的协商

为了让Martin Prikryl的建议对我起作用,我不仅要将密钥存储在socket.getInetAddress().getHostName()下,还要存放在socket.getInetAddress().getHostName()下。 (解决方案从这里被盗。)