Как подключиться к серверу 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 Integration с использованием bean-компонентов 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.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, и поэтому сервер отклоняет попытку.
Исходя из этого вопроса о возобновлении сеансов SSL в JSSE, выясняется, что Java предполагает или требует разные сеансы для каждой комбинации хост / пост. Моя гипотеза состоит в том, что, поскольку соединение для передачи данных FTPS находится на другом порту, чем управляющее соединение, он не находит существующий сеанс и пытается установить новый, поэтому соединение не устанавливается.
Я вижу три основные возможности:
- Сервер не следует стандарту FTPS, требуя того же сеанса TLS на порте данных, что и на порте управления. Я могу нормально подключиться к серверу (используя тот же хост / пользователя / пароль, который я пытаюсь использовать в своем коде), используя FileZilla 3.13.1. Сервер идентифицирует себя как "FileZilla Server 0.9.53 beta" при входе в систему, так что, возможно, это какой-то проприетарный метод FileZilla, и есть кое-что странное, что мне нужно сделать, чтобы убедить Java использовать тот же сеанс TLS.
- Клиент Apache Commons Net на самом деле не следует стандарту FTPS, а допускает только некоторые подмножества, которые не позволяют защищать соединения для передачи данных. Это может показаться странным, так как кажется, что это стандартный способ подключения к FTPS из Java.
- Я совершенно что-то упускаю и неправильно диагностирую это.
Я был бы признателен за любые указания относительно того, как подключиться к серверу FTPS такого типа. Спасибо.
5 ответов
Действительно, некоторые серверы 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 (
NoSessionReuseRequired
директива)
Что может помочь в реализации, так это то, что клиент Cyberduck FTP(S) поддерживает повторное использование сеансов TLS/SSL и использует библиотеку Apache Commons Net:
https://trac.cyberduck.io/ticket/5087 - повторно использовать ключ сеанса при подключении к данным
Увидеть его
FTPClient.java
код (расширяет сеть CommonsFTPSClient
), особенно его переопределение_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 NetFTPSClient
специально, чтобы позволить реализацию повторного использования сеанса TLS/SSL:
https://issues.apache.org/jira/browse/NET-426Нативная поддержка для повторного использования все еще ожидается:
https://issues.apache.org/jira/browse/NET-408Вам, очевидно, нужно будет переопределить интеграцию Spring
DefaultFtpsSessionFactory.createClientInstance()
вернуть свой обычайFTPSClient
реализация с поддержкой повторного использования сессии.
Вышеупомянутое решение больше не работает само по себе, так как JDK 8u161.
Согласно примечаниям к выпуску обновления JDK 8u161 (и ответу @Laurent):
Добавлен хэш сеанса TLS и расширенная поддержка расширений главного секрета.
...
В случае проблем с совместимостью приложение может отключить согласование этого расширения, установив свойство системы
jdk.tls.useExtendedMasterSecret
вfalse
в JDK
То есть, вы можете вызвать это, чтобы решить проблему:
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 и расширенная поддержка расширений главного секрета.
В случае проблем с совместимостью приложение может отключить согласование этого расширения, установив для свойства System jdk.tls.useExtendedMasterSecret значение false в JDK.
Чтобы предложение Мартина Прикрыла сработало для меня, мне пришлось хранить ключ не только под socket.getInetAddress().getHostName()
но и под socket.getInetAddress().getHostAddress()
, (Решение украдено отсюда.)
Если вы можете позволить себе добавить Bouncy Castle TLSorg.bouncycastle:bctls-jdk18on
в качестве зависимости вы можете попробовать следующее решение, которое позволяет избежать отражения и установки глобальных свойств:
class FTPSClientWithResumeBC extends FTPSClient {
static {
Security.addProvider(new BouncyCastleJsseProvider());
}
public FTPSClientWithResumeBC() {
super(createSslContext());
}
private static SSLContext createSslContext() {
try {
// doesn't work with TLSv1.3
SSLContext sslContext = SSLContext.getInstance("TLSv1.2", "BCJSSE");
sslContext.init(null, new TrustManager[]{TrustManagerUtils.getValidateServerCertificateTrustManager()}, new SecureRandom()); // 1
return sslContext;
} catch (NoSuchAlgorithmException | NoSuchProviderException | KeyManagementException e) {
throw new RuntimeException("Cannot create SSL context.", e);
}
}
@Override
protected void _connectAction_() throws IOException {
super._connectAction_();
execPBSZ(0);
execPROT("P");
}
@Override
protected void _prepareDataSocket_(Socket dataSocket) {
if (_socket_ instanceof BCSSLSocket sslSocket) {
BCExtendedSSLSession bcSession = sslSocket.getBCSession();
if (bcSession != null && bcSession.isValid() && dataSocket instanceof BCSSLSocket dataSslSocket) {
dataSslSocket.setBCSessionToResume(bcSession); // 2
dataSslSocket.setHost(bcSession.getPeerHost()); // 3
}
}
}
}
Примечания:
- работал у меня™ с
org.bouncycastle:bctls-jdk18on:1.77
,commons-net:commons-net:3.10.0
и сервер FileZilla 1.7.3 - Реализация BouncyCastle TLS поддерживает возобновление сеанса — используется в строке, отмеченной цифрой 2.
- строка, отмеченная цифрой 3, была необходима для одного из моих тестовых серверов, она требовала присутствия SNI в ClientHello подключения к данным (и не делала этого сама по себе)
- инициализация в строке с пометкой 1 аналогична тому, что
FTPSClient
делает по умолчанию, вам может потребоваться настроить его в соответствии с вашими потребностями - Я не эксперт ни по SSL, ни по FTPS, поэтому отнеситесь к ответу с некоторой осторожностью.
Ответ @Martin Prikryl мне помог.
Согласно моей практике, стоит упомянуть, что если вы использовали
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
и это не работает, вы можете попробовать параметры JVM той же функции:
-Djdk.tls.useExtendedMasterSecret=false
.
Надеюсь помочь вам.