Was server certificate pinning broken in mysql-connector-java versions 5.1.42 onwards?
This is the conclusion I'm reaching, but 5.1.42 was released in April 2017 and something like this should likely affect more people. The lack of information I'm finding suggests there's something I'm overlooking, or:
- many have not updated past 5.1.41, or
- are not using
verifyServerCertificate=true
(which defaults totrue
whenuseSSL=true
), or - are not using
trustCertificateKeyStoreUrl
to pin certificates.
Background
Prior to version 5.1.42 (up until 5.1.41), given a server certificate chain such as this:
chain[0]: Certificate of MySQL server
+- chain[1]: Intermediate CA
+- chain[2]: Root CA
I could put "Certificate of MySQL server" (and it only) in a keystore, save it as a .jks
file, and specify this file to the trustCertificateKeyStoreUrl
property when connecting. This ensured I connected to the server described by "Certificate of MySQL server" and it only.
This was performed by mysql-connector-java
by using the default implementation of X509TrustManager
returned by TrustManagerFactory.getTrustManagers()
. The internal implementation of sun.security.ssl.X509TrustManagerImpl
uses sun.security.validator.PKIXValidator.validate(...)
which correctly builds the certificate chain from the truststore before validating it.
This handles any of these cases:
{chain[0]*} <- chain[1] <- chain[2]
{chain[0] <- chain[1]*} <- chain[2]
{chain[0] <- chain[1] <- chain[2]*}
{chain[0] <- chain[1] <- chain[2] <- *}
Where chain[0..2]
is the chain provided by the server during the handshake. The *
marks the certificate matching anything in the truststore. The {}
encloses the resulting chain that should be validated. And <-
describes the cert <- issuer cert
relationship.
What changed
From 5.1.42 onwards to the currently latest 8.0.17, mysql-connector-java
fails to connect to the server using the .jks
file described above due to a
javax.net.ssl.SSLHandshakeException
with the following cause:
Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
at com.mysql.cj.protocol.ExportControlled$X509TrustManagerWrapper.checkServerTrusted(ExportControlled.java:382)
at sun.security.ssl.AbstractTrustManagerWrapper.checkServerTrusted(SSLContextImpl.java:1091)
at sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1621)
... 28 more
Caused by: java.security.cert.CertPathValidatorException: Path does not chain with any of the trust anchors
at sun.security.provider.certpath.PKIXCertPathValidator.validate(PKIXCertPathValidator.java:154)
at sun.security.provider.certpath.PKIXCertPathValidator.engineValidate(PKIXCertPathValidator.java:80)
at java.security.cert.CertPathValidator.validate(CertPathValidator.java:292)
at com.mysql.cj.protocol.ExportControlled$X509TrustManagerWrapper.checkServerTrusted(ExportControlled.java:375)
... 30 more
Looking at the code shows mysql-connector-java
is still using sun.security.ssl.X509TrustManagerImpl
for the main verification, but before that there is an extra check which is where the exception is thrown.
This code was probably added because development wanted to also check the validity of any certificates used in the truststore. (As mentioned in the release note of 5.1.42 citing Bug #20515688.)
However, the implementation for determining the certificate in the truststore used doesn't look sound. Rather than building a chain by combining the truststore with the server provided chain, it takes the whole chain provided by the server as is then validates that as a built chain.
More precisely, the implementation uses the provided chain in a java.security.cert.CertPath
whose specifications states:
By convention, X.509 CertPaths (consisting of X509Certificates), are ordered starting with the target certificate and ending with a certificate issued by the trust anchor. That is, the issuer of one certificate is the subject of the following one. The certificate representing the TrustAnchor should not be included in the certification path.
Then calls sun.security.provider.certpath.PKIXCertPathValidator.validate(...)
to validate the above statement or else throws java.security.cert.CertPathValidatorException
.
The side-effect of this likely unintended exception effectively restricts the chain verification that can be performed from 5.1.42 onwards like so:
chain[0]* <- chain[1] <- chain[2]
-- brokenchain[0] <- chain[1]* <- chain[2]
-- brokenchain[0] <- chain[1] <- chain[2]*
-- brokenchain[0] <- chain[1] <- chain[2] <- *
-- OK
Due to this, the usefulness of trustCertificateKeyStoreUrl
is greatly reduced as the only certificate now acceptable is the one issuer beyond the chain provided by the server.
Related posts
I've only been able to find these posts describing issues that might share the same cause: