ExplicitTrustManager - evanx/vellum GitHub Wiki

Say we intend to deploy hundreds of devices that connect to our private Java server to perform online transactions. So we want client-authenticated SSL sockets. The question is, should we buy CA-signed certificates, or use self-signed certificates?

Server

CA-signed certificates are typically used for the authentication of public servers. However, our clients must trust our private server exclusively, and not any server certificate issued by some CA, e.g. to negate a man-in-the-middle attack. So we need our own CA specific to our server, and none other.

Client

Actually we are primarily concerned with client authentication, and we want to automate the deployment and enrollment of new devices. Self-signed certificates enable automation, but is this at the cost of security?

Rogue certificates

Self-signed peer certificates in a truststore are effectively root CA certificates. Consequently, a rogue certificate can be issued using a compromised client's keystore, by abusing the client key as a trusted CA key, to exploit the server's trust, as follows.

$ keytool -keystore keystore.jks -alias mykey -gencert -validity 365 \
    -rfc -infile rogue.csr -outfile rogue.signed.pem -dname "CN=clientx"

where keytool is used sign a CSR for a rogue certificate, using our compromised keystore. Alternatively, one could use openssl.

The rogue certificate would be trusted by virtue of being signed by a trusted certificate. Let's prevent that!

Custom trust manager

So for a private server, we should trust only client certificates that are explicitly imported into our truststore, irrespective of their certificate chain. For this purpose, we implement a custom X509TrustManager.

public class ExplicitTrustManager implements X509TrustManager {
   final private Map<String, X509Certificate> certificateMap = 
         new HashMap();

   public ExplicitTrustManager(KeyStore trustStore) 
            throws GeneralSecurityException {
      this.delegate = KeyStores.findX509TrustManager(trustStore);
      for (String alias : Collections.list(trustStore.aliases())) {
         X509Certificate certificate = (X509Certificate) 
               trustStore.getCertificate(alias);
         String commonName = 
               Certificates.getCommonName(certificate.getSubjectDN());
         certificateMap.put(commonName, certificate);
      }
   }
   ...
}

where we build a map of our trusted certificates by their common name.

We implement the methods required for an X509TrustManager.

   @Override
   public X509Certificate[] getAcceptedIssuers() {
      return new X509Certificate[0];
   }

   @Override
   public void checkClientTrusted(X509Certificate[] chain, 
         String authType) throws CertificateException {
      checkTrusted(chain);
   }

   @Override
   public void checkServerTrusted(X509Certificate[] chain, 
         String authType) throws CertificateException {
      throw new CertificateException(
         "Server authentication not supported");
   }

where we return an empty list of accepted issuers, so our trust manager applies to all certificates. We delegate checkClientTrusted to the following method.

   private void checkTrusted(X509Certificate[] chain) 
         throws CertificateException {
       if (chain.length == 0) {
          throw new CertificateException("Invalid cert chain length");
       }
       X509Certificate trustedCertificate = certificateMap.get(
             Certificates.getCommonName(chain[0].getSubjectDN()));
       if (trustedCertificate == null) {
          throw new CertificateException("Untrusted peer certificate");
       }
       if (!Arrays.equals(chain[0].getPublicKey().getEncoded(),
             trustedCertificate.getPublicKey().getEncoded())) {
          throw new CertificateException("Invalid peer certificate");
       }
       trustedCertificate.checkValidity();
   }

where we lookup the trusted certificate with the same common name as the peer's key certificate, i.e. the first certificate in the chain. We check the equality of this peer certificate to our trusted certificate, via its public key. This prevents a spoofing attack using a rogue certificate with the same common name.

We note that certificates are "revoked" by removing them from the truststore, and restarting our server.

Disclaimer: This trust manager is an untested prototype and should not be used in production without further review and thorough testing ;)

Delegate trust manager

Additionally we might choose to delegate to the default trust manager, which we find as follows.

public static X509TrustManager findX509TrustManager(KeyStore trustStore)
         throws GeneralSecurityException {
   TrustManagerFactory trustManagerFactory = 
      TrustManagerFactory.getInstance("SunX509");
   trustManagerFactory.init(trustStore);
   if (trustManagerFactory.getTrustManagers().length != 1) {
      throw new GeneralSecurityException(
         "Multiple default trust managers");
   }
   if (trustManagerFactory.getTrustManagers()[0] 
         instanceof X509TrustManager) {
      return (X509TrustManager) 
         trustManagerFactory.getTrustManagers()[0];
   }
   throw new GeneralSecurityException(
      "Default X509TrustManager not found");
}

In this case, our checkClientTrusted() method also delegates to the default trust manager as follows.

   @Override
   public void checkClientTrusted(X509Certificate[] chain, 
         String authType) throws CertificateException {
      checkTrusted(chain);
      delegate.checkClientTrusted(chain, authType);
   }

Common name

Incidently, we extract the common name as follows.

public class Certificates {

   public static String getCommonName(Principal principal) 
            throws CertificateException {
      String dname = principal.getName();
      try {
         LdapName ln = new LdapName(dname);
         for (Rdn rdn : ln.getRdns()) {
            if (rdn.getType().equalsIgnoreCase("CN")) {
               return rdn.getValue().toString();
            }
         }
         throw new InvalidNameException("no CN: " + dname);
      } catch (Exception e) {
         throw new CertificateException(e.getMessage());
      }
   }
}

where we use javax.naming.ldap.LdapName.

Server key change

When we discover that our server has been compromised, we must generate a new server key. If our clients trust the server certificate specifically, then we must update each client's truststore accordingly. This might be quite a burden. Moreover, we might wish to change our server key periodically.

We want a server key change to not affect its clients. So we sign its certificate using an offline CA key, which we create for this purpose. Our private clients trust this CA certificate, which cannot be compromised (except by physical access). In the event that our server is compromised, we generate a new server key, which we sign by transferring its CSR to our CA machine, and returning its signed certificate reply, via USB key. (See the companion Local CA article in this series.)

$ keytool -keystore ca.jks -alias ca -gencert -infile server.csr \
    -dname "CN=server.com" \
    -validity 365 -rfc -outfile server.signed.pem \
    -ext BasicConstraints:critical=ca:false,pathlen:0 \
    -ext KeyUsage:critical=keyEncipherment \
    -ext ExtendedKeyUsage:critical=serverAuth

In this case, we can perform a server key change whereby our clients are unaffected.

Conclusion

CA-signed certificates are typically used for the authentication of public servers by any browser. However, we are concerned with client-authenticated devices, connecting to a private server. Our server and each of its clients should exclusively trust each other, and certainly not trust any certificate issued by some public CA.

We note that a self-signed client certificate in our server truststore is effectively a CA certificate. Therefore a compromised client keystore can be abused to sign rogue certificates. These are likewise trusted, by virtue of being issued by a trusted certificate. So we implement a custom trust manager that trusts only certificates that are explicitly imported into our truststore, irrespective of their certificate chain.

We note that a compromised client keystore can itself be used to spoof that client. Such an attack can be detected by analyzing logs for simultaneous connections, ostensibly from the same client, but from an alternate IP address. We note that rogue certificates can spoof any chosen certificate name, and so present a higher risk.

If our server is compromised, the attacker might inject a fraudulent client certificate into our server truststore. However such tampering is overt, and detectable by monitoring our truststore.

When we discover that our server is compromised, we must of course change our server key. Since our clients specifically trust our server's certificate, we must update every client's truststore. To avoid this burden, we should use an offline CA key to sign our server certificate. Our clients trust this local CA certificate, and so are unaffected by a server key change.

Resources

https://github.com/evanx/vellum/wiki - see ExplicitTrustManager.java.

Further reading

This is part of The Final Quadrilogy on Java crypto.

In Client Authentication, we introduce a trust manager to automate certificate enrollment, where new clients' certificates are automatically imported into an SQL truststore when they connect for the first time.

In Local CA, we discuss how one could setup a local private CA facility to issue client certificates.

Also see the previous series, The Enigma Posts.

Also relating to Java crypto, see my blog articles: Password Salt for secure passwords; and leveraging the Google Authenticator mobile app for multi-factor authentication for your own sites.

⚠️ **GitHub.com Fallback** ⚠️