JettyでmTLSを使う

本記事は[FOLIO Advent Calendar 2022](https://adventar.org/calendars/7813)の8日目です

# mTLSとは?

mTLS(mutual tls,相互TLS)とは、サーバーだけでなくクライアントについても証明書による検証を行う通信方式。
一般的なWebサイトではクライアントの正当性を証明する必要性は少ないが、エンタープライズレベルのセキュリティーが必要とされるケースなどで使用される。とくにゼロトラストなどのバズワードと一緒に使うと効果が大きい。

# 使用するツール

* Java
* OpenSSL
* Jetty
* Google HTTP Client

# 証明書の作成

サーバー・クライアント双方にオレオレ証明書を作成する。
Javaを使う場合一般的にはkeytoolで直接証明書を作成することが多いが、筆者はkeytoolが嫌いなためopensslを使用する(そのほうが応用も効くと思う)。

## CA証明書の作成

```
openssl req \
    -new \
    -x509 \
    -newkey rsa:4096 \
    -days $DAYS \
    -subj '/CN=localhost ca' \
    -keyout "$NAME"ca.key \
    -out "$NAME"ca.crt \
    -passout pass:$PASS
```

* `-new` 新規作成
* `-x509` 証明書を作成する
* `-newkey rsa:4096` RSA4096bitの秘密鍵
  * ナウでヤングなエンジニアは`-newkey ec -pkeyopt ec_paramgen_curve:secp384r1`を使おう!
* `-subj` subject - なんでもいい。
* `-keyout` 秘密鍵を作成して出力する(`-key`で使用する鍵を指定出来る)
* `-passout pass:hoge` 実際のパスワードとして`hoge`を指定

## CSRの作成

```
openssl req \
    -new \
    -newkey rsa:4096 \
    -subj '/CN=localhost' \
    -keyout $NAME.key \
    -out $NAME.csr \
    -passout pass:$PASS
```

* CSR(certificate signing request)とは、CAに対しサーバー証明書の署名を要求するもの
* `-x509`がないのでCSRが作成される
* `-subj` subject - サーバーのドメイン。今回は `localhost`

## 証明書作成

```
openssl x509 \
    -req \
    -in $NAME.csr \
    -CA "$NAME"ca.crt \
    -CAkey "$NAME"ca.key \
    -CAcreateserial \
    -days $DAYS \
    -passin pass:$PASS \
    -out $NAME.crt
```

* `-req` CSRに対する処理であることを示す
* `-in` 入力ファイル。`-req`なのでCSRを指定
* `-CA` CA証明書
* `-CAkey` CA秘密鍵
* `-CAcreateserial` 証明書のシリアルナンバーを新規作成する

## CA証明書のJava keystore形式への変換

```
keytool -import \
    -trustcacerts \
    -noprompt \
    -file "$NAME"ca.crt \
    -keystore "$NAME"ca.keystore \
    -storepass $PASS
```

## 証明書のJava keystore形式への変換

```
openssl pkcs12 \
    -export \
    -in $NAME.crt \
    -inkey $NAME.key \
    -passin pass:$PASS \
    -out $NAME.p12 \
    -passout pass:$PASS \
    -CAfile "$NAME"ca.crt
keytool -importkeystore \
    -srckeystore $NAME.p12 \
    -srcstoretype PKCS12 \
    -srcstorepass $PASS \
    -destkeystore $NAME.keystore \
    -deststorepass $PASS
```

* 直接秘密鍵のPEMを取り込むことは出来ないらしい
    * まずPEMをPKCS12形式に変換し、その後JKSを作成する
    * 有識者による啓蒙を求ム

# サーバー

```kotlin:server.kt
private const val trustStore = "clientca.keystore"
private const val trustStorePass = "hogehoge"
private const val keyStore = "server.keystore"
private const val keyStorePass = "hogehoge"

fun main() {

    val server = Server()

    val sslContextFactory = SslContextFactory.Server().also {
        it.trustStore = KeyStore.getInstance(Paths.get(trustStore).toFile(), trustStorePass.toCharArray())
        it.setTrustStorePassword(trustStorePass)
        it.keyStore = KeyStore.getInstance(Paths.get(keyStore).toFile(), keyStorePass.toCharArray())
        it.keyStorePassword = keyStorePass
        // クライアント証明書を要求する
        it.needClientAuth = true
    }

    val httpConnectionFactory = HttpConnectionFactory(HttpConfiguration().also {
        it.addCustomizer(SecureRequestCustomizer())
    })

    val connector = ServerConnector(server, sslContextFactory, httpConnectionFactory).also {
        it.port = 8080
    }

    server.addConnector(connector)
    server.handler = object : AbstractHandler() {
        override fun handle(
            target: String,
            baseRequest: Request,
            request: HttpServletRequest,
            response: HttpServletResponse,
        ) {
            response.writer.println("hello, mtls")

            // SecureRequestCustomizerをセットしていると取得できる
            @Suppress("UNCHECKED_CAST")
            val cert = request.getAttribute(JAKARTA_SERVLET_REQUEST_X_509_CERTIFICATE) as Array<X509Certificate>
            response.writer.println("Your cert algorithm: ${cert.first().sigAlgName}")

            response.writer.close()
        }
    }

    server.start()
}
```

# クライアント

```kotlin:client.kt
private const val trustStore = "serverca.keystore"
private const val trustStorePass = "hogehoge"
private const val keyStore = "client.keystore"
private const val keyStorePass = "hogehoge"

fun main() {

    val transport = NetHttpTransport.Builder()
        .trustCertificates(
            KeyStore.getInstance(Paths.get(trustStore).toFile(), trustStorePass.toCharArray()),
            KeyStore.getInstance(Paths.get(keyStore).toFile(), keyStorePass.toCharArray()),
            keyStorePass
        )
        .build()

    val response = transport.createRequestFactory()
        .buildGetRequest(GenericUrl("https://localhost:8080"))
        .execute()

    val responseBody = response.content.readAllBytes().toString(StandardCharsets.UTF_8)
    println(responseBody)
}
```

`.trustCertificates()`のキーストアの引数を省略すると、証明書の検証が失敗するので、エラーになる。

```
Exception in thread "main" javax.net.ssl.SSLHandshakeException: Received fatal alert: bad_certificate
```

# 後記

* [リポジトリ](https://github.com/minebreaker/teach-yourself-mtls)
* [参考文献](https://github.com/minebreaker/teach-yourself-mtls#%E5%8F%82%E8%80%83%E6%96%87%E7%8C%AE)