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

In a recent blog entry, I publicized my Final Quadrilogy of articles on Java SSL.

This article is a much shortened version of the rambling Local CA article in that series.

Overview

CA-signed certificates are used for public server authentication. In this article, we consider a private network of devices connecting to our servers i.e. via client-authenticated SSL. In this case devices have client certificates which we explicitly trust (and none other).

Let's setup a private CA to sign certificates with our own CA key. We will sign our server certificates, and consider signing client certificates as well.

What is a digital certificate?

Firstly we note that public key cryptography requires a private and public key. These are mathematically linked, such that the public key is used to encrypt data which can be decrypted only using the private key. Moreover, the private key can create a digital signature, which is verified using the public key.

According to Wikipedia,

A public key certificate (also known as a digital certificate or identity certificate) is an electronic document used to prove ownership of a public key. The certificate includes information about the key, information about its owner's identity, and the digital signature of an entity that has verified the certificate's contents are correct. If the signature is valid, and the person examining the certificate trusts the signer, then they know they can use that key to communicate with its owner.

So a certificate is a document which contains a public key, and its related information such as its "subject." This is the name assigned by its creator, who is the sole holder of the corresponding private key.

This document is digitally signed by the "issuer." If we trust the issuer then we can use the public key to communicate to the subject. Cryptographically speaking, we can use the public key to encrypt information, which can be decrypted only by the holder of the corresponding private key.

Finally, X.509 is standard for Public Key Infrastructure (PKI) that specifies formats for public key certicates, revocation lists, etc.

Root CA certificate

By definition, a root certificate (e.g. a CA certificate) is self-signed, and so has the same "Issuer" and "Subject." For example, inspect a GoDaddy root certificate as follows:

$ curl -s https://certs.godaddy.com/repository/gd-class2-root.crt | 
    openssl x509 -text | grep 'Issuer:\|Subject:'

        Issuer: ... OU=Go Daddy Root Certificate Authority
        Subject: ... OU=Go Daddy Root Certificate Authority

A self-signed certificate is a public key and its subject name, which is digitally signed using its corresponding private key. We can verify its signature using the public key, but have no other inherent assurances about its authenticity. We trust it explicitly via a "truststore."

Keystore vs truststore

A "keystore" contains a private key, which has a public key certificate. Additionally the keystore must contain the certificate chain of that key certificate, through to its root certificate (which is self-signed by definition).

A "truststore" contains peer or CA certificates which we trust. By definition we trust any peer certificate chain which includes any certificate which is 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, whereas the truststore must not contain the certificate chain of included trusted certificates, they differ critically in this respect.

Client certificate management

In order to review active credentials, we 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 could 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 consider signing our client certificates to avoid having such a truststore containing all our clients' self-signed certificates, but nevertheless end up with one - which is telling.

We could similarly record revoked client certificates. However for private networks where the number of certificates is relatively small, it is simpler and more secure to trust clients explicitly, rather than implicitly trusting all client certificates signed by our CA, and managing a revocation list.

If the number of clients is large, then probably we need to automate enrollment, which is addressed in the companion article Client Authentication in this series, which proposes a dynamic SQL truststore for client certificates.

Alternatively we might use a client certificate authentication server, e.g. see my experimental Node microservice github.com/evanx/certserver - which uses Redis to store certificates and their revocation list.

Self-signed client certificates

We prefer self-signed client certificates which are explicitly imported into our server truststore, where they can be reviewed. In this case, they are "revoked" by removing them from the truststore.

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 TrustManager for our server - see the Explicit Trust Manager article in this series.

Private CA

Consider that we must detect when our server has been compromised, and then generate a new server key. If using a self-signed server certificate, then we must update every clients' truststore. In order to avoid such a burden, our server certificate must be signed using a CA key which our clients trust.

However, our clients must trust only our private server, and not for example any server with a Go Daddy certificate. So we generate a private CA key. This key controls access to our server.

While our server naturally resides in a DMZ accessible to the Internet, its CA key should be isolated on a secure internal machine. In fact, it should be generated offline, where it can never be compromised (except by physical access). We transfer the "Certificate Signing Request" (CSR) to the offline CA computer, and return its signed certificate e.g. using a USB stick.

In the event that our server is compromised, we generate a new server key, and sign it using our offline CA key. Our clients are unaffected, since they trust our CA, and thereby our new server key. However our clients must no longer trust the old compromised server key. It could be used to perpetrate a man-in-the-middle (MITM) attack.

So we must support certificate revocation. For example, we could publish a certificate revocation list to our clients, or provide a revocation query service, e.g. an OCSP responder.

Alternatively, we could publish the server certificate that our clients should explicitly trust. Before connecting, our clients read this certificate, verify that it is signed by our CA, and establish it as their explicit truststore for the purposes of connecting to our server.

We consider a scenario where the above "revocation" service and our server both suffer a simultaneous coordinated MITM attack. Generally speaking, our architecture should make such an attack expensive and detectable. Our revocation service should be divorced from our server infrastructure at least, to make it more challenging.

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

Naturally the common name of a server certificate is its domain name. This is validated by the client e.g. the browser, that the certificate's "Common Name" matches the host name used to lookup its 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.

$ 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.

We import this signed certificate reply into our server keystore. But keytool will not allow a signed certificate to be imported unless its parent certificate chain is already present in the keystore. So we must import our CA cert first.

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

$ 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

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.

openssl

We can use openssl to connect to our 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

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.

Consider that our clients must trust only our server, whose certificate happens to be issued by GoDaddy - we don't want those private clients to trust any server with a certificate issued by GoDaddy!

Client keystore

We create the private keystore on each of our clients.

$ 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 as follows:

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

We inspect the certificate using openssl.

$ keytool -keystore client.jks -alias client -exportcert -rfc |
    openssl x509 -text -in client.pem
Enter keystore password:
Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number: 345747950 (0x149bb1ee)
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=client
        Validity
            Not Before: Feb 14 11:27:19 2015 GMT
            Not After : Feb 14 11:27:19 2016 GMT
        Subject: CN=client
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
...

Finally, we import each client's self-signed certificate into our server truststore.

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

Conclusion

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

Our clients should trust only our server, and not any server certificate issued by some public CA. We sign the server certificate using an offline CA key which our clients solely trust. When our server is compromised, we can change our server key without changing our clients' truststores. However, we must somehow invalidate the old server certificate. We might publish the server certificate that our clients should explicitly trust, after verifying that it is signed by our CA.

We prefer self-signed client certificates, which are explicitly trusted. However, we note that self-signed certificates are effectively CA certificates, and so a compromised private key can be used to create rogue certificates. So we should implement a custom "explicit trust manager" to ensure that the peer's key certificate itself is explicitly included in the truststore, i.e. disregarding its chain of signing certificates.

Further reading

See my experimental Node microservice github.com/evanx/certserver, which uses Redis to store certificates and a revocation list.

See the companion article Explicit Trust Manager.

This is part of my Final Quadrilogy on Java crypto.