Monitoring SSL endpoints

Leverage Real Load JUnit tests to monitor SSL endpoints

Last week I wrote an article to illustrate how the ability to execute JUnit tests opens a whole new world of synthetic monitoring possibilities.

This week I’ve implemented another use case to detect SSL certificate related issues which I’ve actually seen causing issues in production environments. Most issues were were caused by expired certificates or CRLs, affecting websites, APIs or VPNs.

I’ve implemented a series of JUnit tests to verify these attributes of an SSL endpoint:

  • Check for weak SSL cipher suites
  • Check for weak SSL protocols (SSL v1.0 or v1.1 for example)
  • Check SSL certificate expiration date. Alert if less than 30 days in future
  • Check whether the certificate ended up in the CRL by mistake
  • Check whether the currently published CRL is not expiring in then next 2 days

Below you’ll find the JUnit code to implement the above tests. Once the code is deployed to the Real Load platform you can configure a Synthetic Monitoring task to execute it regularly.

The code is intended for demonstration purposes only. Actual production code could be enhanced to perform additional checks on the presented SSL certificates or the other certs in the keychain. The code could also be extended to retrieve a list of SSL endpoints to be validated from a document or an API of some sort, to simplify maintenance.

Happy monitoring!

Required dependencies (Maven)

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>com.dkfqs.tools</groupId>
            <artifactId>tools</artifactId>
            <version>4.8.23</version>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcpkix-jdk15to18</artifactId>
            <version>1.68</version>
            <type>jar</type>
        </dependency>

This is the code implementing the SSL checks mentioned above. You’ll notice a few variables at the top to set hostname, port and some other configuration parameters…


import com.dkfqs.tools.crypto.EncryptedSocket;
import com.dkfqs.tools.javatest.AbstractJUnitTest;
import static com.dkfqs.tools.javatest.AbstractJUnitTest.isArgDebugExecution;
import static com.dkfqs.tools.logging.LogAdapterInterface.LOG_DEBUG;
import static com.dkfqs.tools.logging.LogAdapterInterface.LOG_ERROR;
import com.dkfqs.tools.logging.MemoryLogAdapter;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.net.ssl.SSLSocket;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509CRL;
import java.security.cert.X509CRLEntry;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import junit.framework.TestCase;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
import org.junit.Assert;
import org.junit.Ignore;

public class TestSSLPort extends AbstractJUnitTest {

    private final static int TCP_CONNECT_TIMEOUT_MILLIS = 3000;
    private final static int SSL_HANDSHAKE_TIMEOUT_MILLIS = 2000;
    private String sslEndPoint = "www.realload.com"; // The SSL endpoint to validate
    private String DNofCertWithCRLdistPoint ="R3"; // The DN of the issuing CA - Used for CRL checks
    private int certExpirationDaysAlertThreshold = -30; //How many days before cert expiration we should be alerted.
    private int crlExpirationDaysAlertThreshold = -2; //How many days before CRL expiration we should be alerted.
    private final MemoryLogAdapter log = new MemoryLogAdapter();  // default log level is LOG_INFO

    @Before
    public void setUp() throws Exception {
        if (isArgDebugExecution()) {
            log.setLogLevel(LOG_DEBUG);
        }
        //log.setLogLevel(LOG_DEBUG);

        openAllPurposeInterface();
        log.message(LOG_DEBUG, "Testing SSL enpoint " + sslEndPoint + ":" + sslEndpointPort);
    }

    @Test
    public void CheckForWeakCipherSuites() throws Exception {
        SSLSocket sslSocket = SSLConnect(sslEndPoint, sslEndpointPort);
        String[] cipherSuites = sslSocket.getEnabledCipherSuites();
        // Add all weak ciphers here....
        TestCase.assertNotNull(cipherSuites);
        TestCase.assertFalse(Arrays.asList(cipherSuites).contains("SSL_RSA_WITH_DES_CBC_SHA"));
        TestCase.assertFalse(Arrays.asList(cipherSuites).contains("SSL_DHE_DSS_WITH_DES_CBC_SHA"));
        sslSocket.close();
    }

    @Test
    public void CheckForWeakSSLProtocols() throws Exception {
        SSLSocket sslSocket = SSLConnect(sslEndPoint, sslEndpointPort);
        String[] sslProtocols = sslSocket.getEnabledProtocols();
        TestCase.assertNotNull(sslProtocols);
        TestCase.assertFalse(Arrays.asList(sslProtocols).contains("TLSv1.0"));
        TestCase.assertFalse(Arrays.asList(sslProtocols).contains("TLSv1.1"));
        sslSocket.close();
    }

    @Test
    public void CheckCertExpiration30Days() throws Exception {
        SSLSocket sslSocket = SSLConnect(sslEndPoint, sslEndpointPort);
        TestCase.assertNotNull(sslSocket);
        Certificate[] peerCerts = sslSocket.getSession().getPeerCertificates();
        for (Certificate cert : peerCerts) {
            if (cert instanceof X509Certificate) {
                X509Certificate x = (X509Certificate) cert;
                if (x.getSubjectDN().toString().contains(sslEndPoint)) {
                    JcaX509CertificateHolder certAttrs = new JcaX509CertificateHolder(x);
                    Date expDate = certAttrs.getNotAfter();
                    log.message(LOG_DEBUG, "Cert expiration: " + expDate + " " + sslEndPoint);
                    Calendar c = Calendar.getInstance();
                    c.setTime(expDate);
                    c.add(Calendar.DATE, certExpirationDaysAlertThreshold);
                    if (new Date().after(c.getTime())) {
                        log.message(LOG_ERROR, "Cert expiration: " + expDate + ". Less than " + certExpirationDaysAlertThreshold + "days in future");
                        Assert.assertEquals(true, false);
                    }
                }
            }
        }
        sslSocket.close();
    }

    @Test
    public void CheckCRLRevocationStatus() throws Exception {
        SSLSocket sslSocket = SSLConnect(sslEndPoint, sslEndpointPort);
        TestCase.assertNotNull(sslSocket);
        Certificate[] peerCerts = sslSocket.getSession().getPeerCertificates();
        for (Certificate cert : peerCerts) {
            if (cert instanceof X509Certificate) {
                X509Certificate certToBeVerified = (X509Certificate) cert;
                if (certToBeVerified.getSubjectDN().toString().contains(DNofCertWithCRLdistPoint)) {
                    checkCRLRevocationStatus(certToBeVerified);
                }
            }
        }
        sslSocket.close();
    }

    @Test
    public void CheckCRLUpdateDueLess2Days() throws Exception {
        SSLSocket sslSocket = SSLConnect(sslEndPoint, sslEndpointPort);
        TestCase.assertNotNull(sslSocket);
        Certificate[] peerCerts = sslSocket.getSession().getPeerCertificates();
        for (Certificate cert : peerCerts) {
            if (cert instanceof X509Certificate) {
                X509Certificate certToBeVerified = (X509Certificate) cert;
                if (certToBeVerified.getSubjectDN().toString().contains(DNofCertWithCRLdistPoint)) {
                    checkCRLUpdateDueLessXDays(certToBeVerified);
                }
            }
        }
        sslSocket.close();
    }

    @After
    public void tearDown() throws Exception {
        closeAllPurposeInterface();
        log.writeToStdoutAndClear();
    }

    private static SSLSocket SSLConnect(String host, int port) throws Exception {
        EncryptedSocket encryptedSocket = new EncryptedSocket(host, port);
        encryptedSocket.setTCPConnectTimeoutMillis(TCP_CONNECT_TIMEOUT_MILLIS);
        encryptedSocket.setSSLHandshakeTimeoutMillis(SSL_HANDSHAKE_TIMEOUT_MILLIS);
        SSLSocket sslSocket = encryptedSocket.connect();
        return sslSocket;
    }

    private void checkCRLRevocationStatus(X509Certificate certificate) throws Exception {

        List<String> crlUrls = getCRLDistributionEndPoints(certificate);
        CertificateFactory cf;
        cf = CertificateFactory.getInstance("X509");

        // Loop through all CRL distribution enpoints
        for (String urlS : crlUrls) {
            log.message(LOG_DEBUG, "CRL endpoint: " + urlS);
            URL url = new URL(urlS);
            URLConnection connection = url.openConnection();
            X509CRL crl = null;
            try (DataInputStream inStream = new DataInputStream(connection.getInputStream())) {
                crl = (X509CRL) cf.generateCRL(inStream);
            }
            X509CRLEntry revokedCertificate = crl.getRevokedCertificate(certificate.getSerialNumber());

            if (revokedCertificate != null) {
                log.message(LOG_DEBUG, "Revoked");
                Assert.assertEquals(true, false);
            } else {
                log.message(LOG_DEBUG, "Valid");
            }
        }
    }

    private void checkCRLUpdateDueLessXDays(X509Certificate certificate) throws Exception {
        List<String> crlUrls = getCRLDistributionEndPoints(certificate);
        CertificateFactory cf;
        cf = CertificateFactory.getInstance("X509");

        // Loop through all CRL distribution enpoints
        for (String urlS : crlUrls) {
            log.message(LOG_DEBUG, "CRL endpoint: " + urlS);
            URL url = new URL(urlS);
            URLConnection connection = url.openConnection();
            X509CRL crl = null;
            try (DataInputStream inStream = new DataInputStream(connection.getInputStream())) {
                crl = (X509CRL) cf.generateCRL(inStream);
            }
            Date nextUpdateDueBy = crl.getNextUpdate();
            log.message(LOG_DEBUG, "CRL Next Update: " + nextUpdateDueBy + " " + urlS);
            Calendar c = Calendar.getInstance();
            c.setTime(nextUpdateDueBy);
            c.add(Calendar.DATE, crlExpirationDaysAlertThreshold);
            if (new Date().after(c.getTime())) {
                log.message(LOG_ERROR, "CRL " + urlS + " expiration: " + nextUpdateDueBy + ". Less than " + crlExpirationDaysAlertThreshold + " days in future");
                Assert.assertEquals(true, false);
            }
        }
    }

    private List<String> getCRLDistributionEndPoints(X509Certificate certificate) throws Exception {
        byte[] crlDistributionPointDerEncodedArray = certificate.getExtensionValue(Extension.cRLDistributionPoints.getId());

        ASN1InputStream oAsnInStream = new ASN1InputStream(new ByteArrayInputStream(crlDistributionPointDerEncodedArray));
        ASN1Primitive derObjCrlDP = oAsnInStream.readObject();
        DEROctetString dosCrlDP = (DEROctetString) derObjCrlDP;
        oAsnInStream.close();

        byte[] crldpExtOctets = dosCrlDP.getOctets();
        ASN1InputStream oAsnInStream2 = new ASN1InputStream(new ByteArrayInputStream(crldpExtOctets));
        ASN1Primitive derObj2 = oAsnInStream2.readObject();
        CRLDistPoint distPoint = CRLDistPoint.getInstance(derObj2);
        oAsnInStream2.close();

        List<String> crlUrls = new ArrayList<String>();
        for (DistributionPoint dp : distPoint.getDistributionPoints()) {
            DistributionPointName dpn = dp.getDistributionPoint();
            // Look for URIs in fullName
            if (dpn != null) {
                if (dpn.getType() == DistributionPointName.FULL_NAME) {
                    GeneralName[] genNames = GeneralNames.getInstance(dpn.getName()).getNames();
                    // Look for an URI
                    for (int j = 0; j < genNames.length; j++) {
                        if (genNames[j].getTagNo() == GeneralName.uniformResourceIdentifier) {
                            String url = DERIA5String.getInstance(genNames[j].getName()).getString();
                            crlUrls.add(url);
                        }
                    }
                }
            }
        }
        return crlUrls;
    }

}

P.S: Some of the code above was inspired from this Stack Overflow post.