如何使用相同的 TLS 会话通过数据连接连接到 FTPS 服务器?
Posted
技术标签:
【中文标题】如何使用相同的 TLS 会话通过数据连接连接到 FTPS 服务器?【英文标题】:How to connect to FTPS server with data connection using same TLS session? 【发布时间】:2015-11-30 15:40:53 【问题描述】:环境:我在 64 位 Windows 7 上使用 Sun Java JDK 1.8.0_60,使用 Spring Integration 4.1.6(内部似乎使用 Apache Commons Net 3.3 进行 FTPS 访问)。
我正在尝试与我们的应用程序集成,从我们客户的 FTPS 服务器自动下载。我已经使用 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
,由 @ 引起987654326@。如果我取消注释 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..
似乎正在发生的事情(这是我第一次处理 FTPS,尽管我之前处理过普通 FTP)是服务器确保对控制和数据连接进行身份验证和加密的方式是“正常” TLS 连接建立控制连接并在那里进行身份验证,每个数据连接都要求客户端连接相同的 TLS 会话。这对我来说是有意义的,因为它应该是如何工作的,但是 Apache Commons Net FTPS 实现似乎并没有这样做。它似乎正在尝试建立一个新的 TLS 会话,因此服务器拒绝了该尝试。
基于this question about resuming SSL sessions in JSSE,Java 似乎假定或要求每个主机/帖子组合使用不同的会话。我的假设是,由于 FTPS 数据连接与控制连接位于不同的端口上,因此它没有找到现有会话并尝试建立新会话,因此连接失败。
我看到了三种主要可能性:
-
服务器不遵循 FTPS 标准,要求数据端口上的 TLS 会话与控制端口上的相同。我可以使用 FileZilla 3.13.1 很好地连接到服务器(使用与我尝试在我的代码中使用的相同的主机/用户/密码)。服务器在登录时将自己标识为“FileZilla Server 0.9.53 beta”,所以这可能是某种专有的 FileZilla 做事方式,我需要做一些奇怪的事情来说服 Java 使用相同的 TLS 会话。李>
Apache Commons Net 客户端实际上并不遵循 FTPS 标准,只允许某些不允许保护数据连接的子集。这看起来很奇怪,因为它似乎是从 Java 中连接到 FTPS 的标准方式。
我完全遗漏了一些东西并误诊了这一点。
如果您能就如何连接到这种 FTPS 服务器提供任何指导,我将不胜感激。谢谢。
【问题讨论】:
详情请参考此链接***.com/questions/46631315/… 【参考方案1】:确实,某些 FTP(S) 服务器确实要求将 TLS/SSL 会话重用于数据连接。这是一种安全措施,服务器可以通过它来验证数据连接是否由与控制连接相同的客户端使用。
一些常见的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
指令)
Cyberduck FTP(S) 客户端确实支持 TLS/SSL 会话重用并且它使用 Apache Commons Net 库,这可能对您的实施有所帮助:
https://github.com/iterate-ch/cyberduck/issues/5087 – 在数据连接上重用会话密钥
查看其FTPClient.java
代码(扩展 Commons Net FTPSClient
),尤其是其override of _prepareDataSocket_
method:
@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));
似乎 _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 Update Release Notes(和answer by @Laurent):
添加了 TLS 会话哈希和扩展的主密钥扩展支持 ... 如果出现兼容性问题,应用程序可以通过在 JDK 中将系统属性
jdk.tls.useExtendedMasterSecret
设置为false
来禁用此扩展的协商
即,您可以调用它来解决问题(您仍然需要覆盖_prepareDataSocket_
):
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
虽然这应该只是一种解决方法。我不知道一个合适的解决方案。
这里有一个替代实现:https://issues.apache.org/jira/browse/NET-408
关于 1.8.0_161 中的问题还有一个单独的问题:SSL Session reuse in Apache FTPS client in JDK 8u161
我过去实际上有过the same problem(只是在 C++/OpenSSL 中,我不使用 Java),所以我知道该用谷歌搜索什么。
【讨论】:
在针对 FileZilla Server 0.9.6 版进行测试时,这对我有用,但对 1.2.0 版则不适用。服务器响应“代码 = 425 无法建立数据连接:数据连接的 TLS 会话未恢复”。有什么想法吗? @mperktold 请使用minimal reproducible example 发布一个新问题。 好的,这是我的问题:***.com/questions/70903926/…【参考方案2】:为了让 Martin Prikryl 的建议对我有用,我不仅必须将密钥存储在 socket.getInetAddress().getHostName()
下,还要存储在 socket.getInetAddress().getHostAddress()
下。
(从here窃取的解决方案。)
【讨论】:
【参考方案3】:您可以使用这个 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 来禁用此扩展的协商
【讨论】:
设置此属性有效!感谢您指出这一点! 你能解释一下如何让它工作吗?我用 SSLReuseSessionFTPSClient 替换 FTPSClient 但它没有改变任何东西 @WaLinke 这应该可以,你设置系统属性了吗?SSLSessionReuseFTPSClient ftpsClient = null; try ftpsClient = new SSLSessionReuseFTPSClient(); ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true)); ...
感谢您的回复,我试过这个:noelshack.com/2019-04-3-1548249527-ftpsclientproblem.png 但它以与 FTPSClient 相同的方式结束。我不知道如何使它工作。
我在 prepareDataSocket 方法中添加了一个断点,但我的代码似乎没有进入它【参考方案4】:
@Martin Prikryl 的回答帮助了我。
根据我的实践,值得一提的是,如果你用过
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
还是不行,你可以试试JVM参数的同一个函数:
-Djdk.tls.useExtendedMasterSecret=false
.
希望能帮到你。
【讨论】:
以上是关于如何使用相同的 TLS 会话通过数据连接连接到 FTPS 服务器?的主要内容,如果未能解决你的问题,请参考以下文章