Why isn't my server requesting a client cert in Play 2.6.x?

独自空忆成欢 提交于 2019-12-08 12:00:03

问题


I'm trying to enable client authentication by following the play-tls-example. Since this is just an experiment, I'm generating self-signed certs.

I have the following SSL engine provider:

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

and the clientca.jks was configured according to the following script:

#!/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

and my localhost.jks is generated by the following script:

#!/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

and when I run in production with the following relevant config:

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

I find that my server doesn't request the client cert when I issue an insecure 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

Despite my server requiring client auth, the client is never prompted for a cert and successfully initiates an insecure connection. Why is this happening?


回答1:


There is an open issue related to this.

To make this work, the sbt PlayAkkaHttp2Support will need to be enabled in the project attempting to do client auth and java agent will need to be updated to circumvent a JDK 161 issue.



来源:https://stackoverflow.com/questions/50180431/why-isnt-my-server-requesting-a-client-cert-in-play-2-6-x

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!