環境:私はSunを使用していますJava 64ビットのWindows 7でJDK 1.8.0_60を使用しており、Spring Integration 4.1.6を使用しています(FTPSアクセスにApache Commons Net 3.3を内部的に使用しているようです)。
私は、クライアントの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ブロックのコメントを外すと、サーバーがデータ接続のSSLハンドシェイクを拒否した後、次のメッセージが送信されたことがわかります(javax.net.debug出力を参照)。
_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はホスト/ポストの組み合わせごとに異なるセッションを想定または必要とするようです。私の仮説は、 FTPSデータ接続は制御接続とは異なるポート上にあり、既存のセッションを見つけておらず、新しいセッションを確立しようとしているため、接続は失敗します。
主な可能性は3つあります。
この種のFTPSサーバーへの接続方法について、ご意見をお寄せいただければ幸いです。ありがとうございました。
実際、一部のFTP(S)サーバーでは、データ接続にTLS/SSLセッションを再利用する必要があります。これは、データ接続が制御接続と同じクライアントによって使用されていることをサーバーが確認できるセキュリティ対策です。
一般的なFTPサーバーの参考資料:
NoSessionReuseRequired
ディレクティブ)実装に役立つ可能性があるのは、Cyberduck FTP(S)クライアントがTLS/SSLセッションの再利用をサポートし、Apache Commons Netライブラリを使用することです。
https://trac.cyberduck.io/ticket/5087 -データ接続でセッションキーを再利用
その _FTPClient.Java
_ コード(Commons Net FTPSClient
を拡張)、特に __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 Net FTPSClient
に追加され、TLS/SSLセッションの再利用の実装を具体的に許可しているようです:
https://issues.Apache.org/jira/browse/NET-426
再利用のネイティブサポートはまだ保留中です。
https://issues.Apache.org/jira/browse/NET-408
セッションの再利用をサポートするカスタムFTPSClient
実装を返すには、明らかにSpring Integration DefaultFtpsSessionFactory.createClientInstance()
をオーバーライドする必要があります。
上記のソリューションは、JDK 8u161以降、それ自体では機能しません。
JDK 8u161 Updateリリースノート (および answer by @Laurent )によると:
TLSセッションハッシュと拡張マスターシークレット拡張サポートを追加しました
...
互換性の問題が発生した場合、アプリケーションは、JDKでシステムプロパティ_
jdk.tls.useExtendedMasterSecret
_をfalse
に設定することにより、この拡張機能のネゴシエーションを無効にすることができます
つまり、これを呼び出して問題を修正できます。
_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セッションハッシュと拡張マスターシークレット拡張サポートを追加
互換性の問題が発生した場合、アプリケーションは、JDKでシステムプロパティjdk.tls.useExtendedMasterSecretをfalseに設定することにより、この拡張機能のネゴシエーションを無効にすることがあります。
Martin Prikrylの提案を機能させるには、socket.getInetAddress().getHostName()
だけでなくsocket.getInetAddress().getHostAddress()
にもキーを格納する必要がありました。 ( here から盗まれたソリューション)