javasslocspcertificate-revocation

How to enable OCSP stapling on java TLS server?


This post is cross-posted, so make sure you check for updates in coderanch.

I'm quite stuck implementing OCSP revocation checking on my client/server application, I managed to make client-side OCSP work, I implemented my own OCSP responder with openssl and I'm checking certificates signed by my own CA.

The problem comes when trying to check them from the server. I followed the instructions on the Standard Edition Security Developer’s Guide, more exactly like this:

static class ServerParameters {
    boolean enabled = true;
    int cacheSize = 256;
    int cacheLifetime = 3600;
    int respTimeout = 5000;
    String respUri = "http://localhost:9999";
    boolean respOverride = false;
    boolean ignoreExts = false;
    String[] protocols = new String[]{ "TLSv1.2" };
    String[] ciphers = null;

    ServerParameters() { }
}

... Before I call SSLContext.getInstance("TSL"):

ServerParameters servParams = new ServerParameters();        
System.setProperty("jdk.tls.server.enableStatusRequestExtension",
                Boolean.toString(servParams.enabled));

    System.setProperty("jdk.tls.stapling.cacheSize",
            Integer.toString(servParams.cacheSize));
    System.setProperty("jdk.tls.stapling.cacheLifetime",
            Integer.toString(servParams.cacheLifetime));
    System.setProperty("jdk.tls.stapling.responseTimeout",
            Integer.toString(servParams.respTimeout));
    System.setProperty("jdk.tls.stapling.responderURI", servParams.respUri);
    System.setProperty("jdk.tls.stapling.responderOverride",
            Boolean.toString(servParams.respOverride));
    System.setProperty("jdk.tls.stapling.ignoreExtensions",
            Boolean.toString(servParams.ignoreExts));

Inspecting the packets during the handshake, I realized that the client adds correctly the status_request extension, but the server doesn't send the CertificateStatusMessage, neither it sends a request to the OCSP responder.

I added the responder URL to the certificate also. I tried with both TLS 1.2 and 1.3, Java 11 and 15. No success.

How it should work:
How it should work

How Wireshark looks:
How Wireshark looks

Correctly configured ports for wireshark shown messages as tls:
Correctly configured ports for wireshark shown messages as tls

I don't know what I'm doing wrong, the documentation looks really easy, but I can't make it work.

Text file generated by Wireshark with the handshake packet info: file.txt

Client hello status_request:

        Extension: status_request (len=5)
            Type: status_request (5)
            Length: 5
            Certificate Status Type: OCSP (1)
            Responder ID list Length: 0
            Request Extensions Length: 0

As it can be seen, the client hello include the status request, but the server, that should include one if the client does, is not writing the extension, is like it doesn't realize about the status_request.

ADDED: Full code I'm using for test, which is a modification getting rid of the start of a ocsp responder and creating certificates of this code :

    /*
 * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

// SunJSSE does not support dynamic system properties, no way to re-use
// system properties in samevm/agentvm mode.

/*
 * @test
 * @bug 8046321 8153829
 * @summary OCSP Stapling for TLS
 * @library ../../../../java/security/testlibrary
 * @build CertificateBuilder SimpleOCSPServer
 * @run main/othervm SSLSocketWithStapling
 */

import java.io.*;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.Socket;
import java.net.ServerSocket;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import javax.net.ssl.*;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.Security;
import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
import java.security.cert.CertPathValidatorException.BasicReason;
import java.security.cert.Certificate;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.security.cert.PKIXRevocationChecker;
import java.security.cert.PKIXRevocationChecker.Option;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

public class SSLSocketWithStapling {

    /*
     * =============================================================
     * Set the various variables needed for the tests, then
     * specify what tests to run on each side.
     */

    // Turn on TLS debugging
    static final boolean debug = false;

    /*
 * Should we run the client or server in a separate thread?
 * Both sides can throw exceptions, but do you have a preference
 * as to which side should be the main thread.
 */
static boolean separateServerThread = true;
Thread clientThread = null;
Thread serverThread = null;

static String passwd = "serverpass";

/*
 * Is the server ready to serve?
 */
volatile static boolean serverReady = false;
volatile int serverPort = 443;

volatile Exception serverException = null;
volatile Exception clientException = null;

// PKI components we will need for this test
static KeyStore serverKeystore;         // SSL Server Keystore
static KeyStore trustStore;             // SSL Client trust store

// Extra configuration parameters and constants
static final String[] TLS13ONLY = new String[] { "TLSv1.3" };
static final String[] TLS12MAX =
        new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" };

/*
 * If the client or server is doing some kind of object creation
 * that the other side depends on, and that thread prematurely
 * exits, you may experience a hang.  The test harness will
 * terminate all hung threads after its timeout has expired,
 * currently 3 minutes by default, but you might try to be
 * smart about it....
 */
public static void main(String[] args) throws Exception {
    if (debug) {
        System.setProperty("javax.net.debug", "ssl:handshake");
    }

    try {
        String keystoreServerPath ="/Users/lexy/Desktop/Clases/Seguridad/almacenes/keystoreServidor.jceks";
        String trustClientPath = "/Users/lexy/Desktop/Clases/Seguridad/almacenes/truststoreClient.jceks";
        
        System.setProperty("jdk.security.allowNonCaAnchor", "true" );

        System.setProperty("javax.net.ssl.trustStore", trustClientPath);
        System.setProperty("javax.net.ssl.trustStoreType",     "JCEKS");
        System.setProperty("javax.net.ssl.trustStorePassword", "clientpass");
        serverKeystore = KeyStore.getInstance("JCEKS");
        //System.out.println(passwd_key);
        serverKeystore.load(new FileInputStream(keystoreServerPath),"serverpass".toCharArray());
        trustStore = KeyStore.getInstance("JCEKS");
        //trustedStore.load(new FileInputStream("C:\\Users\\usuario\\Desktop\\alamcenes/clientTrustedCerts.jks"), "clientpass".toCharArray());
        trustStore.load(new FileInputStream(trustClientPath), "clientpass".toCharArray()); //supuestamente no hay que poner contrase�a es la misma pero no se deberia i dont know 
        

        testPKIXParametersRevEnabled(false);
        testPKIXParametersRevEnabled(true);
    } finally {
    }
}

/**
 * Do a basic connection using PKIXParameters with revocation checking
 * enabled and client-side OCSP disabled.  It will only pass if all
 * stapled responses are present, valid and have a GOOD status.
 */

static class ClientParameters {
    boolean enabled = true;
    PKIXBuilderParameters pkixParams = null;
    PKIXRevocationChecker revChecker = null;
    String[] protocols = null;
    String[] ciphers = null;

    ClientParameters() { }
}

static class ServerParameters {
    boolean enabled = true;
    int cacheSize = 256;
    int cacheLifetime = 3600;
    int respTimeout = 5000;
    String respUri = "http://localhost:9999";
    boolean respOverride = false;
    boolean ignoreExts = false;
    String[] protocols = null;
    String[] ciphers = null;

    ServerParameters() { }
}
static void testPKIXParametersRevEnabled(boolean isTls13) throws Exception {
    ClientParameters cliParams = new ClientParameters();
    ServerParameters servParams = new ServerParameters();
    if (isTls13) {
        cliParams.protocols = TLS13ONLY;
        servParams.protocols = TLS13ONLY;
    } else {
        cliParams.protocols = TLS12MAX;
        servParams.protocols = TLS12MAX;
    }
    serverReady = false;

    System.out.println("=====================================");
    System.out.println("Stapling enabled, PKIXParameters with");
    System.out.println("Revocation checking enabled ");
    System.out.println("=====================================");

    cliParams.pkixParams = new PKIXBuilderParameters(trustStore,
            new X509CertSelector());
    cliParams.pkixParams.setRevocationEnabled(true);
    Security.setProperty("ocsp.enable", "false");
    cliParams.enabled=true;
    servParams.enabled=true;
    servParams.respUri="http://localhost:9999";

    SSLSocketWithStapling sslTest = new SSLSocketWithStapling(cliParams,
            servParams);
    TestResult tr = sslTest.getResult();
    if (tr.clientExc != null) {
        throw tr.clientExc;
    } else if (tr.serverExc != null) {
        throw tr.serverExc;
    }

    System.out.println("                PASS");
    System.out.println("=====================================\n");
}

/*
 * Define the server side of the test.
 *
 * If the server prematurely exits, serverReady will be set to true
 * to avoid infinite hangs.
 */
void doServerSide(ServerParameters servParams) throws Exception {

    // Selectively enable or disable the feature
    System.setProperty("jdk.tls.server.enableStatusRequestExtension",
            Boolean.toString(servParams.enabled));

    // Set all the other operating parameters
    System.setProperty("jdk.tls.stapling.cacheSize",
            Integer.toString(servParams.cacheSize));
    System.setProperty("jdk.tls.stapling.cacheLifetime",
            Integer.toString(servParams.cacheLifetime));
    System.setProperty("jdk.tls.stapling.responseTimeout",
            Integer.toString(servParams.respTimeout));
    System.setProperty("jdk.tls.stapling.responderURI", servParams.respUri);
    System.setProperty("jdk.tls.stapling.responderOverride",
            Boolean.toString(servParams.respOverride));
    System.setProperty("jdk.tls.stapling.ignoreExtensions",
            Boolean.toString(servParams.ignoreExts));

    // Set keystores and trust stores for the server
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(serverKeystore, passwd.toCharArray());
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
    tmf.init(trustStore);

    SSLContext sslc = SSLContext.getInstance("TLS");
    sslc.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);

    SSLServerSocketFactory sslssf = new CustomizedServerSocketFactory(sslc,
            servParams.protocols, servParams.ciphers);

    try (SSLServerSocket sslServerSocket =
            (SSLServerSocket) sslssf.createServerSocket(serverPort)) {

        serverPort = sslServerSocket.getLocalPort();

        /*
         * Signal Client, we're ready for his connect.
         */
        serverReady = true;

        try (SSLSocket sslSocket = (SSLSocket) sslServerSocket.accept();
                InputStream sslIS = sslSocket.getInputStream();
                OutputStream sslOS = sslSocket.getOutputStream()) {
            int numberIn = sslIS.read();
            int numberSent = 85;
            log("Server received number: " + numberIn);
            sslOS.write(numberSent);
            sslOS.flush();
            log("Server sent number: " + numberSent);
        }
    }
}

/*
 * Define the client side of the test.
 *
 * If the server prematurely exits, serverReady will be set to true
 * to avoid infinite hangs.
 */
void doClientSide(ClientParameters cliParams) throws Exception {

    // Wait 5 seconds for server ready
    for (int i = 0; (i < 100 && !serverReady); i++) {
        Thread.sleep(50);
    }
    if (!serverReady) {
        throw new RuntimeException("Server not ready yet");
    }

    // Selectively enable or disable the feature
    System.setProperty("jdk.tls.client.enableStatusRequestExtension",
            Boolean.toString(cliParams.enabled));

    // Create the Trust Manager Factory using the PKIX variant
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");

    // If we have a customized pkixParameters then use it
    if (cliParams.pkixParams != null) {
        // LIf we have a customized PKIXRevocationChecker, add
        // it to the PKIXBuilderParameters.
        if (cliParams.revChecker != null) {
            cliParams.pkixParams.addCertPathChecker(cliParams.revChecker);
        }

        ManagerFactoryParameters trustParams =
                new CertPathTrustManagerParameters(cliParams.pkixParams);
        tmf.init(trustParams);
    } else {
        tmf.init(trustStore);
    }

    SSLContext sslc = SSLContext.getInstance("TLS");
    sslc.init(null, tmf.getTrustManagers(), null);

    SSLSocketFactory sslsf = new CustomizedSocketFactory(sslc,
            cliParams.protocols, cliParams.ciphers);
    try (SSLSocket sslSocket = (SSLSocket)sslsf.createSocket("localhost",
            serverPort);
            InputStream sslIS = sslSocket.getInputStream();
            OutputStream sslOS = sslSocket.getOutputStream()) {
        int numberSent = 80;
        sslOS.write(numberSent);
        sslOS.flush();
        log("Client sent number: " + numberSent);
        int numberIn = sslIS.read();
        log("Client received number:" + numberIn);
    }
}

/*
 * Primary constructor, used to drive remainder of the test.
 *
 * Fork off the other side, then do your work.
 */
SSLSocketWithStapling(ClientParameters cliParams,
        ServerParameters servParams) throws Exception {
    Exception startException = null;
    try {
        if (separateServerThread) {
            startServer(servParams, true);
            startClient(cliParams, false);
        } else {
            startClient(cliParams, true);
            startServer(servParams, false);
        }
    } catch (Exception e) {
        startException = e;
    }

    /*
     * Wait for other side to close down.
     */
    if (separateServerThread) {
        if (serverThread != null) {
            serverThread.join();
        }
    } else {
        if (clientThread != null) {
            clientThread.join();
        }
    }
}


TestResult getResult() {
    TestResult tr = new TestResult();
    tr.clientExc = clientException;
    tr.serverExc = serverException;
    return tr;
}

void startServer(ServerParameters servParams, boolean newThread)
        throws Exception {
    if (newThread) {
        serverThread = new Thread() {
            public void run() {
                try {
                    doServerSide(servParams);
                } catch (Exception e) {
                    /*
                     * Our server thread just died.
                     *
                     * Release the client, if not active already...
                     */
                    System.err.println("Server died...");
                    e.printStackTrace(System.err);
                    serverReady = true;
                    serverException = e;
                }
            }
        };
        serverThread.start();
    } else {
        try {
            doServerSide(servParams);
        } catch (Exception e) {
            serverException = e;
        } finally {
            serverReady = true;
        }
    }
}

void startClient(ClientParameters cliParams, boolean newThread)
        throws Exception {
    if (newThread) {
        clientThread = new Thread() {
            public void run() {
                try {
                    doClientSide(cliParams);
                } catch (Exception e) {
                    /*
                     * Our client thread just died.
                     */
                    System.err.println("Client died...");
                    clientException = e;
                }
            }
        };
        clientThread.start();
    } else {
        try {
            doClientSide(cliParams);
        } catch (Exception e) {
            clientException = e;
        }
    }
}

/**
 * Log a message on stdout
 *
 * @param message The message to log
 */
private static void log(String message) {
    if (debug) {
        System.out.println(message);
    }
}

// The following two classes are Simple nested class to group a handful
// of configuration parameters used before starting a client or server.
// We'll just access the data members directly for convenience.


static class CustomizedSocketFactory extends SSLSocketFactory {
    final SSLContext sslc;
    final String[] protocols;
    final String[] cipherSuites;

    CustomizedSocketFactory(SSLContext ctx, String[] prots, String[] suites)
            throws GeneralSecurityException {
        super();
        sslc = (ctx != null) ? ctx : SSLContext.getDefault();
        protocols = prots;
        cipherSuites = suites;
    }

    @Override
    public Socket createSocket(Socket s, String host, int port,
            boolean autoClose) throws IOException {
        Socket sock =  sslc.getSocketFactory().createSocket(s, host, port,
                autoClose);
        customizeSocket(sock);
        return sock;
    }

    @Override
    public Socket createSocket(InetAddress host, int port)
            throws IOException {
        Socket sock = sslc.getSocketFactory().createSocket(host, port);
        customizeSocket(sock);
        return sock;
    }

    @Override
    public Socket createSocket(InetAddress host, int port,
            InetAddress localAddress, int localPort) throws IOException {
        Socket sock = sslc.getSocketFactory().createSocket(host, port,
                localAddress, localPort);
        customizeSocket(sock);
        return sock;
    }

    @Override
    public Socket createSocket(String host, int port)
            throws IOException {
        Socket sock =  sslc.getSocketFactory().createSocket(host, port);
        customizeSocket(sock);
        return sock;
    }

    @Override
    public Socket createSocket(String host, int port,
            InetAddress localAddress, int localPort)
            throws IOException {
        Socket sock =  sslc.getSocketFactory().createSocket(host, port,
                localAddress, localPort);
        customizeSocket(sock);
        return sock;
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return sslc.getDefaultSSLParameters().getCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return sslc.getSupportedSSLParameters().getCipherSuites();
    }

    private void customizeSocket(Socket sock) {
        if (sock instanceof SSLSocket) {
            if (protocols != null) {
                ((SSLSocket)sock).setEnabledProtocols(protocols);
            }
            if (cipherSuites != null) {
                ((SSLSocket)sock).setEnabledCipherSuites(cipherSuites);
            }
        }
    }
}

static class CustomizedServerSocketFactory extends SSLServerSocketFactory {
    final SSLContext sslc;
    final String[] protocols;
    final String[] cipherSuites;

    CustomizedServerSocketFactory(SSLContext ctx, String[] prots, String[] suites)
            throws GeneralSecurityException {
        super();
        sslc = (ctx != null) ? ctx : SSLContext.getDefault();
        protocols = prots;
        cipherSuites = suites;

    }

    @Override
    public ServerSocket createServerSocket(int port) throws IOException {
        ServerSocket sock =
                sslc.getServerSocketFactory().createServerSocket(port);
        customizeSocket(sock);
        return sock;
    }

    @Override
    public ServerSocket createServerSocket(int port, int backlog)
            throws IOException {
        ServerSocket sock =
                sslc.getServerSocketFactory().createServerSocket(port,
                        backlog);
        customizeSocket(sock);
        return sock;
    }

    @Override
    public ServerSocket createServerSocket(int port, int backlog,
            InetAddress ifAddress) throws IOException {
        ServerSocket sock =
                sslc.getServerSocketFactory().createServerSocket(port,
                        backlog, ifAddress);
        customizeSocket(sock);
        return sock;
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return sslc.getDefaultSSLParameters().getCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return sslc.getSupportedSSLParameters().getCipherSuites();
    }

    private void customizeSocket(ServerSocket sock) {
        if (sock instanceof SSLServerSocket) {
            if (protocols != null) {
                ((SSLServerSocket)sock).setEnabledProtocols(protocols);
            }
            if (cipherSuites != null) {
                ((SSLServerSocket)sock).setEnabledCipherSuites(cipherSuites);
            }
        }
    }
}


static class TestResult {
    Exception serverExc = null;
    Exception clientExc = null;
}

}

Solution

  • Problem solved:

    In my case, the server Stapling was not working cause a bad configuration of the server certificate.

    The server certificate must be chained to the root CA certificate, and mine was alone. Also, I specified the authorityInfoAccess extension on the Certificate.

    So:

    My openssl config file for signing the ssl certs look like this (see authorityInfoAccess with the URI of your OCSP):

    [ auth_cert ]
    basicConstraints = CA:FALSE
    subjectKeyIdentifier = hash
    authorityKeyIdentifier = keyid,issuer:always
    keyUsage = critical, digitalSignature, keyEncipherment
    extendedKeyUsage = critical, serverAuth,clientAuth
    authorityInfoAccess = OCSP;URI:http://localhost:9999
    

    And make sure too that your ssl certificate is chained with the CA or CA and intermediate certificates, depending how your setup is.

    If they are in per format, just concatenate them, in linux with the cat tool, the command I used is:

    cat ca.crt.pem >> server/serverauth.crt.pem
    

    For more info about how I realized, check my posts on this coderanch thread.