为什么我的服务器不在 Play 2.6.x 中请求客户端证书?
Why isn't my server requesting a client cert in Play 2.6.x?
我正在尝试按照 play-tls-example 启用客户端身份验证。由于这只是一个实验,我正在生成自签名证书。
我有以下 SSL 引擎提供商:
package https
import java.nio.file.{FileSystems, Files}
import java.security.KeyStore
import play.core.ApplicationProvider
import play.server.api._
import javax.net.ssl._
import play.api.Configuration
class CustomSSLEngineProvider(appProvider: ApplicationProvider) extends SSLEngineProvider {
private val config: Configuration = appProvider.current.get.configuration
private val certificateDirectory: String = config.get[String]("certificateDirectory")
private def readTrustInputStream(): java.io.InputStream = {
val keyPath = FileSystems.getDefault.getPath(certificateDirectory, "clientca.jks")
Files.newInputStream(keyPath)
}
private def readPassword(): Array[Char] = {
val passwordPath = FileSystems.getDefault.getPath(certificateDirectory, "password")
Files.readAllLines(passwordPath).get(0).toCharArray
}
private def readTrustManagers(): Array[TrustManager] = {
val password = readPassword()
val trustInputStream = readTrustInputStream()
try {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
keyStore.load(trustInputStream, password)
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
tmf.init(keyStore)
tmf.getTrustManagers
} finally {
trustInputStream.close()
}
}
private def readKeyInputStream(): java.io.InputStream = {
val keyPath = FileSystems.getDefault.getPath(certificateDirectory, "localhost.jks")
Files.newInputStream(keyPath)
}
private def readKeyManagers(): Array[KeyManager] = {
val password = readPassword()
val keyInputStream = readKeyInputStream()
try {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
keyStore.load(keyInputStream, password)
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
kmf.init(keyStore, password)
kmf.getKeyManagers
} finally {
keyInputStream.close()
}
}
private def createSSLContext(applicationProvider: ApplicationProvider): SSLContext = {
val keyManagers = readKeyManagers()
val trustManagers = readTrustManagers()
// Configure the SSL context to use TLS
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagers, trustManagers, null)
sslContext
}
override def createSSLEngine(): SSLEngine = {
val sslContext = createSSLContext(appProvider)
// Start off with a clone of the default SSL parameters...
val sslParameters = sslContext.getDefaultSSLParameters
// Tells the server to ignore client's cipher suite preference.
// http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#cipher_suite_preference
sslParameters.setUseCipherSuitesOrder(true)
// http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLParameters
val needClientAuth = true
sslParameters.setNeedClientAuth(needClientAuth)
// Clone and modify the default SSL parameters.
val engine = sslContext.createSSLEngine
engine.setSSLParameters(sslParameters)
println(s"Need client auth: ${sslParameters.getNeedClientAuth}")
engine
}
}
并且 clientca.jks
是根据以下脚本配置的:
#!/bin/bash
export PW=`cat password`
# Create a self signed certificate & private key to create a root certificate authority.
keytool -genkeypair -v \
-alias clientca \
-keystore client.jks \
-dname "CN=clientca, OU=foo, O=bar, L=baz, ST=Ohio, C=US" \
-keypass:env PW \
-storepass:env PW \
-keyalg EC \
-keysize 256 \
-ext KeyUsage:critical="keyCertSign" \
-ext BasicConstraints:critical="ca:true" \
-validity 365
# Create another key pair that will act as the client. We want this signed by the client CA.
keytool -genkeypair -v \
-alias client \
-keystore client.jks \
-dname "CN=client, OU=foo, O=bar, L=baz, ST=Ohio, C=US" \
-keypass:env PW \
-storepass:env PW \
-keyalg EC \
-keysize 256 \
# Create a certificate signing request from the client certificate.
keytool -certreq -v \
-alias client \
-keypass:env PW \
-storepass:env PW \
-keystore client.jks \
-file client.csr
# Make clientCA create a certificate chain saying that client is signed by clientCA.
keytool -gencert -v \
-alias clientca \
-keypass:env PW \
-storepass:env PW \
-keystore client.jks \
-infile client.csr \
-outfile client.crt \
-ext EKU="clientAuth" \
-rfc
# Export the client-ca certificate from the keystore. This goes to nginx under "ssl_client_certificate"
# and is presented in the CertificateRequest.
keytool -export -v \
-alias clientca \
-file clientca.crt \
-storepass:env PW \
-keystore client.jks \
-rfc
# Import the signed client certificate back into client.jks. This is important, as JSSE won't send a client
# certificate if it can't find one signed by the client-ca presented in the CertificateRequest.
keytool -import -v \
-alias client \
-file client.crt \
-keystore client.jks \
-storetype JKS \
-storepass:env PW
# Export the client CA to pkcs12, so it's safe.
keytool -importkeystore -v \
-srcalias clientca \
-srckeystore client.jks \
-srcstorepass:env PW \
-destkeystore client.p12 \
-deststorepass:env PW \
-deststoretype PKCS12
# Import the client CA's public certificate into a JKS store for Play Server to read (we don't use
# the PKCS12 because it's got the CA private key and we don't want that.
keytool -import -v \
-alias clientca \
-file clientca.crt \
-keystore clientca.jks \
-storepass:env PW << EOF
yes
EOF
# List out the contents of client.jks just to confirm it.
keytool -list -v \
-keystore client.jks \
-storepass:env PW
而我的 localhost.jks
是由以下脚本生成的:
#!/bin/bash
export PW=`cat password`
# Create a server certificate, tied to localhost
keytool -genkeypair -v \
-alias localhost \
-dname "CN=localhost, OU=foo, O=bar, L=baz, ST=Ohio, C=US" \
-keystore localhost.jks \
-keypass:env PW \
-storepass:env PW \
-keyalg EC \
-keysize 256 \
-validity 385
# Create a certificate signing request for localhost
keytool -certreq -v \
-alias localhost \
-keypass:env PW \
-storepass:env PW \
-keystore localhost.jks \
-file localhost.csr
# Tell ca to sign the localhost certificate.
# Technically, digitalSignature for DHE or ECDHE, keyEncipherment for RSA
keytool -gencert -v \
-alias ca \
-keypass:env PW \
-storepass:env PW \
-keystore ca.jks \
-infile localhost.csr \
-outfile localhost.crt \
-ext KeyUsage:critical="digitalSignature,keyEncipherment" \
-ext EKU="serverAuth" \
-ext SAN="DNS:localhost" \
-rfc
# Tell localhost.jks it can trust ca as a signer.
keytool -import -v \
-alias ca \
-file ca.crt \
-keystore localhost.jks \
-storetype JKS \
-storepass:env PW << EOF
yes
EOF
# Import the signed certificate back into localhost.jks
keytool -import -v \
-alias localhost \
-file localhost.crt \
-keystore localhost.jks \
-storetype JKS \
-storepass:env PW
当我 运行 在生产中使用以下相关配置时:
http.port=disabled
https.port = 9443
play.server.https.engineProvider=https.CustomSSLEngineProvider
certificateDirectory=/path/to/certs/
play.server.https.keyStore.path=/path/to/certs/localhost.jks
play.server.https.keyStore.password=`cat /path/to/certs/password` # actual string
jdk.tls.rejectClientInitiatedRenegotiation=true
我发现当我发出不安全的 curl 时我的服务器不请求客户端证书:curl -k https://localhost:9443
:
13:35:13.173 [application-akka.actor.default-dispatcher-4] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
13:35:13.182 [application-akka.actor.default-dispatcher-4] DEBUG akka.event.EventStream - logger log1-Slf4jLogger started
13:35:13.185 [application-akka.actor.default-dispatcher-4] DEBUG akka.event.EventStream - Default Loggers started
13:35:13.213 [main] DEBUG play.api.libs.concurrent.ActorSystemProvider - Starting application default Akka system: application
13:35:13.368 [main] DEBUG controllers.AssetsConfiguration - Using the following cache configuration for assets:
enableCaching = true
enableCacheControl = true
defaultCacheControl = public, max-age=3600
aggressiveCacheControl = public, max-age=31536000, immutable
configuredCacheControl:
13:35:13.414 [main] INFO play.api.Play - Application started (Prod)
13:35:13.678 [application-akka.actor.default-dispatcher-2] DEBUG com.typesafe.sslconfig.akka.AkkaSSLConfig - Initializing AkkaSSLConfig extension...
13:35:13.680 [application-akka.actor.default-dispatcher-2] DEBUG com.typesafe.sslconfig.akka.AkkaSSLConfig - buildHostnameVerifier: created hostname verifier: com.typesafe.sslconfig.ssl.DefaultHostnameVerifier@4acf72b6
13:35:14.046 [application-akka.actor.default-dispatcher-3] DEBUG akka.io.TcpListener - Successfully bound to /0:0:0:0:0:0:0:0:9443
13:35:14.054 [main] INFO play.core.server.AkkaHttpServer - Listening for HTTPS on /0:0:0:0:0:0:0:0:9443
13:35:20.420 [application-akka.actor.default-dispatcher-3] DEBUG akka.io.TcpListener - New connection accepted
Need client auth: true
13:35:20.597 [application-akka.actor.default-dispatcher-3] DEBUG akka.stream.impl.io.TLSActor - closing output
尽管我的服务器需要 客户端身份验证,但从未提示客户端提供证书并成功启动了不安全的连接。为什么会这样?
有一个 open issue 与此相关。
为了使这项工作正常进行,需要在尝试进行客户端身份验证的项目中启用 sbt PlayAkkaHttp2Support
和 java agent will need to be updated 以规避 JDK 161 问题。
我正在尝试按照 play-tls-example 启用客户端身份验证。由于这只是一个实验,我正在生成自签名证书。
我有以下 SSL 引擎提供商:
package https
import java.nio.file.{FileSystems, Files}
import java.security.KeyStore
import play.core.ApplicationProvider
import play.server.api._
import javax.net.ssl._
import play.api.Configuration
class CustomSSLEngineProvider(appProvider: ApplicationProvider) extends SSLEngineProvider {
private val config: Configuration = appProvider.current.get.configuration
private val certificateDirectory: String = config.get[String]("certificateDirectory")
private def readTrustInputStream(): java.io.InputStream = {
val keyPath = FileSystems.getDefault.getPath(certificateDirectory, "clientca.jks")
Files.newInputStream(keyPath)
}
private def readPassword(): Array[Char] = {
val passwordPath = FileSystems.getDefault.getPath(certificateDirectory, "password")
Files.readAllLines(passwordPath).get(0).toCharArray
}
private def readTrustManagers(): Array[TrustManager] = {
val password = readPassword()
val trustInputStream = readTrustInputStream()
try {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
keyStore.load(trustInputStream, password)
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
tmf.init(keyStore)
tmf.getTrustManagers
} finally {
trustInputStream.close()
}
}
private def readKeyInputStream(): java.io.InputStream = {
val keyPath = FileSystems.getDefault.getPath(certificateDirectory, "localhost.jks")
Files.newInputStream(keyPath)
}
private def readKeyManagers(): Array[KeyManager] = {
val password = readPassword()
val keyInputStream = readKeyInputStream()
try {
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType)
keyStore.load(keyInputStream, password)
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm)
kmf.init(keyStore, password)
kmf.getKeyManagers
} finally {
keyInputStream.close()
}
}
private def createSSLContext(applicationProvider: ApplicationProvider): SSLContext = {
val keyManagers = readKeyManagers()
val trustManagers = readTrustManagers()
// Configure the SSL context to use TLS
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagers, trustManagers, null)
sslContext
}
override def createSSLEngine(): SSLEngine = {
val sslContext = createSSLContext(appProvider)
// Start off with a clone of the default SSL parameters...
val sslParameters = sslContext.getDefaultSSLParameters
// Tells the server to ignore client's cipher suite preference.
// http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#cipher_suite_preference
sslParameters.setUseCipherSuitesOrder(true)
// http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SSLParameters
val needClientAuth = true
sslParameters.setNeedClientAuth(needClientAuth)
// Clone and modify the default SSL parameters.
val engine = sslContext.createSSLEngine
engine.setSSLParameters(sslParameters)
println(s"Need client auth: ${sslParameters.getNeedClientAuth}")
engine
}
}
并且 clientca.jks
是根据以下脚本配置的:
#!/bin/bash
export PW=`cat password`
# Create a self signed certificate & private key to create a root certificate authority.
keytool -genkeypair -v \
-alias clientca \
-keystore client.jks \
-dname "CN=clientca, OU=foo, O=bar, L=baz, ST=Ohio, C=US" \
-keypass:env PW \
-storepass:env PW \
-keyalg EC \
-keysize 256 \
-ext KeyUsage:critical="keyCertSign" \
-ext BasicConstraints:critical="ca:true" \
-validity 365
# Create another key pair that will act as the client. We want this signed by the client CA.
keytool -genkeypair -v \
-alias client \
-keystore client.jks \
-dname "CN=client, OU=foo, O=bar, L=baz, ST=Ohio, C=US" \
-keypass:env PW \
-storepass:env PW \
-keyalg EC \
-keysize 256 \
# Create a certificate signing request from the client certificate.
keytool -certreq -v \
-alias client \
-keypass:env PW \
-storepass:env PW \
-keystore client.jks \
-file client.csr
# Make clientCA create a certificate chain saying that client is signed by clientCA.
keytool -gencert -v \
-alias clientca \
-keypass:env PW \
-storepass:env PW \
-keystore client.jks \
-infile client.csr \
-outfile client.crt \
-ext EKU="clientAuth" \
-rfc
# Export the client-ca certificate from the keystore. This goes to nginx under "ssl_client_certificate"
# and is presented in the CertificateRequest.
keytool -export -v \
-alias clientca \
-file clientca.crt \
-storepass:env PW \
-keystore client.jks \
-rfc
# Import the signed client certificate back into client.jks. This is important, as JSSE won't send a client
# certificate if it can't find one signed by the client-ca presented in the CertificateRequest.
keytool -import -v \
-alias client \
-file client.crt \
-keystore client.jks \
-storetype JKS \
-storepass:env PW
# Export the client CA to pkcs12, so it's safe.
keytool -importkeystore -v \
-srcalias clientca \
-srckeystore client.jks \
-srcstorepass:env PW \
-destkeystore client.p12 \
-deststorepass:env PW \
-deststoretype PKCS12
# Import the client CA's public certificate into a JKS store for Play Server to read (we don't use
# the PKCS12 because it's got the CA private key and we don't want that.
keytool -import -v \
-alias clientca \
-file clientca.crt \
-keystore clientca.jks \
-storepass:env PW << EOF
yes
EOF
# List out the contents of client.jks just to confirm it.
keytool -list -v \
-keystore client.jks \
-storepass:env PW
而我的 localhost.jks
是由以下脚本生成的:
#!/bin/bash
export PW=`cat password`
# Create a server certificate, tied to localhost
keytool -genkeypair -v \
-alias localhost \
-dname "CN=localhost, OU=foo, O=bar, L=baz, ST=Ohio, C=US" \
-keystore localhost.jks \
-keypass:env PW \
-storepass:env PW \
-keyalg EC \
-keysize 256 \
-validity 385
# Create a certificate signing request for localhost
keytool -certreq -v \
-alias localhost \
-keypass:env PW \
-storepass:env PW \
-keystore localhost.jks \
-file localhost.csr
# Tell ca to sign the localhost certificate.
# Technically, digitalSignature for DHE or ECDHE, keyEncipherment for RSA
keytool -gencert -v \
-alias ca \
-keypass:env PW \
-storepass:env PW \
-keystore ca.jks \
-infile localhost.csr \
-outfile localhost.crt \
-ext KeyUsage:critical="digitalSignature,keyEncipherment" \
-ext EKU="serverAuth" \
-ext SAN="DNS:localhost" \
-rfc
# Tell localhost.jks it can trust ca as a signer.
keytool -import -v \
-alias ca \
-file ca.crt \
-keystore localhost.jks \
-storetype JKS \
-storepass:env PW << EOF
yes
EOF
# Import the signed certificate back into localhost.jks
keytool -import -v \
-alias localhost \
-file localhost.crt \
-keystore localhost.jks \
-storetype JKS \
-storepass:env PW
当我 运行 在生产中使用以下相关配置时:
http.port=disabled
https.port = 9443
play.server.https.engineProvider=https.CustomSSLEngineProvider
certificateDirectory=/path/to/certs/
play.server.https.keyStore.path=/path/to/certs/localhost.jks
play.server.https.keyStore.password=`cat /path/to/certs/password` # actual string
jdk.tls.rejectClientInitiatedRenegotiation=true
我发现当我发出不安全的 curl 时我的服务器不请求客户端证书:curl -k https://localhost:9443
:
13:35:13.173 [application-akka.actor.default-dispatcher-4] INFO akka.event.slf4j.Slf4jLogger - Slf4jLogger started
13:35:13.182 [application-akka.actor.default-dispatcher-4] DEBUG akka.event.EventStream - logger log1-Slf4jLogger started
13:35:13.185 [application-akka.actor.default-dispatcher-4] DEBUG akka.event.EventStream - Default Loggers started
13:35:13.213 [main] DEBUG play.api.libs.concurrent.ActorSystemProvider - Starting application default Akka system: application
13:35:13.368 [main] DEBUG controllers.AssetsConfiguration - Using the following cache configuration for assets:
enableCaching = true
enableCacheControl = true
defaultCacheControl = public, max-age=3600
aggressiveCacheControl = public, max-age=31536000, immutable
configuredCacheControl:
13:35:13.414 [main] INFO play.api.Play - Application started (Prod)
13:35:13.678 [application-akka.actor.default-dispatcher-2] DEBUG com.typesafe.sslconfig.akka.AkkaSSLConfig - Initializing AkkaSSLConfig extension...
13:35:13.680 [application-akka.actor.default-dispatcher-2] DEBUG com.typesafe.sslconfig.akka.AkkaSSLConfig - buildHostnameVerifier: created hostname verifier: com.typesafe.sslconfig.ssl.DefaultHostnameVerifier@4acf72b6
13:35:14.046 [application-akka.actor.default-dispatcher-3] DEBUG akka.io.TcpListener - Successfully bound to /0:0:0:0:0:0:0:0:9443
13:35:14.054 [main] INFO play.core.server.AkkaHttpServer - Listening for HTTPS on /0:0:0:0:0:0:0:0:9443
13:35:20.420 [application-akka.actor.default-dispatcher-3] DEBUG akka.io.TcpListener - New connection accepted
Need client auth: true
13:35:20.597 [application-akka.actor.default-dispatcher-3] DEBUG akka.stream.impl.io.TLSActor - closing output
尽管我的服务器需要 客户端身份验证,但从未提示客户端提供证书并成功启动了不安全的连接。为什么会这样?
有一个 open issue 与此相关。
为了使这项工作正常进行,需要在尝试进行客户端身份验证的项目中启用 sbt PlayAkkaHttp2Support
和 java agent will need to be updated 以规避 JDK 161 问题。