Construct secure networked applications with certificates, Part 4

Authenticate clients and servers, and verify certificate chains

Over the course of the last several months, you've learned about public-key cryptography, X.509 certificates, and certificate revocation lists. You've surveyed their associated Java classes, and you've come to understand their importance. Now you need to learn how to use them, and that's where you're headed this month.

You can read the whole series on certificates:

Imagine the following scenario: You're building a distributed application for the health insurance industry. The HIPPA (Health Insurance Portability and Accountability Act of 1996) Security Guidelines require secure access to sensitive information stored in compliant systems. Those requirements encompass both access by individuals and access by other applications. Therefore, in your distributed system, interactions between the components must be secure. Client applications must be able to authenticate the servers they connect to before transmitting sensitive information. Servers must be able to authenticate client applications before accepting and operating on sensitive information provided by clients.

One way to provide authentication is to use SSL (Secure Socket Layer). SSL, which is available for Java in the JSSE (Java Secure Socket Extension), handles authentication among communicating processes using X.509 technology and provides encryption support using various encryption algorithms of assorted strengths. For many applications, honestly, this is the way to go, especially if you want an out-of-the-box solution and can guarantee that both sides provide SSL support. However, SSL won't work in some cases; maybe you don't need it, can't use it, or don't want to use it. In those cases you have to provide similar functionality yourself.

Authentication

Let's take a high-level look at the problem. Consider the following interactions between a client and a server, which are typical of both SSL-enabled applications (although hidden from view) and the custom applications built using X.509 technology:

  1. The client opens a connection to the server and asks the server to authenticate itself.
  2. The server authenticates itself and -- optionally -- asks the client to authenticate itself. Client authentication, while possible with SSL, is seldom used in most SSL transactions. However, for enterprise applications, in which auditing of all transactions is important, client authentication provides the only way to determine for sure that the client's claimed identity is legitimate.
  3. The client authenticates itself. If the client desires an encrypted connection, it takes steps to establish one. I won't describe the process of establishing an encrypted connection at this time.
  4. The client begins the transaction.

Server authentication and client authentication essentially mirror each other, so it's sufficient to talk about one or the other. Let's look closely at the server authentication process (steps 1 and 2).

For a client to authenticate a server, the server must complete a task that only it can complete, and the client must be able to verify that operation. To do that, both the client and the server use the public-key cryptography operations that I discussed in Part 1.

After a client connects to a server, it sends a block of data to the server and asks the server to digitally sign the data with its private key -- an operation that only the server should be able to do -- assuming that its private key hasn't been compromised. The server responds with the digital signature, and the client, using the public key stored in the server's certificate, verifies the server's digital signature. Authentication is complete.

Not quite. In many situations it's possible that the client and server have not been introduced previously, and therefore, the client won't have the server's certificate in its possession. If this is likely to be a common scenario, the server can return its own certificate along with the signature in its response.

If the server supplies a copy of its certificate to the client, the application becomes susceptible to what is known as a man-in-the-middle attack. In this attack, a malicious third party sits between two communicating parties and, to each party, masquerades as the other party. To defend against this attack, you must verify that the supplied certificate belongs to the party to which you intend to communicate.

In no event should the server be allowed to sign data it generates itself and then return both the data and the signature to the client for verification. While not obvious, that approach is susceptible to what is known as a replay attack: a malicious third party intercepts the data and the signature and subsequently uses them to authenticate itself as the original server.

Let's talk about how to verify the supplied certificate in order to protect against these attacks.

Certificate verification

The most obvious way to verify a certificate is to compare the name in the certificate (the DN, or distinguished name, in X.509 -- although sometimes the subjectAltName extension is used as well) with the name of the party with which you intend to communicate. Unfortunately, this simple approach features a snag: anyone can generate a certificate that contains a public key and an arbitrary name. How do you verify that the public key in the certificate actually belongs to the entity named in the certificate?

A certificate contains at least three pieces of information:

  • A name
  • A public key purportedly belonging to that name
  • A third party's signature indicating that the third party believes the certificate's information to be truthful

Therefore, to verify the certificate, you must validate the third-party signature on the certificate.

But what's to prevent the unknown server from impersonating a third party and signing the certificate? Nothing. However, you can reject certificates signed by anyone but a small group of trusted entities known as certificate authorities, or CAs (see Part 1). If you know the CA's public key -- because you have a copy of its certificate -- you can verify the CA's signature on the certificate and gain some assurance of its accuracy.

In summary, the client verifies the server's certificate by examining the name on the certificate and determining whether or not it's the name of the server to which it desires to communicate. If the server's certificate has the right name, the client inspects the certificate's signature to determine whether or not it was generated by a CA that it trusts. Authentication is complete.

Again, not quite. In practice the situation proves more complicated. First, it's possible that someone you don't know or trust signed the server certificate, but you have a certificate for this signer that was, in turn, signed by someone you do trust. In practice, these certificate chains can extend several generations. To verify the server certificate, you must verify every certificate in the chain all the way back to a trusted root certificate.

But wait, there's more: Certificates are valid for only a limited period of time. X.509 certificates contain begin and end dates. For the certificate chain to be valid, all certificates in the chain must be within their validity period. Furthermore, as you learned in Part 3, certificates can be revoked. It's therefore necessary to check the appropriate CRLs (certificate revocation lists) for every certificate in the certificate chain.

You're not done yet. Certificates can contain critical extensions, as you learned in Part 2. Although the details aren't well defined, implementations supposedly ensure that restrictions embodied in extensions are followed when they pertain to certificate validation. For example, the keyUsage extension specifies how to use a certificate: you shouldn't use an encipherment certificate for digital signatures.

Unfortunately, you're still not finished. The IETF PKIX's (Internet Engineering Task Force Public Key Infrastructure X.509 working group) latest draft of the certificate standard, which goes into great depth on certificate chain verification, also requires that the verification process handle policies. Certificate policy terms indicate the policy under which a certificate has been issued and the purposes for which the certificate may be used.

Tired yet? Frankly, I'm about shot. This process requires a lot of work and it's hard to get it right. Later I'll talk about some efforts under development that will alleviate some of the difficulty. Right now I want to give you some code to play with that completes a subset of the steps above, but enough to provide accurate and useful certificate chain verification.

The code

If you toss out policies, extensions, and CRLs, a manageable set of operations remains for you to work with. Precedent indicates that you can sometimes leave these pieces out. SSL, the X.509 killer app available in nearly every browser in existence, doesn't perform any of those operations.

Here are the steps to verify an X.509 certificate chain:

  1. Working down the chain, for every certificate in the chain, verify that the certificate's subject is the issuer of the next certificate in the chain
  2. Verify that a third party who the client trusts issued the chain's first certificate
  3. Verify that the chain's last certificate corresponds to the server you desire to authenticate
  4. For every certificate in the chain, verify that the certificate is valid at the current time

The following code performs these operations for a certificate against a certificate chain:

  public
  static
  boolean
  verify
  (
    X509Certificate x509certificateRoot,
    Collection collectionX509CertificateChain,
    String stringTarget
  )
  {
    int nSize = collectionX509CertificateChain.size();
    X509Certificate [] arx509certificate = new X509Certificate [nSize];
    collectionX509CertificateChain.toArray(arx509certificate);
    // Working down the chain, for every certificate in the chain,
    // verify that the subject of the certificate is the issuer of the
    // next certificate in the chain.
    Principal principalLast = null;
    for (int i = 0; i < nSize; i++)
    {
      X509Certificate x509certificate = arx509certificate[i];
      Principal principalIssuer = x509certificate.getIssuerDN();
      Principal principalSubject = x509certificate.getSubjectDN();
      if (principalLast != null)
      {
        if (principalIssuer.equals(principalLast))
        {
          try
          {
            PublicKey publickey = arx509certificate[i - 1].getPublicKey();
            arx509certificate[i].verify(publickey);
          }
          catch (GeneralSecurityException generalsecurityexception)
          {
            System.out.println("signature verification failed");
            return false;
          }
        }
        else
        {
          System.out.println("subject/issuer verification failed");
          return false;
        }
      }
      principalLast = principalSubject;
    }
    // Verify that the the first certificate in the chain was issued
    // by a third-party that the client trusts.
    try
    {
      PublicKey publickey = x509certificateRoot.getPublicKey();
      arx509certificate[0].verify(publickey);
    }
    catch (GeneralSecurityException generalsecurityexception)
    {
      System.out.println("signature verification failed");
      return false;
    }
    // Verify that the last certificate in the chain corresponds to
    // the server we desire to authenticate.
    Principal principalSubject = arx509certificate[nSize - 
1].getSubjectDN();
    if (!stringTarget.equals(principalSubject.getName()))
    {
      System.out.println("target verification failed");
      return false;
    }
    // For every certificate in the chain, verify that the certificate
    // is valid at the current time.
    Date date = new Date();
    for (int i = 0; i < nSize; i++)
    {
      try
      {
        arx509certificate[i].checkValidity(date);
      }
      catch (GeneralSecurityException generalsecurityexception)
      {
        System.out.println("invalid date");
        return false;
      }
    }
    return true;
  }

In Resources I've included a client that performs these operations on a chain of certificates. Assuming you've added the appropriate jar files to your CLASSPATH -- the example uses RSA (Rivest-Shamir-Adleman) certificates, so you'll also need to download and install JCE (Java Cryptography Extension) and JSSE -- you run the client as follows:

  java Client <> <>

The client will attempt to verify the certificate chain using the techniques I've presented in this article. It will print the result of this authentication step to the console.

Future directions

Earlier I mentioned that certificate verification is a lengthy process, and that implementers are prone to making mistakes. Luckily, help is on the way. Java Specification Request #55, Certification Path API, lead by Sean Mullen of Sun, will provide a general purpose API for completing X.509 certification path (or chain) verification according to the emerging PKIX standards. If carried out correctly, it will greatly simplify the process of certificate verification.

1 2 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more