§Testing SSL
Testing an SSL client not only involves unit and integration testing, but also involves adversarial testing, which tests that an attacker cannot break or subvert a secure connection.
§Unit Testing
Play comes with play.api.test.WsTestClient
, which provides two methods, wsCall
and wsUrl
. It can be helpful to use PlaySpecification
and in new WithApplication
"calls index" in new WithApplication() {
await(wsCall(routes.Application.index()).get())
}
wsUrl("https://example.com").get()
§Integration Testing
If you want confirmation that your client is correctly configured, you can call out to HowsMySSL, which has an API to check JSSE settings.
import java.io.File
import play.api.{Mode, Environment}
import play.api.libs.json.JsSuccess
import play.api.libs.ws._
import play.api.libs.ws.ning._
import play.api.test._
import com.typesafe.config.{ConfigFactory, ConfigValueFactory}
import scala.concurrent.duration._
class HowsMySSLSpec extends PlaySpecification {
def createClient(rawConfig: play.api.Configuration): WSClient = {
val classLoader = Thread.currentThread().getContextClassLoader
val parser = new WSConfigParser(rawConfig, new Environment(new File("."), classLoader, Mode.Test))
val clientConfig = new NingWSClientConfig(parser.parse())
// Debug flags only take effect in JSSE when DebugConfiguration().configure is called.
//import play.api.libs.ws.ssl.debug.DebugConfiguration
//clientConfig.ssl.map {
// _.debug.map(new DebugConfiguration().configure)
//}
val builder = new NingAsyncHttpClientConfigBuilder(clientConfig)
val client = new NingWSClient(builder.build())
client
}
def configToMap(configString: String): Map[String, _] = {
import scala.collection.JavaConverters._
ConfigFactory.parseString(configString).root().unwrapped().asScala.toMap
}
"WS" should {
"verify common behavior" in {
// Digital Signature Trust Co certificate is not in the JDK trust chain.
val dstRootCa = """|-----BEGIN CERTIFICATE-----
|MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
|MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
|DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
|PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
|Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
|AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
|rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
|OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
|xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
|7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
|aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
|HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
|SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
|ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
|AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
|R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
|JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
|Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
|-----END CERTIFICATE-----
|""".stripMargin
val configString = """
|//play.ws.ssl.debug=["certpath", "ssl", "trustmanager"]
|play.ws.ssl.protocol="TLSv1"
|play.ws.ssl.enabledProtocols=["TLSv1"]
|
|play.ws.ssl.trustManager = {
| stores = [
| { type: "PEM", data = ${dstRootCa.pem} }
| ]
|}
""".stripMargin
val rawConfig = ConfigFactory.parseString(configString)
val configWithPem = rawConfig.withValue("dstRootCa.pem", ConfigValueFactory.fromAnyRef(dstRootCa))
val configWithSystemProperties = ConfigFactory.load(configWithPem)
val playConfiguration = play.api.Configuration(configWithSystemProperties)
val client = createClient(playConfiguration)
val response = await(client.url("https://www.howsmyssl.com/a/check").get())(5.seconds)
response.status must be_==(200)
val jsonOutput = response.json
val result = (jsonOutput \ "tls_version").validate[String]
result must beLike {
case JsSuccess(value, path) =>
value must_== "TLS 1.0"
}
}
}
}
Note that if you are writing tests that involve custom configuration such as revocation checking or disabled algorithms, you may need to pass system properties into SBT:
javaOptions in Test ++= Seq("-Dcom.sun.security.enableCRLDP=true", "-Dcom.sun.net.ssl.checkRevocation=true", "-Djavax.net.debug=all")
§Adversarial Testing
There are several points of where a connection can be attacked. Writing these tests is fairly easy, and running these adversarial tests against unsuspecting programmers can be extremely satisfying.
NOTE:This should not be taken as a complete list, but as a guide. In situations where security is paramount, a review should be done by professional info-sec consultants.
§Testing Certificate Verification
Write a test to connect to "https://example.com". The server should present a certificate which says the subjectAltName is dnsName, but the certificate should be signed by a CA certificate which is not in the trust store. The client should reject it.
This is a very common failure. There are a number of proxies like mitmproxy or Fiddler which will only work if certificate verification is disabled or the proxy’s certificate is explicitly added to the trust store.
§Testing Weak Cipher Suites
The server should send a cipher suite that includes NULL or ANON cipher suites in the handshake. If the client accepts it, it is sending unencrypted data.
NOTE: For a more in depth test of a server’s cipher suites, see sslyze.
§Testing Certificate Validation
To test for weak signatures, the server should send the client a certificate which has been signed with, for example, the MD2 digest algorithm. The client should reject it as being too weak.
To test for weak certificate, The server should send the client a certificate which contains a public key with a key size under 1024 bits. The client should reject it as being too weak.
NOTE: For a more in depth test of certification validation, see tlspretense and frankencert.
§Testing Hostname Verification
Write a test to "https://example.com". If the server presents a certificate where the subjectAltName’s dnsName is not example.com, the connection should terminate.
NOTE: For a more in depth test, see dnschef.
Next: Databases
Found an error in this documentation? The source code for this page can be found here. After reading the documentation guidelines, please feel free to contribute a pull request. Have questions or advice to share? Go to our community forums to start a conversation with the community.