Jetty 9.4 をHTTPS (SSL/TLS) モードで実行する。Javaコードのみでの実現方法を詳解。Keytool、Servlet、クライアント証明書認証までハンズオン

#概要

- Javaの軽量Web APサーバーである[Jetty9](https://www.eclipse.org/jetty/) をつかってHTTPS(SSL/TLS)サーバーを作る最小コードを掲載します。
- Jetty関連の外部設定ファイル不要、コードのみで起動できます。
- 実験用にオレオレ証明書をつくります。OpenSSL等の外部ソフトウェアは不要です。JDK環境だけでOKです。
- 他のカスタマイズ例も掲載します
    - Step1.Jettyの仕組み（コネクション、ハンドラの概念あたりを中心に）おさらい
    - Step2.HTTPとHTTPSを両方ともホストする方法の最小コード
    - Step3.HTTPでアクセスがあったら、自動的にHTTPSにリダイレクトする方法の最小コード
    - Step4.Servletが共存する場合の最小コード
    - Step5.SSLクライアント認証のやり方の最小コード

### 環境
- Jettyのバージョンは2018年12月現在最新の [Jetty 9.4.14.v20181114](https://github.com/eclipse/jetty.project)
- Windows環境で作成(MacやLinuxでも同等)
- JDK(JDK9,JDK10)、ブラウザ(Chrome 70、Firefox 63、Edge) にて動作確認済

### ソースコード
- 本投稿で作成したソースコードはすべて https://github.com/riversun/jetty9-minimal-ssl  で公開しています


# 20行で実動するHTTPSサーバーのソースコード

JettyをHTTPSサーバーとして起動するコードは以下のとおり。
たったこれだけ。


```java
package com.example;
import org.eclipse.jetty.server.*;
import org.eclipse.jetty.server.handler.ResourceHandler;
import org.eclipse.jetty.util.ssl.SslContextFactory;

public class MinimalSSLServer {
	public static void main(String[] args) throws Exception {
		final Server server = new Server();
		final SslContextFactory sslContextFactory = new SslContextFactory();
		sslContextFactory.setKeyStorePath(System.getProperty("user.dir") + "/mykeystore.jks");
		sslContextFactory.setKeyStorePassword("mypassword");
		final ServerConnector httpsConnector = new ServerConnector(server, sslContextFactory);
		httpsConnector.setPort(443);
		final ResourceHandler resourceHandler = new ResourceHandler();
		resourceHandler.setResourceBase(System.getProperty("user.dir") + "/htdocs");
		server.setConnectors(new Connector[] { httpsConnector });
		server.setHandler(resourceHandler);
		server.start();
		server.join();
	}
}

```

以下、それぞれの行をみていく

```java:Jettyサーバーを生成する
final Server server = new Server();
```

```java:キーストアの保存場所とキーストアのパスワードを指定
final SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setKeyStorePath(System.getProperty("user.dir") + "/mykeystore.jks");
sslContextFactory.setKeyStorePassword("mypassword");
```

```java:HTTPSポート(443)をListenするコネクタを生成
final ServerConnector httpsConnector = new ServerConnector(server, sslContextFactory);
httpsConnector.setPort(443);
```

```java:静的コンテンツ用のハンドラを準備して/htdocsディレクトリ以下を公開
final ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setResourceBase(System.getProperty("user.dir") + "/htdocs");
```

```java:HTTPS用のコネクタをセットし、かつ静的コンテンツ用のハンドラをサーバーにセットする
server.setConnectors(new Connector[] { httpsConnector });
server.setHandler(resourceHandler);
```

```java:jettyを起動する
server.start();
server.join();
```

## Maven/Gradleの設定

依存関係を記述する

```XML:POM.xml
	<dependencies>
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-webapp</artifactId>
			<version>9.4.14.v20181114</version>
		</dependency>
	</dependencies>
```

```shell:Gradle
compile group: 'org.eclipse.jetty', name: 'jetty-webapp', version: '9.4.14.v20181114'
```

# Javaのkeytoolをつかってオレオレ証明書を作る

サーバーはあっという間にできたが、
証明書(オレオレ証明書でOK)が無いとサーバーを起動できないので、サーバー起動のために証明書を作成する。
JDKに標準で添付されているkeytoolをつかってキーストアを作る。

## keytool の使い方

keytoolは [JAVA_HOME]/bin にあるので、そこにパスを通しておく。

Windowsなら **C:/Program Files/Java/jdk-x.x.x/bin** 以下にkeytoolコマンドがある。

## keytoolをつかってキーストア(keystore)を作成する

コマンドラインで以下のようにする

```shell
keytool -genkey -dname "cn=localhost, ou=Example div., o=Example Inc., l=Minato-ku, st=Tokyo, c=JP" -alias jetty -keystore mykeystore.jks -storepass mypassword -keypass mypassword -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -validity 3650 -ext SAN=dns:localhost
```

<table>
	<tr>
		<th>オプション</th>
		<th>値</th>
		<th>意味</th>
	</tr>
	<tr>
		<th>-genkeypair</th>
		<td></td>
		<td>鍵のペアを生成する</td>
	</tr>
	<tr>
		<th>-dname</th>
		<td>※1.下で説明</td>
		<td>サブジェクトの内容を指定する</td>
	</tr>
	<tr>
		<th>-alias</th>
		<td>jetty </td>
		<td>キーストア内で鍵を特定するための名前</td>
	</tr>
	<tr>
		<th>-keystore</th>
		<td>mykeystore.jks</td>
		<td>キーストアのファイル名</td>
	</tr>
	<tr>
		<th>-storepass</th>
		<td>mypassword</td>
		<td>キーストアのパスワード</td>
	</tr>
	<tr>
		<th>-keypass</th>
		<td>mypassword</td>
		<td>秘密鍵のパスワード</td>
	</tr>
	<tr>
		<th>-keyalg</th>
		<td>RSA</td>
		<td>鍵ペアの生成アルゴリズムにRSAを指定</td>
	</tr>
	<tr>
		<th>-keysize</th>
		<td>2048</td>
		<td>キー長を2048ビットに指定</td>
	</tr>
	<tr>
		<th>-sigalg</th>
		<td>SHA256withRSA</td>
		<td>自己署名証明書への署名アルゴリズムにSHA256指定。</td>
	</tr>
	<tr>
		<th>-validity</th>
		<td>3650</td>
		<td>証明書の有効日数 10年</td>
	</tr>
	<tr>
		<th>-ext</th>
		<td>※2.下で説明</td>
		<td>X.509証明書の拡張機能を指定</td>
	</tr>
</table>

### ※1 dnameで指定するコモンネーム(cn)は、対象のドメイン名(FQDN)にあわせる

**-dname** に指定している"**cn=localhost, ou=Example div., o=Example Inc., c=JP**"の意味は以下のとおり。

重要なのはコモンネーム(cn)の項目で、これをHTTPSをホストするドメイン名(FQDN)にあわせる必要がある。
ここではドメイン名を **localhost** にしている。

<table>
	<tr>
		<th>サブジェクト</th>
		<th>値</th>
		<th>説明</th>
	</tr>
	<tr>
		<th>cn</th>
		<td>localhost</td>
		<td>コモンネーム<br>HTTPSしたいURL(FQDN)と一致させる</td>
	</tr>
	<tr>
		<th>ou</th>
		<td>Example div.</td>
		<td>小さな組織</td>
	</tr>
	<tr>
		<th>o</th>
		<td>Example inc.</td>
		<td>大きな組織</td>
	</tr>
	<tr>
		<th>l</th>
		<td>Minato-ku</td>
		<td>地域名</td>
	</tr>
	<tr>
		<th>st</th>
		<td>Tokyo</td>
		<td>地方名</td>
	</tr>
	<tr>
		<th>c</th>
		<td>JP</td>
		<td>国コード</td>
	</tr>
</table>

### ※2 SAN は Chromeブラウザ対策で。

**-ext** に "**SAN=dns:localhost**"を指定している件。

SANは Subject Alternative Name の略でサブジェクトの別名という意味。

本来は**SANに代替のサブドメイン名つきドメイン名(FQDN)など書いておくとコモンネームで指定したドメイン名以外にそちらも有効になる**という効能がある。

今回はオレオレ証明書を使うホストが**localhost**を想定しているが、SANとして**localhost**を再度指定した。
これはchromeブラウザ対策で、chromeブラウザはSANが無いと警告を出す仕様になっているため。

これでキーストア **mykeystore.jks**が完成した。


### (コラム)KeytoolまわりのTips

**・PKCS12形式** のキーストアができる
ここで作成されるのはPKCS12形式のキーストアとなる。PKCS12形式の場合はPKCS12キーストアが生成される。
ストアのパスワード(storepassword)と鍵のパスワード(keypassword)は一致させる必要がある。

**・java.io.IOException: Incorrect AVA format**エラーが出る場合
-dnameで指定した oやouにカンマ(,)など指定できない文字列が入っていると発生する


# サーバーを起動して、HTTPSでアクセスする

正常動作するようにディレクトリにファイルを配置していく

## プロジェクト用のディレクトリを作る


(1)作業ディレクトリ**jetty9-minimal-ssl**を作り、ソースコード　**MinimalSSLServer.java
`** を以下のように配置した(mavenプロジェクトの標準的な形式)

```
jetty9-minimal-ssl
├── src/main/java
│   └── com.example
│       └── MinimalSSLServer.java
```

(2) ここに、さきほど作ったキーストアファイル**mykeystore.jks**を配置する

ここまでの**フォルダ構成**

```
jetty9-minimal-ssl
├── src/main/java
│   └── com.example
│       └── MinimalSSLServer.java
└── mykeystore.jks
```

(3)さらに、静的コンテンツをおくために、**/htdocs**ディレクトリをつくって、その下に**index.html**を配置する

```HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>Hello jetty9-https</h1>

</body>
</html>
```

ここまでの**フォルダ構成**

```
jetty9-minimal-ssl
├── src/main/java
│   └── com.example
│       └── MinimalSSLServer.java
├── htdocs
│   └── index.html
└── mykeystore.jks

```

(4)最後にmaven用のpom.xmlを配置する

```xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>org.riversun</groupId>
	<artifactId>jetty-minimal-ssl</artifactId>
	<version>1.0.0</version>
	<packaging>jar</packaging>
	<name>jetty-minimal-ssl</name>
	<description></description>
	
	<dependencies>
		<dependency>
			<groupId>org.eclipse.jetty</groupId>
			<artifactId>jetty-webapp</artifactId>
			<version>9.4.14.v20181114</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>exec-maven-plugin</artifactId>
				<version>1.6.0</version>
				<executions>
					<execution>
						<goals>
							<goal>java</goal>
						</goals>
					</execution>
				</executions>
				<configuration>
					<mainClass>com.example.MinimalSSLServer</mainClass>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>
```

ここまでの**フォルダ構成**

```
jetty9-minimal-ssl
├── src/main/java
│   └── com.example
│       └── MinimalSSLServer.java
├── htdocs
│   └── index.html
├── mykeystore.jks
└── pom.xml
```

# サーバーを起動する

```shell
cd jetty9-minimal-ssl
mvn compile exec:java
```

で起動する
(または、Eclipse等から run しても良い)

```shell
2018-12-08 14:26:02.242:INFO::com.example.MinimalSSLServer.main(): Logging initialized @2191ms to or
g.eclipse.jetty.util.log.StdErrLog
2018-12-08 14:26:02.306:INFO:oejs.Server:com.example.MinimalSSLServer.main(): jetty-9.4.14.v20181114
; built: 2018-11-14T21:20:31.478Z; git: c4550056e785fb5665914545889f21dc136ad9e6; jvm 9.0.1+11
2018-12-08 14:26:02.591:INFO:oejus.SslContextFactory:com.example.MinimalSSLServer.main(): x509=X509@
2ef809c3(jetty,h=[localhost],w=[]) for SslContextFactory@2b97d413[provider=null,keyStore=file:/dev/jetty9-minimal-ssl/mykeystore.jks,trustStore=null]
2018-12-08 14:26:02.783:WARN:oejusS.config:com.example.MinimalSSLServer.main(): No Client EndPointId
entificationAlgorithm configured for SslContextFactory@2b97d413[provider=null,keyStore=file:/dev/jetty9-minimal-ssl/mykeystore.jks,trustStore=null]
2018-12-08 14:26:02.805:INFO:oejs.AbstractConnector:com.example.MinimalSSLServer.main(): Started Ser
verConnector@3e7c7cea{SSL,[ssl, http/1.1]}{0.0.0.0:443}
2018-12-08 14:26:02.807:INFO:oejs.Server:com.example.MinimalSSLServer.main(): Started @2756ms
```

HTTPSサーバーが無事起動した

# ブラウザからアクセスしてみる

ローカルサーバーとして起動できたので、早速、**https**でアクセスする

https://localhost


## ブラウザにおこられる

Windows環境でchromeブラウザで https://localhost にアクセスしたらブラウザに怒られる。**オレオレ証明書**なので当然。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/f1679a27-32df-c959-10a0-234ea6671563.png)

そこで、怒られないようにするために、オレオレ証明書をブラウザにインポートする。

## Javaのキーストア(keystore)から証明書をエクスポートする

コマンドラインで以下を実行すると、さきほどつくったキーストア**mykeystore.jks**にある証明書をエクスポートできる。

```shell
keytool -export -alias jetty -storepass mypassword -file oreore.crt -keystore mykeystore.jks

証明書がファイル<oreore.crt>に保存されました
```

## オレオレ証明書をブラウザにインポートする

Windows環境の場合で説明する。

oreore.crt　をダブルクリックすると、以下のようなダイアログがでるので
**[証明書のインストール]**を選択するとインポートウィザードが開く

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/33baf430-d4ec-b2f0-4be5-05d974308086.png)

証明書ストアをどこにするか聞かれるので、 **証明書をすべて次のストアに配置する**を選択して、**[参照]**をクリック。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/5af912ff-18d7-01f2-7faa-b49d7f759953.png)

**信頼されたルート証明機関**　を選択する。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/3e55c8b9-4fe5-c5c2-9a79-f47d37190ce8.png)

**[次へ]**で次の画面へ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/78a4fcf4-b14a-e492-bdc1-8daca472fb37.png)

**警告が出る**（オレオレ証明書をインポートしようとしているので当然)が**[はい]**を押す
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/582613d0-58aa-7996-f49a-eb09a4c19236.png)

## ブラウザから再度アクセスする

今度は、エラーも無く、ちゃんとhttpsでアクセスできた
（いったん、ブラウザをすべて閉じてからブラウザを再起動しないと証明書が反映されないことがある）

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/405bb3c6-3f31-db77-2802-a1dfb5d5a6e9.png)

## 脆弱性のあるSSLはデフォルト無効になってるか？

YES。SSLまわりの既知の一般的な問題には最新のJettyならデフォルトで対応している。

ある意味あたりまえだが、例えば、POODLEで有名になったSSLv3などはデフォルトで無効化されている。
ほかにも、脆弱性のある"SSL", "SSLv2", "SSLv2Hello"のようなものはデフォルトで無効化されているし、"MD5","SHA","SHA-1"など弱いハッシュアルゴリズムが含まれている暗号も無効となっている。

# Jettyをカスタマイズして、もう少し、こまやかに制御してみる

HTTPSの起動コードは20行で動作させられたので、ひとまず目的は達成できました。
カンタンにつかえるので開発用途で活用できそうです。

最近では、HTTPSは例えばAWSならELB（ロードバランサー）の層で片付けることが多いのですが、Jettyはコードのみで柔軟にサーバーの挙動を制御できるので、参考用にJettyの仕組みと、その他のHTTPS処理コード例も掲載しておきます。

コネクションやハンドラの概念もあるので、以下の順番で説明します

- Step1.Jettyの仕組み（コネクション、ハンドラの概念あたりを中心に）おさらい
- Step2.HTTPとHTTPSを両方ともホストする方法の最小コード
- Step3.HTTPでアクセスがあったら、自動的にHTTPSにリダイレクトする方法の最小コード
- Step4.Servletが共存する場合の最小コード
- Step5.SSLクライアント認証のやり方の最小コード

## ステップ1:Jettyの仕組み

コードだけではイメージしにくいので、まずJettyの仕組みをざっくりと理解する。

Jettyがリクエストを処理する手順は以下のようになっている。

**(1)コネクタ(connector)**：コネクタでTCP通信を解釈する。
　↓
**(2)サーバー(server)**：Jettyサーバー本体。処理用スレッドプールをもっており、コネクタとハンドラを制御する。
　↓
**(3)ハンドラ(handler)**：HTTPリクエストとHTTPレスポンスを処理する。バケツリレー的に複数のハンドラを順次処理していく。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/48d24dc3-f8ab-693d-aa49-c455845a81c6.png)

### コネクタの処理
コネクタ(ServletConnector)はTCP接続を受け取るコンポーネントで、TCP接続があるたびに ConnectionFactoryがコネクションを生成する。SSLを取り扱うときには、SslConnectionFactoryとHttpConnectionFactoryをコネクタ(ServerConnector)にセットする。SSL接続要求があればまず、SslConnectionFactoryがSslConnectionを生成する。SslConnectionによってSSLの暗号化/複合化処理が実行され、処理がおわると、次のプロトコルとしてHTTP/1.1を処理するため、次のConnectionに渡される。具体的にはHttpConnectionFactoryがHttpConnectionを生成する。

コネクタはTCP接続ごとに作るので、HTTP(TCPポート80)とHTTPS(TCPポート443)に対応させたければコネクタを2つ準備することになる。
2つのコネクタを登録するにいは以下のようにする。

```java
server.setConnectors(new Connector[] { httpConnector, httpsConnector });
```

### ハンドラの処理
ハンドラ(handler)はHTTPリクエスト/HTTPレスポンスを処理するコンポーネント。
ハンドラにはRequestHandlerのようにリクエストに応じて静的コンテンツを提供するものや、ServletContextHandlerのようにリクエストに応じてサーブレットを起動するもの、ConstraintSecurityHandlerのようにリクエストに応じて、セキュリティ機能を提供するものなどがある。

ハンドラのメインの処理は以下の#handleメソッドになっている。

```java
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
```
このようにリクエストを処理して、レスポンスを返すというシンプルなインタフェースを実装している。
しかけはシンプルだが、複数のハンドラをチェーンのようにつないで処理させることができる。

いちばん上流にハンドラをセットしたい場合にはServer#setHandlerをつかってハンドラを登録できる。
複数のハンドラをセットしたい場合には HandlerList というクラスをつかって、Server#setHandler(HandlerList)としてあげれば良い。

また、各ハンドラの中に下位のハンドラをセットすることができるため、ハンドラーを直列でつなぎ、さらにハンドラーにハンドラーがネストされているような処理形態を作ることも可能。

実際に、この後の例ではハンドラのつなぎかたを工夫してHTTPS関連処理を実現する例も掲載した。


## ステップ2:HTTPとHTTPSを両方ホストする
いままでは20行で動作するHTTPSサーバーということで、HTTPSだけをみてきたが、もう少し実用的にしていく。ただしなるべく最小コードで。

まず、HTTPとHTTPSの共存から。

```java
public class Step2HttpAndHttpsServer {

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

        final Server server = new Server();
        final SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setKeyStorePath(System.getProperty("user.dir") + "/mykeystore.jks");
        sslContextFactory.setKeyStorePassword("mypassword");

        ServerConnector httpConnector = new ServerConnector(server);
        httpConnector.setPort(80);

        ServerConnector httpsConnector = new ServerConnector(server, sslContextFactory);
        httpsConnector.setPort(443);

        final ResourceHandler resourceHandler = new ResourceHandler();
        resourceHandler.setResourceBase(System.getProperty("user.dir") + "/htdocs");

        server.setConnectors(new Connector[] { httpConnector, httpsConnector });
        server.setHandler(resourceHandler);

        server.start();
        server.join();
    }
}
```

変更点を中心にみていくと、以下のようにHTTP用のコネクタを作成し、
HTTPとHTTPSの両方に対応させる。

```java:HTTPポート(80)をListenするコネクタを作成
ServerConnector httpConnector = new ServerConnector(server);
httpConnector.setPort(80);
```

```java:HTTPとHTTPSの両方のコネクタをサーバーにセットする
server.setConnectors(new Connector[] { httpConnector, httpsConnector });
```

これで実行すると、 http://localhost と https://localhost の双方でアクセスできるようになる


## ステップ3:HTTPで来たアクセスをHTTPSにリダイレクトする

HTTPでアクセスがあったときに、自動的にHTTPS側に切り替えられるようにする


```java
public class Step3AutoRedirect2HttpsServer {

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

        Server server = new Server();
        SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setKeyStorePath(System.getProperty("user.dir") + "/mykeystore.jks");
        sslContextFactory.setKeyStorePassword("mypassword");

        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setSecureScheme("https");
        httpConfig.setSecurePort(443);

        ServerConnector httpConnector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
        httpConnector.setPort(80);

        ServerConnector httpsConnector = new ServerConnector(server, sslContextFactory);
        httpsConnector.setPort(443);

        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
        Constraint constraint = new Constraint();
        constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);

        ConstraintMapping mapping = new ConstraintMapping();
        mapping.setPathSpec("/*");
        mapping.setConstraint(constraint);

        securityHandler.addConstraintMapping(mapping);

        final ResourceHandler resourceHandler = new ResourceHandler();
        resourceHandler.setResourceBase(System.getProperty("user.dir") + "/htdocs");
        securityHandler.setHandler(resourceHandler);

        server.setConnectors(new Connector[] { httpConnector, httpsConnector });
        server.setHandler(securityHandler);

        server.start();
        server.join();
    }
}
```

こちらも変更点、重要な部分をみていく

```java:HTTP用のコネクタを作るときにセキュア設定をしたConfigを指定する
HttpConfiguration httpConfig = new HttpConfiguration();
httpConfig.setSecureScheme("https");
httpConfig.setSecurePort(443);

ServerConnector httpConnector = new ServerConnector(server, new HttpConnectionFactory(httpConfig));
httpConnector.setPort(80);
```

HTTP用のコネクタのConfigなのに**HttpConfiguration#setSecureScheme("https")**となっていたり、**HttpConfiguration#setSecurePort(443)**となっているので、「えっ？」となる。

そこで、それに関して少し説明してみる。

Jettyでは、パス（コンテキストパス）ごとにセキュリティ設定をすることができる。ざっくりいうと、パスごとにアクセスする際のルールを設定できる。あるパスにアクセスが来た場合には通信を保護したい、という場合、サーバーとして挙動を設定することができる。

たとえば、 "/private" というパスにHTTPでアクセスされたら、403を返すとか、 "/confidential"というパスにHTTPでアクセスされた場合は HTTPS に転送する、などである。
そういった挙動による制御をデータ制約(Data Constraint)という考え方で行う。

今回は HTTPS に転送する挙動になるように実装したが、これは **CONFIDENTIAL**という名の制約となる。CONFIDENTIALというのはJetty実装としてはセキュアな手段への転送を意味している。
（ほかの制約の例として**FORBIDDEN**という名の制約があるが、これをセットすると403返す挙動となる）

もとの話にもどると、``setSecureScheme("https")``、``setSecurePort(443)``というのは、**CONFIDENTIAL**という種類の制約がセキュリティとして設定されている状態の場合に、「HTTP用コネクタにアクセスがあった場合のセキュアな手段として"https"というschemeで、portは443です」というのをHttpConfigurationであらかじめ定義しているということになる。

次は前の同じようにHTTPS用コネクタを生成する

```java:HTTPS用のコネクタを生成する
        ServerConnector httpsConnector = new ServerConnector(server, sslContextFactory);
        httpsConnector.setPort(443);
```

```java:セキュリティ設定をする
ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
Constraint constraint = new Constraint();
constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);

ConstraintMapping mapping = new ConstraintMapping();
mapping.setPathSpec("/*");
mapping.setConstraint(constraint);

securityHandler.addConstraintMapping(mapping);
```

ConstraintSecurityHandlerクラスはセキュリティ系設定をするハンドラーで、設定したセキュリティ系の挙動を有効にしたい場合はこのハンドラーをServerにセットする。

次にでてくるConstraint クラスはそうしたセキュリティを実現するための各種制約をセットするためのもので、ここでは``constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);``で**CONFIDENTIAL**という制約をセットしている。
ConstraintMapping クラスは、実際にブラウザからアクセスされるパスと制約のマッピングをしている

```java
mapping.setPathSpec("/*");
mapping.setConstraint(constraint);
securityHandler.addConstraintMapping(mapping);
```

この設定によって、パス "**/＊**"つまり、アクセスされるすべてのパスに対して **CONFIDENTIAL**という制約が有効になる。
``securityHandler.addConstraintMapping(mapping);``でConstraintSecurityHandlerに制約とパスのマッピングを追加する。


```java:静的コンテンツのハンドラを生成する
final ResourceHandler resourceHandler = new ResourceHandler();
resourceHandler.setResourceBase(System.getProperty("user.dir") + "/htdocs");
securityHandler.setHandler(resourceHandler);
```

``securityHandler.setHandler(resourceHandler);``に注目。
静的コンテンツにアクセスするときに、ConstraintSecurityHandlerのハンドラーをまず経由させるために、ConstraintSecurityHandlerに対してsetHandlerする。


```java:serverに対してsecurityHandlerをセットする
server.setHandler(securityHandler);
```
最後に、serverに対してConstraintSecurityHandlerをセットする。
これで、アクセスがあった場合に、まずConstraintSecurityHandlerで評価され、ConstraintSecurityHandlerの処理プロセスの中でResourceHandlerが処理されるのでさきほどセットした一連のセキュリティ設定が機能する。

## ステップ4.Servletの導入

```java:コード全体

public class Step4HttpsWithServletServer {

    public static void main(String[] args) throws Exception {
        final Server server = new Server();
        final SslContextFactory sslContextFactory = new SslContextFactory();
        sslContextFactory.setKeyStorePath(System.getProperty("user.dir") + "/mykeystore.jks");
        sslContextFactory.setKeyStorePassword("mypassword");

        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setSecureScheme("https");
        httpConfig.setSecurePort(443);
        ServerConnector httpConnector = new ServerConnector(server,
                new HttpConnectionFactory(httpConfig));
        httpConnector.setPort(80);

        ServerConnector httpsConnector = new ServerConnector(server, sslContextFactory);
        httpsConnector.setPort(443);

        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
        Constraint constraint = new Constraint();
        constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);

        ConstraintMapping mapping = new ConstraintMapping();
        mapping.setPathSpec("/*");
        mapping.setConstraint(constraint);

        securityHandler.addConstraintMapping(mapping);

        final ResourceHandler resourceHandler = new ResourceHandler();
        resourceHandler.setResourceBase(System.getProperty("user.dir") + "/htdocs");
        server.setConnectors(new Connector[] { httpConnector, httpsConnector });
        ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
        servletContextHandler.addServlet(ExampleServlet.class, "/test");

        HandlerList handlerList = new HandlerList();
        handlerList.addHandler(resourceHandler);
        handlerList.addHandler(servletContextHandler);

        securityHandler.setHandler(handlerList);

        server.setHandler(securityHandler);

        server.start();
        server.join();
    }

    @SuppressWarnings("serial")
    public static class ExampleServlet extends HttpServlet {

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.setContentType("text/plain; charset=UTF-8");

            final PrintWriter out = resp.getWriter();

            out.println("Hello Servlet");
            out.close();

        }
    }
}
```

すこし長くなってきたが、重要なところだけみていく。

```java
ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
servletContextHandler.addServlet(ExampleServlet.class, "/test");

HandlerList handlerList = new HandlerList();
handlerList.addHandler(resourceHandler);
handlerList.addHandler(servletContextHandler);

securityHandler.setHandler(handlerList);
```

Servletを追加したいので、ServletContextHandlerに登場してもらった。
ここでは /test というパスにサーブレットをアサインしている。

静的コンテンツとサーブレットと両方ともセキュリティが効くようにしておきたいので、securityHandlerに両方ともセットするために、HandlerListをつくって、そちらにResourcehandler と ServletContextHandler の両方のハンドラを追加。

これで、 http://localhost/test にアクセスすると、 https://localhost/test に転送され、意図どおり動作する。

## ステップ5.クライアント証明書のハンドリング

ここでは、クライアント証明書の認証に対応させてみる。

クライアント証明書認証とは、PCなどデバイスに証明書（クライアント証明書）をインストールしておいて、サーバーに証明書を添えてアクセスすることで、サーバー側で証明書を検証し利用者を認証する仕組み。一般にユーザー名＋パスワードの認証よりも安全性が高いといわれている。

いわゆるSSL/TLS対応したサーバーとクライアント証明書をもったクライアントの双方で認証することを相互認証(mutual authentication)と呼ぶ。

以下、クライアント証明書をハンドリングできるコードを掲載する。
（認証処理まで入れていないが、コードをみれば、どうすれば良いかわかるはず）

```java:クライアント証明書認証をハンドリングできる
public class Step5ClientCertificateAuthServer {

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

        final Server server = new Server();
        final SslContextFactory sslContextFactory = new SslContextFactory();

        sslContextFactory.setKeyStorePath(System.getProperty("user.dir") + "/mykeystore.jks");
        sslContextFactory.setKeyStorePassword("mypassword");
        sslContextFactory.setWantClientAuth(true);

        HttpConfiguration httpConfig = new HttpConfiguration();
        httpConfig.setSecureScheme("https");
        httpConfig.setSecurePort(443);
        ServerConnector httpConnector = new ServerConnector(server,
                new HttpConnectionFactory(httpConfig));
        httpConnector.setPort(80);

        HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
        httpsConfig.addCustomizer(new SecureRequestCustomizer());

        ServerConnector httpsConnector = new ServerConnector(server,
                new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.asString()),
                new HttpConnectionFactory(httpsConfig));
        httpsConnector.setPort(443);

        ConstraintSecurityHandler securityHandler = new ConstraintSecurityHandler();
        Constraint constraint = new Constraint();
        constraint.setDataConstraint(Constraint.DC_CONFIDENTIAL);

        ConstraintMapping mapping = new ConstraintMapping();
        mapping.setPathSpec("/*");
        mapping.setConstraint(constraint);

        securityHandler.addConstraintMapping(mapping);

        final ResourceHandler resourceHandler = new ResourceHandler();
        resourceHandler.setResourceBase(System.getProperty("user.dir") + "/htdocs");

        server.setConnectors(new Connector[] { httpConnector, httpsConnector });
        ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
        servletContextHandler.addServlet(ExampleServlet.class, "/test");

        HandlerList handlerList = new HandlerList();
        handlerList.addHandler(resourceHandler);
        handlerList.addHandler(servletContextHandler);

        securityHandler.setHandler(handlerList);

        server.setHandler(securityHandler);

        server.start();
        server.join();
    }

    @SuppressWarnings("serial")
    public static class ExampleServlet extends HttpServlet {

        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

            final X509Certificate[] clientCerts = (X509Certificate[]) req.getAttribute("javax.servlet.request.X509Certificate");
            final String cipherSuite = (String) req.getAttribute("javax.servlet.request.cipher_suite");
            final Integer keySize = (Integer) req.getAttribute("javax.servlet.request.key_size");
            final String idStr = (String) req.getAttribute("javax.servlet.request.ssl_session_id");

            final StringBuilder sb = new StringBuilder();

            if (clientCerts != null) {
                for (int i = 0; i < clientCerts.length; i++) {
                    final X509Certificate cert = clientCerts[i];
                    sb.append("cert[" + i + "] subjectDN=" + cert.getSubjectDN()).append("\n");
                }
            }
            sb.append("cipherSuite=" + cipherSuite).append("\n");
            sb.append("keySize=" + keySize).append("\n");
            sb.append("idStr=" + idStr).append("\n");

            resp.setContentType("text/plain; charset=UTF-8");

            final PrintWriter out = resp.getWriter();

            out.println("Hello Servlet");
            out.println("Client Certificate Info");
            out.println(sb.toString());
            out.close();

        }
    }
}
```


Jettyで、クライアント証明書認証を有効にするには以下のようにする。

```java:クライアント認証を有効にする
   sslContextFactory.setWantClientAuth(true);
```

これで、ブラウザからJettyにリクエストがあったときに、クライアント証明書を要求するようになる。

認証のやりかたは色々あるが、クライアント証明書が取り出せないとはじまらない。
以下のコードでその設定をする

```java:HTTPS用のConfigを生成する。
HttpConfiguration httpsConfig = new HttpConfiguration(httpConfig);
httpsConfig.addCustomizer(new SecureRequestCustomizer());
```

**SecureRequestCustomizer**をセットしておくと、クライアント証明書をJettyのSSL処理エンジン(SslEngine)から取り出してRequestのAttributeに保存しておいてくれる。これをservlet等からHttpRequest#getAttributeで取り出しが可能になる。

具体的には以下のようにする

```java:クライアント証明書情報の取得
final X509Certificate[] clientCerts = (X509Certificate[])req.getAttribute("javax.servlet.request.X509Certificate");
final String cipherSuite = (String) req.getAttribute("javax.servlet.request.cipher_suite");
final Integer keySize = (Integer) req.getAttribute("javax.servlet.request.key_size");
final String idStr = (String) req.getAttribute("javax.servlet.request.ssl_session_id");
```

### Keytoolを使ってクライアント証明書を作成する

上の例をためすために再び keytool をつかってクライアント証明書（オレオレ）をつくってみる。
今回は、CA(認証局)を使わないのでサーバー側のKeystoreにクライアント証明書の公開鍵を登録することでクライアント認証が成立する環境をつくる。

**(1)クライアント証明書を含むキーストアの作成**

```shell
keytool -genkey -dname "cn=user, ou=User div., o=User Inc., l=Minato-ku, st=Tokyo, c=JP" -alias user -keystore myclientkeystore.jks -storepass mypassword -keypass mypassword -keyalg RSA -keysize 2048 -sigalg SHA256withRSA -validity 3650
```

クライアント証明書用のキーストア **myclientkeystore.jks**ができた。

**(2)クライアント証明書の公開鍵をサーバー側のキーストアに取り込む**

クライアント証明書の発行元をサーバーが信頼していないと、クライアント証明書認証ができないので、Jettyサーバー側のキーストア(**mykeystore.jks**)に、いまつくったクライアント証明書の公開鍵を取り込む。

まず、クライアント証明書用のキーストアから公開鍵をエクスポートする。

```shell:クライアント証明書用のキーストアから公開鍵を含む証明書をエクスポートする
keytool -export -alias user -storepass mypassword -file myclient.crt -keystore myclientkeystore.jks
```

これでクライアントの公開鍵を含む証明書 **myclient.crt** ができた。

開いてみるとこんな感じ。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/79723562-5068-4475-cb64-7a37b9f899cb.png)

こいつを、サーバー側のキーストアに取り込むことでサーバーにこのクライアント証明書（の発行元）を信頼させる状況をつくる。

```shell:クライアント証明書の公開鍵を、サーバー側のキーストアにインポート
keytool -importcert -alias user -file myclient.crt -keystore mykeystore.jks -storepass mypassword

所有者: CN=user, OU=User div., O=User Inc., L=Minato-ku, ST=Tokyo, C=JP
発行者: CN=user, OU=User div., O=User Inc., L=Minato-ku, ST=Tokyo, C=JP
シリアル番号: 5dd8a186
有効期間の開始日: Sun Dec 09 17:44:03 JST 2018終了日: Wed Dec 06 17:44:03 JST 2028
証明書のフィンガプリント:
         SHA1: E1:2F:E0:23:A0:0A:50:18:22:26:28:28:E9:95:E9:3C:B3:87:BC:4D
         SHA256: EC:2A:54:61:57:0C:19:70:4D:5E:29:47:FF:DF:C3:27:5E:03:A9:5B:E9:39:4C:A0:83:B3:D6:03
:72:76:04:12
署名アルゴリズム名: SHA256withRSA
サブジェクト公開鍵アルゴリズム: 2048ビットRSA鍵
バージョン: 3

拡張:

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: AF 47 BB 4E 18 CF 79 13   6F EE C4 B6 29 FF 3C 91  .G.N..y.o...).<.
0010: BF BC 4C 10                                        ..L.
]
]

この証明書を信頼しますか。 [いいえ]:  yes
証明書がキーストアに追加されました
```

信頼するか聞かれるので yes とこたえれば、インポート成功。

以下のようにすれば、サーバー側のキーストアの中身が見られる。ちゃんと2つ登録されているのがわかる。

```shell:サーバー側のキーストアの中身をみてみる
keytool -list -v -storepass mypassword -keystore  mykeystore.jks

キーストアのタイプ: PKCS12
キーストア・プロバイダ: SUN

キーストアには2エントリが含まれます

別名: jetty
作成日: 2018/12/08
エントリ・タイプ: PrivateKeyEntry
証明書チェーンの長さ: 1
証明書[1]:
所有者: CN=localhost, OU=Example div., O=Example Inc., L=Minato-ku, ST=Tokyo, C=JP
発行者: CN=localhost, OU=Example div., O=Example Inc., L=Minato-ku, ST=Tokyo, C=JP
シリアル番号: 26088242
有効期間の開始日: Sat Dec 08 12:53:11 JST 2018終了日: Tue Dec 05 12:53:11 JST 2028
証明書のフィンガプリント:
         SHA1: 80:F1:F4:BC:19:64:55:57:C7:4B:B8:7C:CB:B0:CC:B2:BC:D3:E0:45
         SHA256: 23:90:61:3F:2D:61:B7:42:D2:C3:02:E6:58:09:34:D2:50:C0:28:DF:BE:E8:98:51:0B:14:21:8F
:76:12:A0:DD
署名アルゴリズム名: SHA256withRSA
サブジェクト公開鍵アルゴリズム: 2048ビットRSA鍵
バージョン: 3

略

*******************************************
*******************************************

別名: user
作成日: 2018/12/09
エントリ・タイプ: trustedCertEntry

所有者: CN=user, OU=User div., O=User Inc., L=Minato-ku, ST=Tokyo, C=JP
発行者: CN=user, OU=User div., O=User Inc., L=Minato-ku, ST=Tokyo, C=JP
シリアル番号: 5dd8a186
有効期間の開始日: Sun Dec 09 17:44:03 JST 2018終了日: Wed Dec 06 17:44:03 JST 2028
証明書のフィンガプリント:
         SHA1: E1:2F:E0:23:A0:0A:50:18:22:26:28:28:E9:95:E9:3C:B3:87:BC:4D
         SHA256: EC:2A:54:61:57:0C:19:70:4D:5E:29:47:FF:DF:C3:27:5E:03:A9:5B:E9:39:4C:A0:83:B3:D6:03
:72:76:04:12
署名アルゴリズム名: SHA256withRSA
サブジェクト公開鍵アルゴリズム: 2048ビットRSA鍵
バージョン: 3

略

*******************************************
*******************************************
```

これでクライアント側の公開鍵がサーバーにキーストアに入ったので、サーバーはクライアント証明書（の発行元）が信頼できる状態になった。

**(3)ブラウザにクライアント証明書を仕込む**

ブラウザにクライアント証明書を仕込む、つまりブラウザにインポートするために、今度は**秘密鍵**がはいった証明書としてキーストアから取り出す。形式はPKCS12形式(*.p12)とする。

```shell:PKCS12(p12)形式でクライアント証明書をエクスポート
keytool -importkeystore -srckeystore myclientkeystore.jks -srcstorepass mypassword -destkeystore myclientcert.p12 -deststoretype PKCS12 -deststorepass  mypassword

キーストアmyclientkeystore.jksをmyclientcert.p12にインポートしています...
別名userのエントリのインポートに成功しました。
インポート・コマンドが完了しました: 1件のエントリのインポートが成功しました。0件のエントリのインポー
トが失敗したか取り消されました
```
これでクライアント証明書がP12ファイル(**myclientcert.p12**)として取り出せた 

### クライアント証明書をインポートする

さて、いま作った myclientcert.p12 をブラウザから使えるようにインポートする。

myclientcert.p12をダブルクリックすると、証明書インポートウィザードが起動する

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/37eefd4b-4f16-80a9-08a5-ed9d46e9bcfa.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/071418d6-1e65-771e-3f06-4a4ea36d4807.png)

パスワードをきかれるので、さきほどセットして mypassword をいれる。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/a5cc5d15-17a1-18d6-4d74-f396bef0f635.png)

**証明書をすべて次のストアに配置する**を選択して、参照をクリック
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/33745061-c3a9-73c2-e826-d175461484b2.png)

**個人**を選択する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/0cb48c76-0518-6375-d9c3-9738cb422a27.png)

あとは画面に従って、**完了**をクリックして、クライアント証明書の取り込み終了

### ブラウザからアクセスしてみる

クライアント証明書のお膳立てがなかなか手間だったが、いよいよブラウザからアクセスしてみる。

Step5ClientCertificateAuthServer.java でサーバーを起動しておく。

https://localhost/test にアクセスすると、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/6b0dac91-cb4a-ef27-785a-c9f6f3d44da2.png)

クライアント認証の証明書選択画面に、さきほど登録したオレオレ・クライアント証明書が表示されたので、ここで**[OK]**を押すと、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/71df9e6a-11e1-4191-a099-8dd96284a2ab.png)

無事、クライアント証明書情報をサーブレット側で取得できた。
（クライアント証明書情報が取得できたので、あとはHandlerでやるなり、サーブレットでやるなり認証処理をすれば良い）


# オレオレ証明書の後始末

いくつか作ってきたが、開発用途でつくったオレオレ証明書は目的がおわったら削除したほうがいいのでそのやり方。

コマンドライン等から、証明書のユーティリティを起動する

- Windows7の場合

```shell
certmgr.msc
```

- Windows 8、Windows 10の場合

```shell
certlm.msc
```

すると、以下のように**信頼されたルート証明機関**を開くと、
発行先が **localhost** となっている証明書がみつかる。これがさっきインポートしたオレオレ証明書となる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/52a563c3-d0bf-e8a2-56b3-adaf2481a2ec.png)

右クリックメニューから**削除**を選択する

![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/a990fe4f-efa6-32f2-5e41-c676d2ab2f7d.png)

削除するときにもこんな警告ダイアログがでるが、オレオレ証明書なので、**はい**で削除完了
![image.png](https://qiita-image-store.s3.amazonaws.com/0/170905/db9431d6-cbc7-1751-fbf7-0810f609ff13.png)

# まとめ

- 今回は、最新のJetty9で最小コードでHTTPSサーバーを起動する方法を紹介しました。
- さらに、HTTPやHTTPSの共存や、セキュリティ設定についてもハンズ音しながらみてきました。
- ソースコードは https://github.com/riversun/jetty9-minimal-ssl にあります
- JettyはGoogleが採用するなど実績もあり、かつ軽量で使い勝手が良いコンテナの1つなので引き続き活用していこうとおもいます
- コネクタやハンドラの構成などさらにJettyのアーキテクチャの深い部分知りたい場合はこちらが参考になります。https://www.eclipse.org/jetty/documentation/9.4.x/architecture.html