本記事は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