Play Framework で SSL/TLS を利用する方法


本記事では、Java Secure Socket Extention（JSSE） API を利用して、Play Framework で実装した Web アプリケーションでSSL/TLSを利用する方法を紹介します。

本記事の元ネタはこちらです。
https://github.com/wsargent/activator-play-tls-example

本記事執筆時点で最新の activator 1.2.12 に対応させたサンプルをこちらに置いています。動作確認の際はこちらを利用ください。
https://github.com/ogis-onishi/activator-play-tls-example

## 前提
* Java 8 をインストールしていること。
* `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` を利用するため、Unlimited Strength policy がインストールされている必要があります。
 * [Java downloads](http://www.oracle.com/technetwork/java/javase/downloads/index.html) にアクセス.
 * "Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files for JDK/JRE 8" で検索。
 * ダウンロードしたポリシーファイルを $JAVA_HOME/lib/security/ にコピー

## 自己署名証明書の生成
（注意）パブリック認証局の証明書を使う場合は、証明書を自分で生成する必要はありません。

HTTPSを使うためには、X.509 証明書を用意する必要があります。証明書の生成は非常に面倒な作業のため、scripts ディレクトリに置いてある生成用のスクリプトを利用してください。証明書の生成に関する詳細は、Play WS SSL の [Certificate Generation](http://www.playframework.com/documentation/2.3.x/CertificateGeneration) セクションを参照してください。

証明書を生成するには、以下を実行してください。

```
cd scripts/
./gencerts.sh
```

`certs` ディレクトリを作成し、生成されたファイルを全て移動してください。

```
cd scripts
mkdir ../certs
mv client* ../certs
mv example* ../certs
mv password ../certs
```

## ホスト名の設定 ( example.com -> localhost）

本サンプルでは、認証中のホスト名を `example.com` にしています。SSLを利用するためにはホスト名を `example.com` にする必要があります。本サンプルの動作確認中は、`example.com` が `localhost` を指すように`/etc/hosts` を修正してください。

```
$ sudo vim /etc/hosts
127.0.0.1       example.com www.example.com
```

（注意）本番環境では正しいホスト名を設定した認証証明書を利用してください。

## HTTPS 設定で Play を起動する

証明書を生成し、ホスト名の設定も行ったため、ようやく Play を起動できます。

本アプリケーションは、`activator` ではなく `./play` を利用して起動してください。スクリプトでアプリケーションを実行するためのシステムプロパティを設定しています。

```
./play run
```

`app/https/CustomSSLEngineProvider` が Play の HTTPS サーバに相当します。詳細については、 [Configuring HTTPS](http://www.playframework.com/documentation/2.3.x/ConfiguringHttps) を参照してください。

## （任意）暗号スイートのリストをチェックする

（注意）以下は動作確認のための作業です。スキップしても設定上問題ありません。

SSLyze をダウンロードしてください。

[https://github.com/iSECPartners/sslyze/releases](https://github.com/iSECPartners/sslyze/releases)

Play アプリケーションに対して、SSLyze を実行してください。

```
cd sslyze-0_9-osx64
python sslyze.py --regular www.example.com:9443
```

以下のような結果が出力されます。

```
 REGISTERING AVAILABLE PLUGINS
 -----------------------------

  PluginOpenSSLCipherSuites
  PluginCertInfo
  PluginCompression
  PluginHSTS
  PluginHeartbleed
  PluginSessionRenegotiation
  PluginSessionResumption



 CHECKING HOST(S) AVAILABILITY
 -----------------------------

   www.example.com:9443                => 127.0.0.1:9443



 SCAN RESULTS FOR WWW.EXAMPLE.COM:9443 - 127.0.0.1:9443
 ------------------------------------------------------

  * Session Renegotiation:
      Client-initiated Renegotiations:   Rejected
      Secure Renegotiation:              Supported

  * Compression:
      DEFLATE Compression:               Disabled

  * Heartbleed:
      OpenSSL Heartbleed:                NOT vulnerable

Unhandled exception when processing --certinfo:
exceptions.KeyError - 'exponent'

  * Session Resumption:
      With Session IDs:                  Not supported (0 successful, 5 failed, 0 errors, 5 total attempts).
      With TLS Session Tickets:          Not Supported - TLS ticket not assigned.

  * SSLV2 Cipher Suites:
      Server rejected all cipher suites.

  * TLSV1_2 Cipher Suites:
      Preferred:
                 ECDHE-ECDSA-AES256-SHA384     256 bits      HTTP 200 OK
      Accepted:
                 ECDHE-ECDSA-AES256-SHA384     256 bits      HTTP 200 OK
                 ECDHE-ECDSA-AES256-SHA        256 bits      HTTP 200 OK
                 ECDHE-ECDSA-AES256-GCM-SHA384 256 bits      HTTP 200 OK
                 ECDHE-ECDSA-DES-CBC3-SHA      168 bits      HTTP 200 OK
                 ECDHE-ECDSA-RC4-SHA           128 bits      HTTP 200 OK
                 ECDHE-ECDSA-AES128-SHA256     128 bits      HTTP 200 OK
                 ECDHE-ECDSA-AES128-SHA        128 bits      HTTP 200 OK
                 ECDHE-ECDSA-AES128-GCM-SHA256 128 bits      HTTP 200 OK

  * TLSV1_1 Cipher Suites:
      Preferred:
                 ECDHE-ECDSA-AES256-SHA        256 bits      HTTP 200 OK
      Accepted:
                 ECDHE-ECDSA-AES256-SHA        256 bits      HTTP 200 OK
                 ECDHE-ECDSA-DES-CBC3-SHA      168 bits      HTTP 200 OK
                 ECDHE-ECDSA-RC4-SHA           128 bits      HTTP 200 OK
                 ECDHE-ECDSA-AES128-SHA        128 bits      HTTP 200 OK

  * TLSV1 Cipher Suites:
      Preferred:
                 ECDHE-ECDSA-AES256-SHA        256 bits      HTTP 200 OK
      Accepted:
                 ECDHE-ECDSA-AES256-SHA        256 bits      HTTP 200 OK
                 ECDHE-ECDSA-DES-CBC3-SHA      168 bits      HTTP 200 OK
                 ECDHE-ECDSA-RC4-SHA           128 bits      HTTP 200 OK
                 ECDHE-ECDSA-AES128-SHA        128 bits      HTTP 200 OK

  * SSLV3 Cipher Suites:
      Preferred:
                 ECDHE-ECDSA-AES256-SHA        256 bits      HTTP 200 OK
      Accepted:
                 ECDHE-ECDSA-AES256-SHA        256 bits      HTTP 200 OK
                 ECDHE-ECDSA-DES-CBC3-SHA      168 bits      HTTP 200 OK
                 ECDHE-ECDSA-RC4-SHA           128 bits      HTTP 200 OK
                 ECDHE-ECDSA-AES128-SHA        128 bits      HTTP 200 OK



 SCAN COMPLETED IN 9.51 S
 ------------------------
 ```

## （任意）クライアント証明書による認証を有効にする

クライアント証明書によるクライアント認証を有効にしたい場合、`./play` スクリプトの以下の設定をコメントアウトしてください。

```
JVM_OPTIONS="$JVM_OPTIONS -Dplay.ssl.needClientAuth=true"
```

サーバを再起動すると以下のような出力を確認できます。

```
   ECDHE-ECDSA-RC4-SHA             ClientCertificateRequested - Server requested a client certificate issued by one of the following CAs: '/C=US/ST=California/L=San Francisco/O=Example Company/OU=Example Org/CN=clientca'.
```

サーバがクライアント認証を要求するようになりました。クライアントは接続が確立する前に `clientca` ルート証明書で署名されたクライアント証明書を提供しなければなりません。

## （任意）Play WS （HTTP クライアントライブラリ）でサーバに接続する

本節では、[Play WS](http://www.playframework.com/documentation/2.3.x/ScalaWS) という HTTP/HTTPS クライアントライブラリを利用して、[TLS 上のクライアント認証](http://www.playframework.com/documentation/2.3.x/WsSSL) を行う方法を紹介します。

（注意）ブラウザでのみサーバにアクセスし、HTTP / HTTPS クライアントを作成しない場合は不要です。

HTTPS クライアントの設定ファイル `ws.conf` は以下のようになっています。

```
ws.ssl {

  protocol = "TLSv1.2"

  enabledProtocols = [ "TLSv1.2" ]

  enabledCiphers = [
    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
  ]

  ws.ssl.disabledSignatureAlgorithms = "MD2, MD4, MD5, SHA1, RSA"

  ws.ssl.disabledKeyAlgorithms = "EC keySize < 384"

  keyManager = {
    stores = [
      /* Note: app must be run from ./play, which loads the KEY_PASSWORD environment variable. */
      { type: "JKS", path: "certs/client.jks", password: ${?KEY_PASSWORD} },
    ]
  }

  trustManager = {
    stores = [
      { type = "JKS", path = "certs/exampletrust.jks" }
    ]
  }
}
```

`TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` は [Suite B Profile for Transport Layer Security](http://tools.ietf.org/html/rfc5430) に定義されている強力な暗号スートです。 (Bruce Schneier は[ECC 定数は NSA に操作されている](https://www.schneier.com/blog/archives/2013/09/the_nsa_is_brea.html#c1675929) と信じていますが、他のオプションはこの点について制限されています。）

前述のとおり、この暗号スイートを利用するには、JCE Unlimited Policy ファイルをインストールする必要があります。まだインストールしていない場合は、前提のセクションを再度確認してください。

[Play WS](http://www.playframework.com/documentation/2.3.x/ScalaWS) スタンドアロンなアプリケーションで直接利用することができます。新しいシェルを立ち上げ、以下をタイプしてください。

```
$ ./play
> runMain Main
```

以下の結果が出力されます。

```
[info] Running Main
header = (Content-Length,Buffer(106))
header = (Content-Type,Buffer(text/html; charset=utf-8))
body =
<!DOCTYPE html>
<html>
 <body>
   <h1>Congratulations!  You are reading the page!</h1>
 </body>
</html>
```

クライアントキーでのみ動くことを検証するため、`ws.conf` の keyManager セクションをコメントアウトし、`Main` を再実行してください。クライアント認証が失敗します。

```
failure = java.net.ConnectException: Received fatal alert: bad_certificate to https://example.com:9443/
```

## ブラウザでサーバに接続する

`./play` を利用すると、以下の構成で Play アプリケーションが起動します。

* HTTP プロトコルは無効
* HTTPS プロトコルは 9443 ポートで有効

ブラウザで https://example.com:9443 にアクセスすると、サーバ証明書がパブリック認証局に署名されていないため、警告が出ます。

警告が出ないようにするためには、`certs/example.com.crt` をブラウザにインポートしてください。以下の手順で証明書をインポートするツールを開くことができます。

* Chromeの場合、設定 → HTTPS/SSL → 証明書の管理 → （OSごとに異なるツールが開きます。CA証明書を追加してください。）
* Firefox の場合、設定 → 詳細 → 証明書 → 証明書の表示 → 認証局証明書 → 読み込む → certs/example.com.crt を選択

再度、アクセスすると今度は Web ページが正しく開きます。

## おわりに

今回のアプリケーションは TLS 1.2 と ECDSA 証明書を使って動作しています。詳細については `./play` スクリプトと証明書生成スクリプトを確認してください。

## TODO: 導入のための要変更事項

今回のサンプルを実際にアプリケーションに利用する際の注意事項を示します。