Как подключиться к серверу FTPS с подключением к данным, используя тот же сеанс TLS?

окружающая среда: я использую Sun Java JDK 1.8.0_60 на 64-разрядной Windows 7, используя Spring Integration 4.1.6 (который, как представляется, использует Apache Commons Net 3.3 для доступа FTPS).

Я пытаюсь интегрировать с нашим приложением автоматическую загрузку с сервера FTPS нашего клиента. Я сделал это успешно с серверами SFTP, использующими Spring Integration без каких-либо проблем для других клиентов без проблем, но это первый раз, когда клиент потребовал от нас использовать FTPS, и получение его для подключения было очень загадочным. В то время как в моем реальном приложении я настраиваю интеграцию Spring с помощью XML-компонентов, чтобы попытаться понять, что не работает, я использую следующий тестовый код (хотя я анонимизирую фактический хост/имя пользователя/пароль здесь):

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 команда, тогда я могу видеть (хотя javax.сеть.debug output), что сервер отправил следующее сообщение после отклонения подключения к данным 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, и поэтому сервер отклоняет попытка.

на основе этот вопрос о возобновлении сеансов SSL в JSSE, похоже, что Java предполагает или требует другого сеанса для каждой комбинации хоста/post. Моя гипотеза заключается в том, что, поскольку соединение данных FTPS находится на другом порту, чем соединение управления, оно не находит существующий сеанс и пытается установить новый, поэтому соединение терпит неудачу.

Я вижу три основных варианта:

  1. сервер не следуя стандарту FTPS, требуя того же сеанса TLS на порту данных, что и на порту управления. Я могу подключиться к серверу нормально (используя тот же хост/пользователь/пароль, который я пытаюсь использовать в своем коде), используя FileZilla 3.13.1. Сервер идентифицирует себя как" FileZilla Server 0.9.53 beta " при входе в систему, поэтому, возможно, это какой-то собственный способ FileZilla делать вещи, и есть что-то странное, что мне нужно сделать, чтобы убедить Java использовать тот же сеанс TLS.
  2. Апач Клиент Commons Net фактически не следует стандарту FTPS и разрешает только некоторое подмножество, которое не позволяет обеспечить безопасность подключений к данным. Это казалось бы странным, поскольку это, по-видимому, стандартный способ подключения к FTPS из Java.
  3. я совершенно что-то упускаю и неправильно диагностирую это.

Я был бы признателен за любое направление, которое вы можете предоставить о том, как подключиться к этому типу сервера FTPS. Спасибо.

3 ответов


действительно, некоторые FTP-серверы требуют, чтобы сеанс TLS/SSL повторно использовался для подключения к данным. Это мера безопасности, с помощью которой сервер может проверить, что подключение к данным используется тем же клиентом, что и подключение к элементу управления.

некоторые ссылки для общих FTP-серверов:


вы можете использовать этот класс 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 и расширенная поддержка расширения master secret

в случае проблем совместимости приложение может отключить согласование этого расширения, установив Системное свойство jdk.tls.useExtendedMasterSecret для false в JDK


чтобы предложение Мартина Прикрила сработало для меня, мне пришлось хранить ключ не только под socket.getInetAddress().getHostName() но и в socket.getInetAddress().getHostAddress(). (Решение украдено из здесь.)