Я пытаюсь включить аутентификацию клиента, следуя 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
и my 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 -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
Несмотря на то, что мой сервер требует аутентификации клиента, клиент никогда не запрашивает сертификат и успешно инициирует небезопасное соединение. Почему это происходит?