LoginSignup
0

More than 1 year has passed since last update.

JettyでmTLSを使う

Last updated at Posted at 2022-12-08

本記事はFOLIO Advent Calendar 2022の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を作成する
    • 有識者による啓蒙を求ム

サーバー

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()
}

クライアント

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

後記

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0