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

ubof19bj  于 2023-06-04  发布在  Java
关注(0)|答案(4)|浏览(327)

环境:我在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,由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)是服务器确保控制和数据连接的身份验证和加密的方式是,在“正常”TLS连接建立控制连接和身份验证之后,每个数据连接都要求客户端连接相同的TLS会话。这对我来说是有意义的,因为它应该是如何工作的,但Apache Commons Net FTPS实现似乎没有做到这一点。它似乎正在尝试建立一个新的TLS会话,因此服务器拒绝了该尝试。
基于this question about resuming SSL sessions in JSSE,Java似乎为每个主机/post组合假设或要求不同的会话。我的假设是,由于FTPS数据连接与控制连接在不同的端口上,它没有找到现有的会话,并试图建立一个新的会话,因此连接失败。
我认为主要有三种可能性:
1.服务器在数据端口上要求与控制端口上相同的TLS会话时不遵循FTPS标准。我可以使用FileZilla 3.13.1连接到服务器(使用与我在代码中尝试使用的相同的主机/用户/密码)。服务器在登录时将自己标识为“FileZilla Server 0.9.53 beta”,所以这可能是某种专有的FileZilla方式,我需要做一些奇怪的事情来说服Java使用相同的TLS会话。

  1. Apache Commons Net客户端实际上并不遵循FTPS标准,只允许一些不允许保护数据连接的子集。这看起来很奇怪,因为它似乎是从Java内部连接到FTPS的标准方式。
    1.我完全忽略了一些东西,误诊了。
    我会很感激你能提供的任何方向如何连接到这种FTPS服务器。谢谢你。
ccgok5k5

ccgok5k51#

事实上,一些FTP(S)服务器确实要求TLS/SSL会话被重新用于数据连接。这是一种安全措施,通过它服务器可以验证数据连接是否由与控制连接相同的客户端使用。
常见FTP服务器的一些参考:

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

@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 8 u161以来,上述解决方案不再单独工作。
根据JDK 8u161 Update Release Notes(以及@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),所以我知道该谷歌什么。
w1e3prcc

w1e3prcc2#

您可以使用以下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来禁用此扩展的协商

jv2fixgn

jv2fixgn3#

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

sh7euo9m

sh7euo9m4#

”Martin Prikryl的回答帮助了我。
根据我的实践,值得一提的是,如果你用
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
如果不起作用,可以尝试相同函数的JVM参数:
-Djdk.tls.useExtendedMasterSecret=false
希望能帮到你。

相关问题