首页 文章

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

提问于
浏览
11

环境:我在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服务器的主"control"连接工作正常,但当它尝试打开列表的数据连接(或我尝试过的任何其他数据连接)时,我得到 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)是服务器确保对控制和数据连接进行身份验证和加密的方式是在“正常”之后 Build 控制连接和身份验证的TLS连接在那里发生,每个数据连接都要求客户端连接相同的TLS会话 . 这对我来说是有意义的,因为它应该如何工作,但Apache Commons Net FTPS实现似乎并没有这样做 . 它似乎正在尝试 Build 一个新的TLS会话,因此服务器拒绝该尝试 .

基于this question about resuming SSL sessions in JSSE,Java似乎假定或要求每个主机/帖子组合使用不同的会话 . 我的假设是,由于FTPS数据连接位于与控制连接不同的端口上,因此它没有找到现有会话并且正在尝试 Build 新会话,因此连接失败 .

我看到三种主要可能性:

  • 服务器未遵循FTPS标准,要求在数据端口上使用与控制端口上相同的TLS会话 . 我可以很好地连接到服务器(使用相同的主机/用户/密码,因为我需要做些奇怪的事情来说服Java使用相同的TLS会话 .

  • Apache Commons Net客户端没有't actually follow the FTPS standard, and only allows some subset that doesn' t允许保护数据连接 . 这看起来很奇怪,因为它似乎是从Java内部连接到FTPS的标准方式 .

  • 我完全错过了一些东西并且误解了这一点 .

我很感激您可以提供的关于如何连接到这种FTPS服务器的任何方向 . 谢谢 .

3 回答

  • 2

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

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


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

    @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));
                }
            }
        }
    }
    

    仍然有待对重用的本机支持:
    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.tls.useExtendedMasterSecret设置为false来禁用此扩展的协商在JDK中

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

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

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

  • 4

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

  • 16

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

相关问题