web-dev-qa-db-ja.com

同じTLSセッションを使用してデータ接続でFTPSサーバーに接続する方法

環境:私は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つあります。

  1. サーバーはFTPS標準に従っていないため、データポートで制御ポートと同じTLSセッションを要求します。 FileZilla 3.13.1を使用して、サーバーに問題なく接続できます(コードで使用しようとしているのと同じホスト/ユーザー/パスワードを使用)。サーバーはログイン時に自分自身を「FileZilla Server 0.9.53 beta」として識別します。おそらくこれは、独自のFileZilla独自の方法であり、納得させるために何か奇妙なことをする必要がありますJava同じTLSセッションを使用します。
  2. Apache Commons Netクライアントは実際にはFTPS標準に準拠しておらず、データ接続の保護を許可しない一部のサブセットのみを許可しています。これは、Java内からFTPSに接続する標準的な方法のように見えるため、奇妙に思えます。
  3. 私は何かを完全に見逃しており、これを誤診しています。

この種のFTPSサーバーへの接続方法について、ご意見をお寄せいただければ幸いです。ありがとうございました。

16
user65839

実際、一部のFTP(S)サーバーでは、データ接続にTLS/SSLセッションを再利用する必要があります。これは、データ接続が制御接続と同じクライアントによって使用されていることをサーバーが確認できるセキュリティ対策です。

一般的なFTPサーバーの参考資料:


実装に役立つ可能性があるのは、Cyber​​duck 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");
_

ただし、これは回避策と見なす必要があります。適切な解決策がわかりません。

21
Martin Prikryl

次の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に設定することにより、この拡張機能のネゴシエーションを無効にすることがあります。

6

Martin Prikrylの提案を機能させるには、socket.getInetAddress().getHostName()だけでなくsocket.getInetAddress().getHostAddress()にもキーを格納する必要がありました。 ( here から盗まれたソリューション)

2
NotX