SST連載・解説記事

  1. HOMEHOME
  2. SSTコラムSSTコラム
  3. SSTtechlog
  4. 05  JavaでSSL/TLS接続アプリケーションを作ってみよう(1):SSL版Echoサーバ/クライアントの作成と脆弱性テスト(testssl.sh, Nmap)

SSTtechlog

対象読者
Javaプログラマ
SSL/TLSに興味を持つエンジニア全般

サンプルコードの動作環境
OS:CentOS6
Java:Java SE 8u77
ビルド:Apache Maven 3.3.9
他:bashとOpenSSL
(※bashとOpenSSLを使って証明書を生成するスクリプトを作っています。 CentOS6が提供する OpenSSL 1.0.1e-fips にて動作確認しています。)

はじめに

techlog第五回解説漫画

illustrations by あおい海月

近年、SSL/TLSプロトコルや実装ライブラリの脆弱性が注目を集めています。
先日も2016年3月1日にOpenSSLのセキュリティアドバイザリが発表され、その中に "DROWN Attack" と呼ばれる脆弱性への対応が含まれていました。本記事をご覧の方の中にも、脆弱性内容の確認やOpenSSLのアップデート、サーバ設定の調整対応などに追われた方もいらっしゃるのではないでしょうか。

注目を集めているSSL/TLSですが、技術要素としては暗号技術や公開鍵基盤(PKI:Public Key Infrastructure)を組み合わせたそれなりに複雑な仕組みです。そのため脆弱性情報を調べたりサーバ設定を調整する際に、それらはSSL/TLSの全体像にどう影響するのか分かりづらいのではないでしょうか。

そこで今回から3回に分けて、Javaを使ったシンプルなSSL/TLS接続アプリケーションを作成し、SSL/TLSの設定を色々変えてみてその影響を観察し、実験していきます。手を動かす実験を繰り返していくことでSSL/TLSの仕組みについて理解が深まり、よりセキュアなサービスを提供する一助になれば幸いです。

第1回目となる今回は、サンプルアプリケーションのビルド方法や実行方法について解説します。またtestssl.shとNmapというオープンソースソフトウェアを用いて、サンプルアプリケーションのSSL/TLSの設定を検査してみます。

サンプルアプリケーション「java-sslsocket-sample」の紹介

SSL/TLS接続アプリケーションのサンプルを作るにあたり、今回はJavaを採用しました。
Javaを採用した理由としては、弊社の診断サービスやクラウド型WAFサービス「Scutum」で活用しているため、SSL/TLS接続の社内でのノウハウが蓄積されているからです。

サンプルアプリケーションのコードは以下のGitHubリポジトリにて公開していますので、実際に動かしたい方は適宜ローカル環境にcloneしてください。

https://github.com/SecureSkyTechnology/java-sslsocket-sample

サンプルアプリケーションでは以下の3つの機能を実装しています。

  1. SSL/TLS接続サーバ (echo_server)
    クライアントから送られてきた1行の文字列を、そのままクライアントに送り返す "echo" (やまびこ)サーバです。
  2. SSL/TLS接続クライアント (echo_client)
    標準入力から1行入力を読み込んだら、サーバに送り、レスポンスを表示するだけのクライアントです。SSL/TLS接続サーバとペアで使います。
  3. GETリクエストを送るだけのシンプルなHTTPSクライアント (https_get)

なおライセンスは The BSD 2-Clause Licenseとします。ソースコードについては自己責任の下でご利用ください。

ビルド

  1. Java SEをインストールします。
    Java SE 8u77 以上のバージョンを推奨します。以下のOracle社のページよりダウンロード・インストールしてください。
  2. ビルドツールとして Apache Maven をインストールします。
    以下のページよりダウンロード・インストールしてください。
  3. サンプルコードをGitHubリポジトリからローカル環境にcloneします。
    (gitが使えない場合は、GitHubからzipファイルでダウンロードできます。)
  4. mvn コマンドでビルドします。
    $ cd java-sslsocket-sample/
    $ mvn package
  5. ビルドに成功すると target/java-sslsocket-sample-1.0.jar が生成されます。
    コマンドラインで -h(--help) オプションを指定すると、詳細な使い方を表示します。
    $ java -jar target/java-sslsocket-sample-1.0.jar --help

テスト用のサーバ証明書の生成

SSL/TLS接続サーバを実行するには、サーバ証明書が必要です。サンプルコード内にbashとOpenSSLを使って証明書を生成するスクリプトを含めていますので、それを使ってテスト用のサーバ証明書を生成します。CentOS6が提供する OpenSSL 1.0.1e-fips にて動作確認しています。

certs-rsa-md5/gen_certs.sh   : RSA鍵にMD5で署名した証明書を生成
certs-rsa-sha2/gen_certs.sh  : RSA鍵にSHA-2で署名した証明書を生成
certs-ecdsa/gen_certs.sh     : ECC鍵にSHA-2で署名した証明書を生成
certs-snitests/gen_certs.sh  : Common Nameの異なる証明書を生成(次回以降使用します)

生成する証明書の種類に応じたそれぞれのディレクトリに移動し、コマンドラインから gen_certs.sh を実行します。

動作確認のため、まずは certs-rsa-sha2/gen_certs.sh を実行してみます。

(".sh"ファイルに実行権限を追加)
$ cd java-sslsocket-sample/
$ ls **/*.sh | xargs chmod +x

(gen_certs.sh を実行)
$ cd certs-rsa-sha2/
$ ./gen_certs.sh

gen_certs.sh の実行によりいくつかのファイルが生成されますが、これらの生成されたファイルのうち、拡張子が .cer のものがサーバ証明書で、.pem が対応するPEM形式の秘密鍵ファイルになります。
 .pem ファイルは暗号化していません。
また、今回の echo サーバ/クライアントではPEM形式には未対応でDER形式の秘密鍵にのみ対応しているため、gen_certs.sh 内で .pem を openssl pkcs8 コマンドによって xxxx_pkcs8.der という名前のDER形式のファイルに変換しており、実行時にはこのDER形式のファイルを使うことになります。

サンプルアプリケーションの実行

  1. SSL/TLS接続サーバとなるecho_serverを実行します。
    コマンドライン引数はTCP接続を待ち受けるポート番号、証明書ファイル、鍵ファイルの順で指定します。ここでは、上記で生成した certs-rsa-sha2 のRSA2048bit鍵の証明書と鍵ファイルを使います。
    $ java -jar target/java-sslsocket-sample-1.0.jar echo_server 8443 certs-rsa-sha2/rsa2048.cer certs-rsa-sha2/rsa2048_pkcs8.der 
    (... SSL/TLSの設定値を含むSSLParametersの内容を表示しています)
    SSLParameters (SSLServerSocket updated):
      Protocols[0]: SSLv2Hello
      Protocols[1]: TLSv1
      Protocols[2]: TLSv1.1
      Protocols[3]: TLSv1.2
      useCipherSuitesOrder: [false]
      CipherSuites[0]: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
    (...)
      CipherSuites[49]: TLS_EMPTY_RENEGOTIATION_INFO_SCSV
      endpointIdentificationAlgorithm: [null]
      needClientAuth: [false]
      wantClientAuth: [false]
    (ここでクライアントからの接続待ちになります)
  2. 別のターミナル画面を開き、SSL/TLS接続クライアントとなるecho_clientを実行します。
    echo_serverと同じホスト上で実行しますので、localhost:8443 を接続先としてコマンドライン引数で指定します。またテスト用のサーバ証明書を検証できるようにするため、コマンドラインオプションでサーバ側で使用しているサーバ証明書ファイルを指定します。
    $ java -jar target/java-sslsocket-sample-1.0.jar echo_client localhost:8443 -servercert ./certs-rsa-sha2/rsa2048.cer
    (... SSL/TLSの設定値を含むSSLParametersの内容を表示しています)
    SSLParameters (SSLSocket updated):
      Protocols[0]: TLSv1
      Protocols[1]: TLSv1.1
      Protocols[2]: TLSv1.2
      useCipherSuitesOrder: [false]
      CipherSuites[0]: TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
    (...)
      CipherSuites[49]: TLS_EMPTY_RENEGOTIATION_INFO_SCSV
      endpointIdentificationAlgorithm: [null]
      needClientAuth: [false]
      wantClientAuth: [false]
    2016-03-10 16:41:18,178 [main] INFO sst.sslsocket.sample.app.EchoSSLClient ## - Echo SSL Client connect to localhost, port=8443
    (キーボードからの入力待ち)
  3. クライアントを起動した側のターミナルでキーボードから適当な英数記号のメッセージを入力し、Enterを押します。
    サーバ側でメッセージを受け取った旨が表示され、すぐにクライアントに送り返されます。クライアント側では入力したものと同じ文字列を受信し、表示されます。
  4. サーバやクライアントを終了するには、それぞれでCtrl-Cを入力します。
  5. https_getについても動作確認してみます。接続先のURLをコマンドライン引数で指定します。
    以下は、localhost上にセットアップしたWebサーバへのアクセス例です。Webサーバのサーバ証明書を検証できるようにするため、コマンドラインオプションでWebサーバのサーバ証明書ファイル(例では /etc/pki/tls/certs/mytest.cer )を指定します。
    $ java -jar target/java-sslsocket-sample-1.0.jar https_get https://localhost/ -servercert /etc/pki/tls/certs/mytest.cer
    (...)
    -----------send>>
    GET / HTTP/1.1
    Host: localhost
    Connection: close
    
    
    -----------send<<
    -----------recv>>
    HTTP/1.1 200 OK
    Date: Thu, 10 Mar 2016 07:45:41 GMT
    Server: Apache
    (...)
    -----------recv<<
    2016-03-10 16:45:41,802 [main] INFO sst.sslsocket.sample.app.HttpsGetClient ## - HTTPS GET Client disconnected

    この機能は、次回以降の検証で使います。
    念のため、自分の管理下にあるHTTPSサーバを指定するようにしてください。

ソースコードの説明

※Javaのソースに興味ある方はお読みください。それ以外の方はスキップして「SSL/TLSサーバの設定確認」に読み進んで頂いて大丈夫です。

Javaソースコードの構成:

java-sslsocket-sample/src/main/java/
    sst/sslsocket/sample/       ... "sst.sslsocket.sample" パッケージディレクトリ
        CliMain.java            ... "-jar" で実行されるブートクラス
        CliSSLParameters.java   ... コマンドラインオプションの格納用
        TinyUtils.java          ... ユーティリティクラス
        app/
            EchoSSLClient.java  ... "echo_client" の実装クラス
            EchoSSLServer.java  ... "echo_server" の実装クラス
            HttpsGetClient.java ... "https_get" の実装クラス

本記事では誌面も限られているため、"echo_server" と "echo_client" のSSL/TLS接続処理でポイントとなる部分だけ解説します。
それ以外のソースコードについては、読者の皆様自身で確認してみてください。

まずSSL/TLSサーバ機能を実装する EchoSSLServer.java のポイントを説明します。以下がサーバサイドのSSLソケットを扱うコードになります。

  public void run(int port, KeyManager[] keyManagers) throws Exception {
        final Logger logger = LoggerFactory.getLogger(this.getClass());

        SSLContext sslContext = SSLContext.getInstance("TLS");
        // SSLContextを指定されたKeyManagerインスタンスの配列で初期化
        sslContext.init(keyManagers, null, null);
        SSLServerSocket serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket();
        SSLParameters copiedSSLParameters = serverSocket.getSSLParameters();
        // SSLServerSocketのSSLParametersを、コマンドラインオプションに従い更新する。
        clisp.updateSSLParameters(copiedSSLParameters, false);
        TinyUtils.dumpSSLParameters(copiedSSLParameters, "SSLServerSocket updated");
        serverSocket.setSSLParameters(copiedSSLParameters);
        serverSocket.setReuseAddress(true);
        serverSocket.bind(new InetSocketAddress(port));

        while (true) {
            try {
                Socket socket = serverSocket.accept();
                logger.info("Echo SSL Server connection accepted from {}", socket.getRemoteSocketAddress());
                // 接続を受け付けたら、別スレッドでEchoサーバ処理を開始
                new EchoServerThreadWorker(socket);
            } catch (Exception e) {
                logger.error("Echo SSL Server exception", e);
            }
        }
    }

ポイント解説:

  1. SSL/TLS接続のパラメータや証明書設定、Socketファクトリなどをまとめて管理するSSLContextのインスタンスを取得します。
    サンプルコードではアルゴリズム名に"TLS"を指定しており、これによりTLSプロトコルの主要なバージョンがサポートされます。
    SSLContext sslContext = SSLContext.getInstance("TLS");
  2. SSLContextインスタンスを初期化します。サーバ側になるため、サーバ証明書や鍵ファイルを管理する KeyManager インスタンスの配列を渡します。
    sslContext.init(keyManagers, null, null);
  3. SSLContextインスタンスの getServerSocketFactory() を呼び、さらにそこから createServerSocket() を呼ぶと SSLServerSocket のインスタンスが返されます。
    インターフェイス上は createServerSocket() の戻り値は ServerSocket になっているため、明示的に SSLServerSocket にキャストしています。
    SSLServerSocket serverSocket = (SSLServerSocket) sslContext.getServerSocketFactory().createServerSocket();
  4. 実際に使うプロトコルバージョンや、暗号スイートなどを SSLParameters クラスを使って微調整しています。
    SSLContext に対してまとめて設定することもできますが、今回は SSLServerSocket 個別に設定しています。
    SSLParameters copiedSSLParameters = serverSocket.getSSLParameters();
    // SSLServerSocketのSSLParametersを、コマンドラインオプションに従い更新する。
    clisp.updateSSLParameters(copiedSSLParameters, false);
  5. 以降は、通常の ServerSocket の利用と変わりません。bind()して、acceptし、接続を受け付けるとSocketが返されますので、別スレッドで実際の "Echo" 処理を行っています。

続いてSSL/TLSクライアント機能を実装する EchoSSLClient.java のポイントを説明します。以下がクライアントサイドのSSLソケットを扱うコードになります。

    public void run(final String connectToHost, int connectToPort) throws Exception {
        final Logger logger = LoggerFactory.getLogger(this.getClass());

        SSLContext sslContext = SSLContext.getInstance("TLS");
        if (this.dontVerifyServerCert) {
            // SSLContextを、証明書の検証を全てpassさせるテスト用のTrustManagerで初期化。
            sslContext.init(null, TinyUtils.createAllAllowTrustManagers(), null);
        } else if (!"".equals(this.serverCertPemFile)) {
            // SSLContextを、指定された証明書か検証するTrustManagerで初期化。
            sslContext.init(null, TinyUtils.createDebugPurposeTrustManagers(this.serverCertPemFile), null);
        } else {
            // 証明書検証をデフォルト処理に任せる方式で初期化。
            sslContext.init(null, null, null);
        }
        SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket();
        SSLParameters copiedSSLParameters = socket.getSSLParameters();
        // SSLSocketのSSLParametersを、コマンドラインオプションに従い更新する。
        clisp.updateSSLParameters(copiedSSLParameters, true);
        TinyUtils.dumpSSLParameters(copiedSSLParameters, "SSLSocket updated");
        socket.setSSLParameters(copiedSSLParameters);

        try {
            socket.connect(new InetSocketAddress(connectToHost, connectToPort));
            // (省略)
        }
    }

ポイント解説:

  1. SSL/TLS接続のパラメータや証明書設定、Socketファクトリなどをまとめて管理するSSLContextのインスタンスを取得します。
    サンプルコードではアルゴリズム名に"TLS"を指定しており、これによりTLSプロトコルの主要なバージョンがサポートされます。
    SSLContext sslContext = SSLContext.getInstance("TLS");
  2. SSLContextインスタンスを初期化します。クライアント側になるため、証明書検証を行う TrustManager インスタンスの配列を渡します。
    サンプルコードではテスト用のサーバ証明書を使うため、証明書の検証を全く行わないTrustManagerか、指定したサーバ証明書のみ検証成功とするTrustManagerを生成し、渡せるようにしています。
    if (this.dontVerifyServerCert) {
        // SSLContextを、証明書の検証を全てpassさせるテスト用のTrustManagerで初期化。
        sslContext.init(null, TinyUtils.createAllAllowTrustManagers(), null);
    } else if (!"".equals(this.serverCertPemFile)) {
        // SSLContextを、指定された証明書か検証するTrustManagerで初期化。
        sslContext.init(null, TinyUtils.createDebugPurposeTrustManagers(this.serverCertPemFile), null);
    } else {
        // 証明書検証をデフォルト処理に任せる方式で初期化。
        sslContext.init(null, null, null);
    }
  3. SSLContextインスタンスの getSocketFactory() を呼び、さらにそこから createSocket() を呼ぶと SSLSocket のインスタンスが返されます。
    インターフェイス上は createSocket() の戻り値は Socket になっているため、明示的に SSLSocket にキャストしています。
    SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket();
  4. 実際に使うプロトコルバージョンや、暗号スイートなどを SSLParameters クラスを使って微調整しています。
    SSLContext に対してまとめて設定することもできますが、今回はSSLSocket個別に設定しています。
    SSLParameters copiedSSLParameters = socket.getSSLParameters();
    // SSLSocketのSSLParametersを、コマンドラインオプションに従い更新する。
    clisp.updateSSLParameters(copiedSSLParameters, true);
  5. 以降は、通常のSocketの利用と変わりません。connect()した後はJavaの標準的な入出力機能を使って送受信を行っています。

まとめると、JavaのライブラリがSSL/TLS処理を行ってくれるため、アプリケーションプログラマは僅かなコードを加えるだけで、既存のSocket処理をSSL/TLSに対応させることが可能です。
またその際に、プロトコルのバージョンや暗号スイートなどもカスタマイズ可能です。サーバ証明書と鍵ファイルの管理や証明書検証についても、独自ロジックを組み込んだクラスをアプリケーションプログラマ自身が実装し、カスタマイズすることが可能です。

Java8の提供するSSL/TLS機能の詳細については、Oracleからの以下のドキュメントをご確認ください。

SSL/TLSサーバの設定確認

次回以降にJava8のSSL/TLS接続の設定をカスタマイズしていきますが、その影響を調べるためのチェックツールの使い方について紹介します。

オープンソースで開発されている次のツールを使ってチェックしていきます。

  • testssl.sh : https://testssl.sh/

    SSL/TLS接続やHTTPSに特化したチェックツールです。この分野では Qualys の SSL LABS が有名ですが、SSL LABSはインターネットに公開されているサーバしかチェックできません。
    今回のようにローカルで実験するためには、testssl.sh が便利です。

  • Nmap : https://nmap.org/

    SSL/TLS 以外にもネットワークのセキュリティ調査で広く使われているツールです。診断の現場で広く使われている実績があります。

本記事で動作確認したバージョン:

Nmap:7.01
testssl.sh:2.6

testssl.shのインストール

  1. https://testssl.sh/ から最新安定版のtestssl.shをダウンロードし、chmod コマンドで実行権限を追加します。
  2. testssl.shのサイトから古いアルゴリズムやプロトコルもサポートしたカスタムビルドのopensslをダウンロードします。
    執筆時点では openssl-1.0.2e-chacha.pm.ipv6.Linux.tar.gz でしたので、これをダウンロードして展開します。
  3. tar.gzを展開したら bin/ ディレクトリ以下に実行ファイルが展開されます。 testssl.sh と同じディレクトリに bin/ ディレクトリを置きます。
  4. testssl.sh を -b オプションを付けて実行します。
    どのOpenSSLを使っているかバナーに表示されるので、OSにインストールされたOpenSSLではなく、testssl.shのサイトからダウンロードしたOpenSSLを使っていることを確認します。
  5. testssl.shのサイトから mapping-rfc.txt をダウンロードして、testssl.shと同じディレクトリに置きます。
    これがあると、暗号スイートの一覧を表示する際にOpenSSL形式の表記とRFCでの表記を両方表示してくれるので便利です。

testssl.shの使い方

  1. サンプルのecho_serverを8443番ポートをlistenする設定で実行します。
    $ java -jar target/java-sslsocket-sample-1.0.jar echo_server 8443 certs-rsa-sha2/rsa2048.cer certs-rsa-sha2/rsa2048_pkcs8.der
  2. testssl.sh を localhost:8443 に対して実行します。
    $ ./testssl.sh localhost:8443
  3. サポートしているプロトコルや暗号スイートのリスト、Server Hello メッセージの内容、SSL/TLSの既知の著名な脆弱性への対応状況などが表示されます。
    ターミナル上でカラフルに表示されますので、ahaなどを使ってカラフルなHTMLに出力を変換すると後々見やすいです。


本記事執筆時に試してみると、以下の項目が赤字で表示され、明らかに問題ありと判定されました。それ以外については緑色系の文字で表示され、問題は指摘されませんでした。

  • Testing server preferences
    Has server cipher order?     nope (NOT ok)
  • Testing vulnerabilities
    Secure Client-Initiated Renegotiation     VULNERABLE (NOT ok), DoS threat

次回は、なぜこれらがNGとされているのか、そしてOKとするためにどんな設定が必要か検証する予定です。

  • 補足1

    実際にどんな検査を行い、どんなロジックで判定しているかについてはtestssl.shのソースコードを読んでみてください。全てshell scriptで記述されているため少々可読性に難はありますが、opensslコマンドを駆使していますので新しい発見もあると思います。
    またtestssl.shの判定ロジックについては絶対的なものではないため、実際に脆弱性検査を意識して使う際は、判定結果をどこまで信頼するか、どう扱うかについて検討が必要です。

  • 補足2

     testssl.sh はコマンドラインオプションによって、どのチェックを行うか切り替えることもできます。例として、プロトコルバージョンごとの暗号スイート対応状況を調べたい場合は -E オプションを使います。

    $ ./testssl.sh -E localhost:8443

Nmapの使い方(暗号スイートの一覧取得)

Nmapはネットワーク診断などに使われるオープンソースソフトウェアのため、沢山の機能があります。
今回は対象サーバがサポートしている暗号スイートの一覧を取得する機能(ssl-enum-ciphers)だけに着目して、使ってみます。

ssl-enum-ciphersの詳細は以下のページを参照してください:

  1. 公式サイトの説明に従いインストールします。(説明は省略)
  2. サンプルのecho_serverを8443番ポートをlistenする設定で実行します。
    $ java -jar target/java-sslsocket-sample-1.0.jar echo_server 8443 certs-rsa-sha2/rsa2048.cer certs-rsa-sha2/rsa2048_pkcs8.der
  3. Nmapを ssl-enum-ciphers スクリプトを指定して localhost:8443 に対して実行します。
    $ nmap --script ssl-enum-ciphers -p 8443 localhost
  4. サポートしている暗号スイートが、プロトコルバージョンごとに表示されます。

まとめ

全3回を予定しているSSL/TLS接続の実験記事のうち、初回である本記事では以下の内容を紹介しました。

  1. サンプルアプリケーションの紹介と、ビルドと実行方法
  2. サンプルアプリケーションのソースコードの簡単な解説
  3. testssl.sh, Nmapを使ったSSL/TLSサーバの設定確認

次回はtestssl.shの実行でNGとなった点の調査と検証や、その他のtestssl.shチェック項目についてわざとNGとなる設定をしてみて、SSL/TLSの脆弱性についていくつか取り上げていきたいと思います。

次回もよろしくお願いいたします。

(2016年3月31日更新)

今までのコラム

Webセキュリティをざっくり理解するための3つのMAP

Page Top