环境:我在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
命令周围的吞噬异常捕获块的注释,那么我可以看到(通过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会话,因此服务器拒绝了这个尝试。
根据这个关于在JSSE中恢复SSL会话的问题,似乎Java假设或要求每个主机/post组合使用不同的会话。我的假设是,由于FTPS数据连接与控制连接在不同的端口上,它没有找到现有的会话,并试图建立一个新的会话,所以连接失败。
我看到三种主要的可能性:
- 服务器没有遵循FTPS标准,要求数据端口和控制端口使用相同的TLS会话。我可以使用FileZilla 3.13.1连接到服务器(使用相同的主机/用户/密码,因为我试图在我的代码中使用)。服务器在登录时将自己标识为"FileZilla server 0.9.53 beta",所以也许这是某种专有的FileZilla做事方式,我需要做一些奇怪的事情来说服Java使用相同的TLS会话。 Apache Commons Net客户端实际上并没有遵循FTPS标准,只允许一些不允许保护数据连接的子集。这看起来很奇怪,因为它似乎是从Java中连接到FTPS的标准方式。
- 我完全错过了一些东西,误诊了。
我很感激任何方向,你可以提供如何连接到这种FTPS服务器。谢谢你。
一些FTP(S)服务器确实要求在数据连接中重用TLS/SSL会话。这是一种安全措施,服务器可以通过它来验证数据连接与控制连接是由同一个客户端使用的。
一些常用FTP服务器的参考:
- vsftpd: https://scarybeastsecurity.blogspot.com/2009/02/vsftpd - 210 released.html
- FileZilla server: 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 NetFTPSClient
),特别是它对_prepareDataSocket_
方法的重写:@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 NetFTPSClient
中是专门为了允许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更新发布说明(和@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的问题有一个单独的问题:
JDK环境下Apache FTPS客户端SSL会话重用[8u161]
我过去也遇到过同样的问题(只是在c++/OpenSSL中,我不做Java),所以我知道该谷歌什么。
您可以使用这个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来禁用此扩展的协商
为了使Martin Prikryl的建议对我有效,我不仅必须将密钥存储在socket.getInetAddress().getHostName()
下,还必须存储在socket.getInetAddress().getHostAddress()
下。(解决方案从这里偷来的)
@Martin Prikryl的回答帮助了我。
根据我的实践,值得一提的是,如果你使用
System.setProperty("jdk.tls.useExtendedMasterSecret", "false");
不起作用,您可以尝试相同函数的JVM参数:
-Djdk.tls.useExtendedMasterSecret=false
.
希望对你有所帮助