Skip to content
Evan Summers edited this page May 7, 2016 · 86 revisions

Labels: Java, SSL, keytool, KeyStore, TrustManager

Overview

Say we have a customer which intends to deploy hundreds of devices that connect to our private Java server to perform online transactions. We propose client-authenticated SSL sockets. The next question is, should we buy CA-signed certificates for each device, or use self-signed certificates?

CA-signed certificates are typically used for server authentication. Certainly we'll buy a CA certificate for our production server. However, we are primarily concerned with client authentication. Moreover, we want to automate the deployment and enrollment of new devices.

Naturally each client generates a private key. We could import each client's self-signed certificate into our server "truststore." But as an experiment, let's setup our own CA, and sign the client certificates with our local CA key.

In the course of this exercise, we'll learn about keytool and Java SSL. We'll examine the security implications, and propose measures to mitigate risks. Finally, we'll see whether local CA signing is preferrable to self-signed certificates for client authentication.

SSLContext

An SSLServerSocket is created using an SSLContext, which we create with a keystore and a truststore, as follows.

public class LocalCaTest {
   ...
   static void accept(KeyStore keyStore, char[] keyPassword, 
         KeyStore trustStore, int port) 
         throws GeneralSecurityException, IOException {
      SSLContext sslContext = 
         SSLContexts.create(keyStore, keyPassword, trustStore);
      SSLServerSocket serverSocket = (SSLServerSocket) sslContext.
         getServerSocketFactory().createServerSocket(port);
      try {
         serverSocket.setNeedClientAuth(true);
         handle(serverSocket.accept());
      } finally {
         serverSocket.close();
      }
   }    
}

where we create an SSL context for our server socket, and accept a client connection. We'll present our SSLContexts utility further below.

Keystore vs truststore

For the purpose of SSL, the keystore must contain an asymmetric private key, which is paired with its public key certificate. Moreover, the keystore must contain the certificate chain of that key certificate, through to its root certificate (which is self-signed by definition). In fact, we'll see that keytool enforces this.

The truststore contains peer or CA certificates that we trust. By definition we trust any peer certificate chain that includes a certificate in our truststore. That is to say, if our truststore contains a CA certificate, then we trust all certificates issued by that CA.

Note that since the keystore must contain the certificate chain of the key certificate, and the truststore needn't include the certificate chains of trusted certificates, they differ critically in this respect, and so the keystore should not be misused as the truststore.

Root CA certificate

For example, let's inspect a GoDaddy root certificate.

$ openssl x509 -text -in godaddy-root.pem 
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 0 (0x0)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: ... OU=Go Daddy Root Certificate Authority
        Validity
            Not Before: Sep  1 00:00:00 2009 GMT
            Not After : Dec 31 23:59:59 2037 GMT
        Subject: ... OU=Go Daddy Root Certificate Authority
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)

We observe that the following "Basic Constraints" and "Key Usage" extensions are set.

        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:TRUE
            X509v3 Key Usage: critical
                Certificate Sign, CRL Sign

where this is indicated as a CA certificate by the "Basic Constraints." It will therefore likely be used for signing certificates, revocation lists, code, or email.

The "Key Usage" restriction of the above certificate indicates that it can be used to sign certificates, and certificate revocation lists. These usage limitations are considered when validating a certificate chain i.e. during an SSL handshake.

Alternatively we import the certificate into a keystore, and the print certificate using keytool.

$ keytool -keystore godaddy.jks -alias godaddy-root -list -v
...
Extensions: 

#1: ObjectId: 2.5.29.19 Criticality=true
BasicConstraints:[
  CA:true
  PathLen:2147483647
]

#2: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
  Key_CertSign
  Crl_Sign
]

where the path length of certificate chains that can be validated is set to the maximum for a signed integer.

Note that certificates are stored in DER format, i.e. using "Distinguished Encoding Rules" where a field can have "Object ID" e.g. we see above that "2.5.29.19" is the Object ID for "Basic Constraints."

Note that the "criticality" indicates if the extension can be ignored e.g. if not yet supported, when validating the certificate. That is, it enables backwards compatibility when new extensions are added.

Local CA key

While our server naturally resides in a DMZ accessible to the Internet, our CA key should be isolated on a more secure internal machine. In fact, our root CA key could be generated offline, where it can never be compromised. In this case, an intermediate CA key would be used for online signing. We would transfer the intermediate CA's certificate signing request to our offline CA computer, and return its signed certificate, using removable USB media.

We create our "local CA" key as follows.

$ keytool -keystore ca.jks -genkeypair -alias ca -dname "CN=ca" \
    -keyalg rsa -keysize 2048 -validity 365 -noprompt \
    -ext BasicConstraints:critical=CA:true,pathlen:2 \
    -ext KeyUsage:critical=keyCertSign,cRLSign

Incidently, our testing using Java SSL sockets indicates that these CA constraints are ignored for root certificates. For example, even if we set CA:false on our CA certificate, and excluding keyCertSign from its key usage, we can use it to sign certificates, which are accepted for Java SSL connections. This means that self-signed server and client keys can be used to issue rogue certificates, even if these contraints are set to prevent that. Later, we'll implement a custom trust manager to address this concern.

Testing

Having created our keystores and truststores using keytool, where both our client and server certificates are signed by our local CA, we will test an SSL socket connection as follows.

private void connect(String serverKeyStoreLocation, 
         String serverTrustStoreLocation, String clientKeyStoreLocation, 
         String clientTrustStoreLocation, char[] pass) 
         throws Exception {
   SSLContext serverSSLContext = SSLContexts.create(
         serverKeyStoreLocation, pass, serverTrustStoreLocation);
   SSLContext clientSSLContext = SSLContexts.create(
         clientKeyStoreLocation, pass, clientTrustStoreLocation);        
   ServerThread serverThread = new ServerThread();
   try {
      serverThread.start(serverSSLContext, PORT);
      Assert.assertNull(connect(clientSSLContext, PORT));
      Assert.assertNull(serverThread.getErrorMessage());
   } finally {
      serverThread.close();
      serverThread.join(1000);
   }        
}

where we create SSL contexts for the server socket and the client, using the provided keystores and truststores, and try to connect. We test the client connection to the server socket as follows.

static String connect(SSLContext context, int port) 
         throws GeneralSecurityException, IOException {
   SSLSocket socket = (SSLSocket) context.getSocketFactory().
         createSocket("localhost", port);
   try {
      DataOutputStream dos = 
         new DataOutputStream(socket.getOutputStream());
      dos.writeUTF("clienthello");
      DataInputStream dis = new DataInputStream(socket.getInputStream());
      Assert.assertEquals("serverhello", dis.readUTF());
      return null;
   } catch (Exception e) {
      return e.getMessage();
   } finally {
      socket.close();
   }
}

where we return an error message, otherwise null which indicates success.

Server certificate signing

We create a keystore containing a private key and its self-signed certificate (for starters) using keytool -genkeypair.

$ keytool -keystore server.jks -genkeypair -alias server -noprompt \
      -dname "CN=server.com" -keyalg rsa -keysize 2048 -validity 365

where we specify a validity period of 365 days.

Naturally the common name of a server certificate is its domain name. This is validated by the client e.g. the browser, i.e. that the certificate's common name matches the host name used to lookup the IP address.

We export a certificate signing request (CSR) using -certreq.

$ keytool -keystore server.jks -alias server -certreq -rfc \
      -file server.csr

We can sign the CSR using using -gencert which is introduced by Java7's keytool.

$ 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

where we set the X509v3 extensions to restrict the key usage for good measure, as we see for certificates we buy from a public CA. In this case, if our server key is compromised, it cannot be used to issue rogue certificates.

We now wish to import this signed certificate reply into our server keystore. As mentioned earlier, keytool will not allow a signed certificate to be imported unless its parent certificate chain is already present in the keystore.

$ keytool -keystore server.jks -alias server -importcert \
      -file server.signed.pem
Enter keystore password:
keytool error: java.lang.Exception: Failed to establish chain from reply

where the signed certificate is a so-called "reply" to its certificate signing request.

So we must import the certificate chain in order starting with the root certificate.

$ keytool -keystore server.jks -alias ca -importcert -file ca.pem

An "intermediate" certificate would be next, if we have one. Finally we import the signed certificate reply.

$ keytool -keystore server.jks -alias server -importcert \
      -file server.signed.pem

Certificate chain

We can list the certificate chain as follows.

$ keytool -keystore server.jks -alias server -list -v
...
Certificate chain length: 2

Certificate[1]:
Owner: CN=server.com
Issuer: CN=ca

Certificate[2]:
Owner: CN=ca
Issuer: CN=ca

where we have signed our server certificate with our local CA root certificate.

Note that the first certificate of the chain is our key certificate, and the last certificate is the root CA certificate. By definition the "root" certificate of a chain is self-signed. Additionally, might have an "intermediate" CA certificate that issues the key certificate.

Server truststore

Our server truststore should contain the CA certificate which signs our client certficates.

$ keytool -keystore server.trust.jks -alias ca -importcert -noprompt \
      -file ca.pem

If we use an intermediate CA certificate to sign our client certificates, then that would be imported into the truststore rather than the root CA certificate.

Note that by definition, any remote peer certificate whose chain contains a certificate in the truststore, is trusted. Therefore since each client certificate chain contains this trusted certificate (as it root certificate), it will be trusted. In fact, any certificate issued by our CA is trusted, according to this truststore.

openssl

We can use openssl to connect to the SSLServerSocket and inspect its key certificate chain as follows.

$ openssl s_client -connect localhost:4444
...
Certificate chain
 0 s:/CN=server.com
   i:/CN=ca
 1 s:/CN=ca
   i:/CN=ca
...
Acceptable client certificate CA names
/CN=ca

where the subject and issuer of the two certificates in our key certificate chain are listed, and our server will accept client certificates whose chain includes our CA certificate, i.e. as its root certificate.

This demonstrates why the keystore requires a certificate chain, i.e. to send to the peer for validation. The peer validates the chain, and checks it against our trusted certificates. It stops checking as soon as it encounters a certificate in the chain that it trusts. Therefore the chain for a trusted certificate need not be stored in the truststore, and actually must not be, otherwise we trust any certificate issued by that trusted certificate's root, irrespective of the trusted certificate itself. For example, if our clients must trust only our server, whose certificate happens to be issued by GoDaddy, we certainly don't want those clients to trust any server with a certificate issued by GoDaddy.

Server-specific CA certificate

In practice we might have multiple servers with different sets of clients, where we generate a CA certificate that is used to sign client certificates for a specific server.

$ keytool -keystore server-ca.jks -genkeypair -noprompt -keyalg rsa \
      -keysize 2048 -alias server-ca -dname "CN=server-ca" -validity 365

where the server's truststore would contain this CA certificate.

While it is possible to sign client certificates with the server key as its own CA, this key resides in the DMZ, which is our least secure zone. If compromised, the key can be used to issue rogue certificates. Therefore we choose not to store CA keys in our DMZ.

Intermediate CA certificate

We might keep our root CA key offline where it cannot possibly be compromised. In this case, we generate an intermediate CA key, where its CSR and certificate reply is transferred via removable USB media between our online intermediate CA server, and our offline root CA server.

Although the intermediate CA key is then used for certificate signing, our server and client truststores still contain the root CA certificate. However, their keystores contain their key certificate chain, which includes the intermediate CA certificate.

$ openssl s_client -connect localhost:4444
...
Certificate chain
 0 s:/CN=server.com
   i:/CN=ca-intermediate
 1 s:/CN=ca-intermediate
   i:/CN=ca
 2 s:/CN=ca
   i:/CN=ca
...
Acceptable client certificate CA names
/CN=ca

In the event that an intermediate keystore is compromised, then it's certificate should be revoked, e.g. where our root CA certificate includes a URL for its certificate revocation list. In this case, our clients can reject servers signed with that compromised intermediate certificate.

Clearly we would have to generate a new intermediate CA key, and re-sign all our certificates. To avoid such a burden, one might prefer self-signed client certificates.

Serial numbers

Incidently, KeyTool.java, as used by the keytool command-line utility, generates a random serial number as follows.

   info.set(X509CertInfo.SERIAL_NUMBER, new CertificateSerialNumber(
         new java.util.Random().nextInt() & 0x7fffffff));

We see that X509Certificate (implemented by X509CertImpl) returns a BigInteger for the certificate serial number, which should be unique, and can be used for revocation.

In a later article, we might build a local CA management tool, which uses a sequence number for certificates which it generates programmatically and records in an SQL database.

Local revocation

Ordinarily we create an SSLContext as follows.

public class SSLContexts {
   ...
   public static SSLContext create(KeyStore keyStore, char[] keyPassword,
            KeyStore trustStore) throws Exception {
      KeyManagerFactory keyManagerFactory = 
            KeyManagerFactory.getInstance("SunX509");
      keyManagerFactory.init(keyStore, keyPassword);
      TrustManagerFactory trustManagerFactory = 
            TrustManagerFactory.getInstance("SunX509");
      trustManagerFactory.init(trustStore);
      SSLContext sslContext = SSLContext.getInstance("TLS");
      sslContext.init(keyManagerFactory.getKeyManagers(),
            trustManagerFactory.getTrustManagers(), new SecureRandom());
      return sslContext;
   }
}

where we provide the keystore and truststore for this SSL context, which can be used to create an SSLServerSocket, or client SSLSocket.

Since we wish to support a local CA for client certificates, we should enable local certificate revocation in order to retain access control. For convenience, we'll revoke certificates by their common name, which must be unique.

So we create an SSLContext with a set of revoked names, as follows.

public class RevocableNameSSLContexts {
   //...
   public static SSLContext create(KeyStore keyStore, char[] keyPass,
            KeyStore trustStore, Set<String> revokedNames) 
            throws Exception {
      KeyManagerFactory keyManagerFactory = 
            KeyManagerFactory.getInstance("SunX509");
      keyManagerFactory.init(keyStore, keyPass);
      SSLContext sslContext = SSLContext.getInstance("TLS");
      TrustManager revocableTrustManager = new RevocableTrustManager(
            KeyStores.findSoleTrustedCertificate(trustStore),
            KeyStores.findX509TrustManager(trustStore),
            revokedNames);
      sslContext.init(keyManagerFactory.getKeyManagers(),
            new TrustManager[] {revocableTrustManager},
            new SecureRandom());
      return sslContext;
   }
}

where we initialise a custom RevocableTrustManager with our client certificate issuer and a set of revoked names.

public class RevocableTrustManager implements X509TrustManager {
   X509Certificate issuerCertificate;
   X509TrustManager delegate;
   Set<String> revokedNames;
   ...
   @Override
   public void checkClientTrusted(X509Certificate[] chain, 
            String authType) throws CertificateException {
      if (chain.length < 2) {
         throw new CertificateException(
            "Invalid certificate chain length");
      }
      if (!chain[0].getIssuerX500Principal().equals(
            issuerCertificate.getSubjectX500Principal())) {
         throw new CertificateException("Untrusted issuer");
      }
      if (!Arrays.equals(chain[1].getPublicKey().getEncoded(),
            issuerCertificate.getPublicKey().getEncoded())) {
         throw new CertificateException("Untrusted issuer public key");
      }
      if (revokedNames.contains(Certificates.getCN(
            chain[0].getSubjectDN()))) {
         throw new CertificateException("Certificate CN revoked");
      }
      delegate.checkClientTrusted(chain, authType);
   }
}

where we check that the client certificate is issued by our CA, and not revoked. Finally, we delegate to the standard X509TrustManager (implemented by X509TrustManagerImpl) for good measure, e.g. to validate the certificate chain, expiry dates and what not (using PKIXValidator).

Since we use this trust manager on the server to validate clients, we know its checkServerTrusted() method is not needed.

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

Dynamic revocation

The set of revoked certificates' identifiers might be read from a file, URL or database. This could be a synchronized Set that can be updated concurrently, and so enables a dynamic truststore, which we test as follows.

public class LocalCaTest {
   ...
   private void testDynamicNameRevocation(KeyStore serverKeyStore, 
            KeyStore serverTrustStore, KeyStore clientKeyStore, 
            KeyStore clientTrustStore, String revokedName) 
            throws Exception {
      Set<String> revokedNames = new ConcurrentSkipListSet();
      SSLContext serverSSLContext = RevocableNameSSLContexts.create(
         serverKeyStore, pass, serverTrustStore, revokedNames);
      SSLContext clientSSLContext = SSLContexts.create(clientKeyStore, 
         pass, clientTrustStore);
      ServerThread serverThread = new ServerThread();
      try {
         serverThread.start(serverSSLContext, port, 2);
         Assert.assertNull(connect(clientSSLContext, port));
         Assert.assertNull(serverThread.getErrorMessage());
         revokedNames.add(revokedName);
         Thread.sleep(1000);
         Assert.assertNotNull(connect(clientSSLContext, port));
         Assert.assertNotNull(serverThread.getErrorMessage());
      } finally {
         serverThread.close();
         serverThread.join(1000);
      }
   }
   ...

where we create a concurrent Set for the revoked certificates, and revoke a client certificate after the server has been started. Before the certificate is revoked, the connection should succeed, and afterwards it should fail.

Incidently, we find that the SSL session is cached, and so we must either recreate the client SSLContext instance, or set a session timeout on the client socket. We set the timeout to its minimum of 1 second, and we sleep for the same so that the client session times out. This forces re-authentication by the server, so that our trust manager is actually invoked and rejects our newly revoked certificate.

public class ServerThread extends Thread {
   ...
   static void handle(SSLSocket socket) throws IOException {
      logger.info("handle: " + socket.getSession().getPeerPrincipal());
      try {
         DataInputStream dis = 
            new DataInputStream(socket.getInputStream());
         Assert.assertEquals("clienthello", dis.readUTF());
         DataOutputStream dos = 
            new DataOutputStream(socket.getOutputStream());
         dos.writeUTF("serverhello");
         logger.info("ok");
      } finally {
         socket.getSession().getSessionContext().setSessionTimeout(1);
         socket.close();
      }
   }
}

Client keystore

Let's create the private keystore for a test client..

$ keytool -keystore client.jks -genkeypair -keyalg rsa -keysize 2048 \
      -validity 365 -alias client -dname "CN=client"

We print our certificate as PEM text using -exportcert -rfc.

$ keytool -keystore client.jks -alias client -exportcert -rfc
----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx
...

We cut and paste the exported PEM text into a file, which we inspect using openssl.

$ openssl x509 -text -in client.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 1380030508 (0x5241982c)
    Signature Algorithm: sha1WithRSAEncryption
        Issuer: CN=client
        Validity
            Not Before: Sep 24 13:48:28 2013 GMT
            Not After : Sep 24 13:48:28 2014 GMT
        Subject: CN=client
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
...

We might import the client's self-signed certificate into our server truststore.

$ keytool -keystore server.trust.jks -alias client -importcert \
      -file client.pem

Similarly, our server's certificate is imported into this client's truststore.

Client certificate signing

We export a certificate signing request (CSR) to be signed by our local CA.

$ keytool -keystore client.jks -alias client -certreq -file client.csr

We sign this CSR using -gencert (from Java7).

$ keytool -keystore ca.jks -gencert -validity 365 -rfc \
    -dname "CN=client" -infile client.csr -outfile client.signed.pem \
    -ext BasicConstraints:critical=ca:false,pathlen:0 \
    -ext KeyUsage:critical=digitalSignature \
    -ext ExtendedKeyUsage:critical=clientAuth

where we specify an unchanged name and validity period for the newly signed certificate. We add the appropriate extensions to restrict key usage so that a client key cannot be used to issue rogue certificates.

We inspect the certificate using openssl.

$ openssl x509 -text -in client.signed.pem
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 448957773 (0x1ac28d4d)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=ca
        Validity
            Not Before: Oct 16 20:02:24 2013 GMT
            Not After : Jul 11 20:02:24 2016 GMT
        Subject: CN=client
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
...

We see that the X509v3 extensions are set as follows.

      X509v3 Basic Constraints: critical
          CA:FALSE
      X509v3 Extended Key Usage: critical
          TLS Web Client Authentication
      X509v3 Key Usage: critical
          Digital Signature

We compare these settings to a certificate bought from GoDaddy.com, for example.

  X509v3 extensions:
      X509v3 Basic Constraints: critical
          CA:FALSE
      X509v3 Extended Key Usage: 
          TLS Web Server Authentication, TLS Web Client Authentication
      X509v3 Key Usage: critical
          Digital Signature, Key Encipherment

where our tests indicate that the "Digital Signature" usage is required for client authentication, and "Key Encipherment" is required for server authentication.

Since the keystore requires its key certificate chain to be imported in the correct order starting with the root cert, we import the CA certificate first, and then our signed cert.

$ keytool -keystore client.jks -importcert -noprompt \
    -file ca.pem -alias ca
Enter keystore password: 
Certificate was added to keystore

$ keytool -keystore client.jks -importcert -noprompt \
    -file client.signed.pem -alias client
Enter keystore password: 
Certificate reply was installed in keystore

Otherwise if the parent chain is not present, we're balked by the following keytool error.

keytool error: java.lang.Exception: Failed to establish chain from reply

Incidently, in unit tests we create keystores programmatically, taking cues from KeyTool.java, to emulate the manual procedure presented here.

Client certificate management

Clearly a certificate with a duplicate common name impersonates the original certificate with that name. So when issuing a client certificate, we must take care to ensure the uniqueness of its common name, and should add the certificate (or at least its unique identifier) to a registry of some sort.

In order to review access, we clearly require a perfect record of all issued certificates. If a certificate is signed but not recorded, or its record is deleted, our server is forever vulnerable to that rogue certificate.

We might record our signed certificates into a keystore file as follows.

$ keytool -keystore server.issued.jks -importcert -alias client \
      -file client.pem 
Certificate was added to keystore

where this is not a truststore per se, but just a database of issued certificates.

Interestingly, we sign our client certificates to avoid having such a truststore containing all our client certificates, but nevertheless end up with one, which is telling.

Self-signed client certificates

If our local CA key is compromised by a breach, or abused by an administrator, rogue certificates can might be created. So we prefer self-signed client certificates which are explicitly imported into our server truststore where they can be reviewed.

However, self-signed client keys are effectively CA keys, and so rogue certificates can be created using compromised client keys, e.g. using keytool -gencert. So we implement a custom ExplicitTrustManager, where the key certificate must be in the truststore. (See the companion Explicit Trust Manager article in this series.)

If using self-signed server certificates, then this explicit trust manager would protect clients against rogue certificates issued by a compromised server key. However, hopefully we know when our server has been compromised. In this event, we should generate a new server key, and update each client's truststore appropriately, which might be quite a burden.

So the best approach for a private server is to sign its certificate using an offline CA key. Our private clients trust its root 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, via USB key. In spite this server key change, our clients are unaffected.

Note that it does not help to buy a server certificate from a public CA, as our clients must trust only our private server, and not any server signed that by that CA. Clearly we need a CA key specific to our server, and none other.

Furthermore

We might implement a local CA management tool that records issued certificates, and supports standard CRLs, if not a local CA server that supports the Online Certificate Status Protocol.

Conclusion

Public CA certificates are typically used for public server authentication. However, we are primarily concerned with client authentication for access to a private server.

As an experiment, we sign client certificates using a local CA root certificate. In this case, we should then support certificate revocation, e.g. to disable certificates for devices that have gone amiss. So we introduce a custom SSL trust manager to support a local revocation list.

We discuss the risk of rogue certificates issued by our CA, and argue that explicitly importing each client certificate into the truststore enables us to review access.

Our clients should trust only our server, and not any server certificate issued by some public CA. Naturally, if our server is compromised, we need to change our server key. In this event, if our clients specifically trust our server, then we must update every client's truststore. To avoid this burden, we rather use an offline server CA key to sign our server certificate. Our clients trust that private CA certificate, and so are unaffected by a server key change.

If we have an offline root CA certificate, we could create an intermediate CA certificate to sign our client certificates. However, in the event that this is compromised, every client's certificate must be reissued. To avoid that eventuality, and also the burden of signing client certificates, we prefer self-signed client certificates.

However, we note that self-signed client certificates are effectively CA certificates, and so compromised client keys can be used to create rogue certificates. So we implement an "explicit" trust manager to check that the peer's key certificate is in the truststore, and disregard its chain.

Resources

You can browse the code for this exercise on github.com/evanx/vellum.

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 Explicit Trust Manager, we present a custom X509TrustManager that only trusts client certificates that are explicitly imported into our truststore, for a secure "private network" of clients connecting to a "private" Java server.

In Dual Control, we present our dualcontrol package for satisfying the PCI DSS requirements for "dual control" and "split knowledge" with respect to protecting data-encrypting keys.

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.