Skip to main content

JavaでTLSを実装する

著者

Himanish Munjal

blog feature toolkit

2022年11月9日

0 分で読めます

TLS(Transport Layer Security)は、インターネット上の通信を暗号化し、安全に保護するために世界中で使われているプロトコルです。

この記事では、TLSとは何か、どのようなメリットがあるのか、なぜ必要なのかを解説します。その後、JavaでTLSを実装する方法を順を追って説明します。

TLSとは

TLSとその前身であるSSL(Secure Sockets Layer)は、暗号化、真正性、完全性を実現するために最も広く使われている暗号プロトコルです。これにより、インターネットを介してアプリケーション間で送信されるデータをエンドツーエンドで保護できます。

TLSとSSLのプロトコルは、アプリケーション層とTCP/IP層の間に位置し、アプリケーションデータを保護してトランスポート層に送信します。この位置にあるため、TLSとSSLは複数のアプリケーション層プロトコルに対応できます。OSI参照モデルの詳細はこちらの記事をご覧ください。

Netscapeが開発したSSL 1.0には複数のセキュリティ上の欠陥があり、一般公開されませんでした。その後、1995年にSSL 2.0、1996年にSSL 3.0が登場しました。SSLは名称が変更され、1999年にTLS 1.0がリリースされました。現行バージョンのTLS 1.3は2018年に登場しました。既知の脆弱性から保護するため、常に最新バージョンのTLSを使用することをおすすめします。この記事でTLSの詳細をご覧ください。

TLSの重要性

かつて、インターネット上の通信はセキュリティがほとんどない平文で行われていました。そのため、サーバーに送信したデータはネットワーク上で見える状態にあり、中間者攻撃を受ける可能性がありました。これは、悪意のある攻撃者がユーザーとアプリケーションのやり取りに割り込む攻撃です。攻撃者は送信中のデータを閲覧・改ざんでき、パスワードや銀行口座情報などの個人情報が漏えいするおそれがあります。

こうした問題を解決するためにSSLとTLSが導入されました。主なメリットは次の3つです。

  • 完全性の保護:TLSは、通信を傍受する攻撃者によるメッセージの改ざんを防ぎます。

  • 認証:多くのモードで、TLSは署名付き証明書を使って通信相手を認証します。通常はサーバーが認証され、サーバーから要求された場合はクライアントも認証されます。この認証はTLSハンドシェイクの一環として行われます。

  • 機密性:TLSはクライアントとサーバー間で送信されるデータを暗号化し、データの機密性とプライバシーを保護します。これにより、通信を受動的に傍受する攻撃者が、マシン間でやり取りされる機密データを閲覧できなくなります。

SSLまたはTLSを実装すれば、ユーザーは自分のデータが公開されたり、送受信中に改ざんされたりしないと確信できます。そのため、インターネットに公開するアプリケーションではTLSの実装が不可欠です。顧客のデータを安全に保護できます。

基本概念

JavaでTLSを実装する方法を詳しく見る前に、TLSの低レベルな仕組みを理解するうえで役立つ概念をいくつか確認しましょう。

ソケット

ソケットとは、ネットワークで接続されたプログラム間の双方向通信インターフェースの片端です。基盤となるトランスポート層(TCPまたはUDP)は、ソケットをポート番号にバインドすることで、データの送信先となるアプリケーションを識別します。

ソケットクラスは、クライアントアプリケーションとサーバーアプリケーション間の接続を表します。java.netパッケージにはSocketとServerSocketの2つのクラスがあり、それぞれ接続のクライアント側とサーバー側を実装します。

ソケットについて詳しく知りたい方は、こちらの記事をご覧ください。

TLS通信は、SSLSocketとSSLServerSocketというセキュアソケットを介して行われます。これらのクラスは、それぞれSocketクラスとServerSocketクラスを拡張したものです。

TLSハンドシェイク

SSLまたはTLSのハンドシェイクとは、クライアントとサーバーが互いを認証し、通信に使用する秘密鍵を確立するために行う一連の処理です。

TLSハンドシェイクの詳細は非常に幅広く、この記事で扱う範囲を超えています。TLSハンドシェイクを理解するための入門としては、TLSハンドシェイクで何が起きるのかを解説した記事と、SSLまたはTLSハンドシェイクの概要がおすすめです。

Java LTS

Java LTS(長期サポート)リリースは、長期間にわたって四半期ごとのセキュリティ、安定性、パフォーマンスに関するアップデートが提供されるバージョンです。標準(非LTS)バージョンは、次のバージョンがリリースされるまでしかアップデートや修正が提供されません。

Java 7、8、11、および最近リリースされたJava 17はLTSバージョンです。サポート期間が長いため、本番環境ではLTSバージョンのみを使用することが推奨されます。

他のソフトウェアと同様に、各バージョンには新機能や改善が含まれています。Java 11でリリースされた機能はこちら、Java 17の機能はこちらで確認できます。Javaのバージョンごとに機能が異なるため、TLSの実装中に予期しない問題が発生した場合は、お使いのバージョンのドキュメントをご確認ください。

JavaでTLSを実装する

理論の説明はここまでです。いよいよ実装に取りかかりましょう。この記事の以降のセクションでは、JSSEフレームワークと、JSSE APIを使ってTLSを実装する方法を紹介します。

JSSE API

Java Secure Socket Extension(JSSE)は、トランスポート層セキュリティ(TLS)の基盤となる実装を抽象化するフレームワークです。基盤となるプロトコルの複雑さを抽象化することで、プログラマーは安全な暗号化通信を簡単に実装できます。JSSEはTLSプロトコルを使用し、クライアントとサーバー間の安全な暗号化通信を実現します。

JSSEの標準実装として、主に次の4つのクラスが用意されています。

  • SSLSocketFactory:セキュアソケットを作成するファクトリクラスです。

  • SSLServerSocketFactory:SSLSocketFactoryクラスに相当しますが、サーバーソケットの作成に特化しています。

  • SSLSocket:SSLSocketはSocketを拡張したクラスです。TCPやUDPなどの基盤となるネットワークトランスポートプロトコルにセキュリティ保護を追加し、SSLとTLSのメリットを提供します。

  • SSLServerSocket:SSLSocketクラスとよく似たクラスです。主な違いは、SSLServerSocketはサーバー側のソケット作成に使用され、SSLSocketはクライアント側のソケット作成に使用される点です。

これらのクラスを使って、クライアント側とサーバー側のTLSを実装します。

クライアント側でTLSを実装する

このセクションでは、SSLSocketFactoryとSSLSocketを使って、クライアント側のTLSを実装します。

JavaTLSClientExample.classという名前のテキストファイルを作成し、以下のコードをコピーして貼り付け、ファイルを保存します。

package io.snyk.programming.tls;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.io.*;

public class JavaTLSClientExample {

    private static final String[] protocols = new String[]{"TLSv1.3"};
    private static final String[] cipher_suites = new String[]{"TLS_AES_128_GCM_SHA256"};

    public static void main(String[] args) throws Exception {

        SSLSocket socket = null;
        PrintWriter out = null;
        BufferedReader in = null;

        try {

            // Step : 1
            SSLSocketFactory factory = (SSLSocketFactory) SSLSocketFactory.getDefault();

            // Step : 2
            socket = (SSLSocket) factory.createSocket("google.com", 443);

            // Step : 3
            socket.setEnabledProtocols(protocols);
            socket.setEnabledCipherSuites(cipher_suites); 

            // Step : 4 {optional}
            socket.startHandshake(); 

            // Step : 5
            out = new PrintWriter(
                    new BufferedWriter(
                            new OutputStreamWriter(
                                    socket.getOutputStream())));

            out.println("GET / HTTP/1.0");
            out.println();
            out.flush();

            if (out.checkError()) {
                System.out.println("SSLSocketClient:  java.io.PrintWriter error");
            }

            // Step : 6
            in = new BufferedReader(
                    new InputStreamReader(
                            socket.getInputStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println(inputLine);
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                socket.close();
            }
            if (out != null) {
                out.close();
            }
            if (in != null) {
                in.close();
            }
        }
    }
}

コードを実行するには、ターミナルを開き、cdコマンドを使って上記のファイルを保存したフォルダに移動します。cd <PathToFile>を実行してください。

javac JavaTLSClientExample.classを実行してファイルをコンパイルします。次に、java JavaTLSClientExample.classコマンドでファイルを実行します。

サンプルコードの各ステップで行われる処理を見ていきましょう。

最初のステップではSSLSocketFactoryを取得します。これを使ってソケットを作成できます。

SSLSocketFactoryを取得する方法はいくつかありますが、この例ではSSLSocketFactory.getDefault()静的メソッドを呼び出し、デフォルトのファクトリを取得します。

2番目のステップでは、SSLソケットを作成します。createSocketメソッドを呼び出し、リモートホストやリモートポートなどの情報を指定します。これによりソケットが作成され、指定したリモートホストの指定ポートに接続されます。

3番目のステップでは、パラメーターを設定します。まず、ソケットでサポートするTLSのバージョンを指定します。この記事の執筆時点で最新のTLSバージョンはTLS v1.3のため、上記の例ではこのバージョンを使用しています。また、CipherSuiteのTLS_AES_128_GCM_SHA256を指定し、TLSハンドシェイクの各ステップで使用するパラメーターのセットを定義します。

4番目のステップでは、ハンドシェイクを開始します。ここでTLSハンドシェイクを明示的に開始します。

ハンドシェイク中、ソケットは前のステップで設定したプロトコルとCipherSuiteを使用します。

5番目のステップではセットアップが完了し、サーバーに実際のリクエストを送信します。このステップでは、テキスト情報をソケットの出力ストリームに渡します。OutputStreamWriterは文字ストリームをバイトストリームに変換します。BufferedWriterは変換処理が頻繁に呼び出されるのを防ぎ、PrintWriterはオブジェクトを整形してテキスト出力ストリームに書き込みます。PrintWriterは例外をスローしないため、checkErrorメソッドを使って例外を明示的に確認する必要があります。

最後の6番目のステップでは、応答を読み取ります。リクエストの送信後、ソケットのinputStreamから応答を読み取り、出力します。

サーバー側でTLSを実装する

次に、SSLServerSocketFactoryとSSLServerSocketを使って、サーバー側のTLSを実装する方法を見ていきます。

JavaTLSServerExample.classというファイルを作成し、以下のコードを貼り付けます。

package io.snyk.programming.tls;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLServerSocket;
import javax.net.ssl.SSLServerSocketFactory;
import java.io.*;

public class JavaTLSServerExample {

    private static final String[] protocols = new String[]{"TLSv1.3"};
    private static final String[] cipher_suites = new String[]{"TLS_AES_128_GCM_SHA256"};

    public static void main(String[] args) throws Exception {

        SSLServerSocket serverSocket = null;

        try {

            // Step : 1
            SSLServerSocketFactory factory =
                    (SSLServerSocketFactory) SSLServerSocketFactory.getDefault();

            // Step : 2
            serverSocket = (SSLServerSocket) factory.createServerSocket(8980);

            // Step : 3
            serverSocket.setEnabledProtocols(protocols);
            serverSocket.setEnabledCipherSuites(cipher_suites);

            // Step : 4
            SSLSocket sslSocket = (SSLSocket) serverSocket.accept();

            // Step : 5
            InputStream inputStream = sslSocket.getInputStream();
            InputStreamReader inputStreamReader = new
                    InputStreamReader(inputStream);

            BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
            String request = null;
            while((request = bufferedReader.readLine()) != null) {
                 System.out.println(request);
                 System.out.flush();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (serverSocket != null) {
                serverSocket.close();
            }
        }
    }
}

コードを実行するには、先ほどファイルを保存したフォルダで、もう一度ターミナルを開きます。

javac JavaTLSServerExample.classを実行してファイルをコンパイルします。

こちらのガイドに従って、CA証明書をセットアップし、キーストアに追加します。

java -Djavax.net.ssl.keyStore=<KeystoreFilePath> -Djavax.net.ssl.keyStorePassword=<KeystorePassword> JavaTLSServerExampleを実行してファイルを起動します。Djavax.net.ssl.keyStoreと-Djavax.net.ssl.keyStorePasswordは、サーバーのSSL証明書をJavaランタイムに渡すために必要です。

コードの各ステップをもう一度確認しましょう。ステップ1〜3は、クライアント側のTLSの例におけるステップ1〜3と同様です。

ステップ1では、デフォルトのSSLSocketFactoryを取得します。

ステップ2では、SSLServerSocketを作成し、ポート8990にバインドします。

ステップ3では、SSLServerSocketがサポートするプロトコルとCipherSuiteを定義します。

ステップ4では、受信リクエストを受け付けます。SSLServerSocket.accept()は、このサーバーソケットへの接続を待ち受け、接続を受け付けて新しいソケットを作成します。接続が確立されるまで、このメソッドはブロックします。

ステップ5では、ソケットの入力ストリームからデータを読み取り、出力します。InputStreamReaderはバイトストリームを文字ストリームに変換し、BufferedReaderは変換処理が頻繁に呼び出されるのを防ぎます。

JSSE APIとSSLSocketFactoryの関係

SSLSocketFactory、SSLServerSocketFactory、SSLSocket、SSLServerSocketを含むJSSE標準APIを示す図。

前述のとおり、JSSEはフレームワークです。JSSEは、クライアントとサーバー間の接続を保護する複雑さを抽象化するさまざまなクラスと実装を提供します。また、クライアントとサーバーのセキュアソケットをセットアップする際に、デフォルト実装として使用できる一連の実装クラスも用意されています。

クライアント側では、SSLSocketFactoryを使ってデフォルトのファクトリを取得し、それを使ってクライアント側のSSLSocketを取得します。サーバー側では、SSLServerSocketFactoryを使ってSSLServerSocketを初期化します。

まとめ

インターネット経由で通信するJavaアプリケーションを作成した場合、クライアント側とサーバー側の両方でTLSを実装する必要があります。TLSは暗号化と完全性の保護によって安全なデータ転送を実現し、双方の認証にも役立ちます。

JSSEは、アプリケーションへのTLS実装に役立つJavaのフレームワークです。SSLSocketやSSLServerSocketなどのクラスを使用して、セキュアソケットを作成できます。

続きを読む

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSecのご紹介:AI開発のスピードに追随するセキュリティ

AIを活用した開発は、従来のセキュリティワークフローを上回るスピードで進んでいます。SnykのAgentic AppSecは、セキュリティエージェントが脆弱性を継続的に検出、修正、検証し、人が基準を定められるようにします。

feature insights context
Blog

自律型攻撃はすでに始まっている。防御もそのスピードに追いつかなければならない。

自律型攻撃者によって、防御に使える時間は短くなっています。継続的な検出、修復、検証、予防で、セキュリティチームが攻撃に歩調を合わせる方法をご紹介します。

illustration hero ai
Blog

AIハリケーンが到来

AIはソフトウェア開発とサイバー攻撃の双方を加速させています。リーダーは、エージェントとコードを開発の初期段階から保護し、実行時に制御を徹底するとともに、防御策を独立して検証しなければなりません。