org.ejbca.ui.web.protocol.OCSPServlet.java Source code

Java tutorial

Introduction

Here is the source code for org.ejbca.ui.web.protocol.OCSPServlet.java

Source

/*************************************************************************
 *                                                                       *
 *  EJBCA Community: The OpenSource Certificate Authority                *
 *                                                                       *
 *  This software is free software; you can redistribute it and/or       *
 *  modify it under the terms of the GNU Lesser General Public           *
 *  License as published by the Free Software Foundation; either         *
 *  version 2.1 of the License, or any later version.                    *
 *                                                                       *
 *  See terms of license at gnu.org.                                     *
 *                                                                       *
 *************************************************************************/

package org.ejbca.ui.web.protocol;

import java.io.IOException;
import java.net.URLDecoder;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.cert.X509Certificate;
import java.util.Set;

import javax.ejb.EJB;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
import org.cesecore.certificates.certificateprofile.CertificateProfileConstants;
import org.cesecore.certificates.ocsp.OcspResponseGeneratorSessionLocal;
import org.cesecore.certificates.ocsp.OcspResponseInformation;
import org.cesecore.certificates.ocsp.cache.OcspConfigurationCache;
import org.cesecore.certificates.ocsp.exception.MalformedRequestException;
import org.cesecore.certificates.ocsp.logging.AuditLogger;
import org.cesecore.certificates.ocsp.logging.GuidHolder;
import org.cesecore.certificates.ocsp.logging.PatternLogger;
import org.cesecore.certificates.ocsp.logging.TransactionCounter;
import org.cesecore.certificates.ocsp.logging.TransactionLogger;
import org.cesecore.config.ConfigurationHolder;
import org.cesecore.config.OcspConfiguration;
import org.cesecore.keys.token.CryptoTokenOfflineException;
import org.cesecore.util.Base64;
import org.cesecore.util.GUIDGenerator;
import org.ejbca.core.ejb.ocsp.OcspKeyRenewalSessionLocal;
import org.ejbca.core.model.InternalEjbcaResources;
import org.ejbca.ui.web.LimitLengthASN1Reader;
import org.ejbca.util.HTMLTools;
import org.ejbca.util.IPatternLogger;

/** 
 * Servlet implementing server side of the Online Certificate Status Protocol (OCSP)
 * For a detailed description of OCSP refer to RFC2560.
 *
 * @version  $Id: OCSPServlet.java 21089 2015-04-13 13:28:10Z jeklund $
 */
public class OCSPServlet extends HttpServlet {

    private static final long serialVersionUID = 8081630219584820112L;
    private static final Logger log = Logger.getLogger(OCSPServlet.class);
    private static final InternalEjbcaResources intres = InternalEjbcaResources.getInstance();

    private final String sessionID = GUIDGenerator.generateGUID(this);

    private enum HttpMethod {
        GET, POST, OTHER
    };

    @EJB
    private OcspResponseGeneratorSessionLocal integratedOcspResponseGeneratorSession;
    @EJB
    private OcspKeyRenewalSessionLocal ocspKeyRenewalSession;

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        try {
            if (log.isTraceEnabled()) {
                log.trace(">doGet()");
            }
            final String keyRenewalSignerDN = request.getParameter("renewSigner");
            final boolean performKeyRenewal = keyRenewalSignerDN != null && keyRenewalSignerDN.length() > 0;
            // We have a command to force reloading of keys that can only be run from localhost
            final boolean doReload = StringUtils.equals(request.getParameter("reloadkeys"), "true");
            final String newConfig = request.getParameter("newConfig");
            final boolean doNewConfig = newConfig != null && newConfig.length() > 0;
            final boolean doRestoreConfig = request.getParameter("restoreConfig") != null;
            final String remote = request.getRemoteAddr();
            if (doReload || doNewConfig || doRestoreConfig) {
                if (!StringUtils.equals(remote, "127.0.0.1")) {
                    log.info("Got reloadkeys or updateConfig of restoreConfig command from unauthorized ip: "
                            + remote);
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }
            }
            if (doReload) {
                log.info(intres.getLocalizedMessage("ocsp.reloadkeys", remote));
                // Reload CA certificates
                integratedOcspResponseGeneratorSession.reloadOcspSigningCache();
                return;
            }
            if (doNewConfig) {
                final String aConfig[] = newConfig.split("\\|\\|");
                for (int i = 0; i < aConfig.length; i++) {
                    log.debug("Config change: " + aConfig[i]);
                    final int separatorIx = aConfig[i].indexOf('=');
                    if (separatorIx < 0) {
                        ConfigurationHolder.updateConfiguration(aConfig[i], null);
                        continue;
                    }
                    ConfigurationHolder.updateConfiguration(aConfig[i].substring(0, separatorIx),
                            aConfig[i].substring(separatorIx + 1, aConfig[i].length()));
                }
                OcspConfigurationCache.INSTANCE.reloadConfiguration();
                log.info("Call from " + remote + " to update configuration");
                return;
            }
            if (doRestoreConfig) {
                ConfigurationHolder.restoreConfiguration();
                OcspConfigurationCache.INSTANCE.reloadConfiguration();
                log.info("Call from " + remote + " to restore configuration.");
                return;
            }
            if (performKeyRenewal) {
                final Set<String> rekeyingTriggeringHosts = OcspConfiguration.getRekeyingTriggingHosts();
                if (!rekeyingTriggeringHosts.contains(remote)) {
                    log.info(intres.getLocalizedMessage("ocsp.rekey.triggered.unauthorized.ip", remote));
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }
                final String rekeyingTriggingPassword = OcspConfiguration.getRekeyingTriggingPassword();
                if (rekeyingTriggingPassword == null) {
                    log.info(intres.getLocalizedMessage("ocsp.rekey.triggered.not.enabled", remote));
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }
                final String requestPassword = request.getParameter("password");
                final String keyrenewalSignerDn = request.getParameter("renewSigner");
                if (!rekeyingTriggingPassword.equals(requestPassword)) {
                    log.info(intres.getLocalizedMessage("ocsp.rekey.triggered.wrong.password", remote));
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }
                try {
                    ocspKeyRenewalSession.renewKeyStores(keyrenewalSignerDn);
                } catch (KeyStoreException e) {
                    log.info(intres.getLocalizedMessage("ocsp.rekey.keystore.notactivated", remote));
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                } catch (CryptoTokenOfflineException e) {
                    log.info(intres.getLocalizedMessage("ocsp.rekey.cryptotoken.notactivated", remote));
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                } catch (InvalidKeyException e) {
                    log.info(intres.getLocalizedMessage("ocsp.rekey.invalid.key", remote));
                    response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
                    return;
                }
                return;
            }
            processOcspRequest(request, response, HttpMethod.GET);
        } finally {
            if (log.isTraceEnabled()) {
                log.trace("<doGet()");
            }
        }
    }

    @Override
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        if (log.isTraceEnabled()) {
            log.trace(">doPost()");
        }
        try {
            final String contentType = request.getHeader("Content-Type");
            if (contentType != null && contentType.equalsIgnoreCase("application/ocsp-request")) {
                processOcspRequest(request, response, HttpMethod.POST);
                return;
            }
            final String remoteAddr = request.getRemoteAddr();
            // Legacy support for activation using ClientToolBox. We will only use this once for upgrading the installation.
            final String activationPassword = request.getHeader("activate");
            if (activationPassword != null && remoteAddr.equals("127.0.0.1")) {
                try {
                    log.warn("'active' will only be used for initial one-time upgrade."
                            + " Use regular CryptoToken activation in EJB CLI or Admin GUI to active your responder keystores.");
                    integratedOcspResponseGeneratorSession.adhocUpgradeFromPre60(activationPassword.toCharArray());
                } catch (Exception e) {
                    log.error("Problem loading keys.", e);
                    response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
                            "Problem. See ocsp responder server log.");
                }
                return;
            }
            if (contentType != null) {
                final String sError = "Content-type is not application/ocsp-request. It is \'"
                        + HTMLTools.htmlescape(contentType) + "\'.";
                log.debug(sError);
                response.sendError(HttpServletResponse.SC_BAD_REQUEST, sError);
                return;
            }
            if (!remoteAddr.equals("127.0.0.1")) {
                final String sError = "You have connected from \'" + remoteAddr
                        + "\'. You may only connect from 127.0.0.1";
                log.debug(sError);
                response.sendError(HttpServletResponse.SC_UNAUTHORIZED, sError);
                return;
            }
        } finally {
            if (log.isTraceEnabled()) {
                log.trace("<doPost()");
            }
        }
    }

    private void processOcspRequest(HttpServletRequest request, HttpServletResponse response,
            final HttpMethod httpMethod) throws ServletException {
        final String remoteAddress = request.getRemoteAddr();
        final String remoteHost = request.getRemoteHost();
        final StringBuffer requestUrl = request.getRequestURL();
        final int localTransactionId = TransactionCounter.INSTANCE.getTransactionNumber();
        // Create the transaction logger for this transaction.
        TransactionLogger transactionLogger = new TransactionLogger(localTransactionId,
                GuidHolder.INSTANCE.getGlobalUid(), remoteAddress);
        // Create the audit logger for this transaction.
        AuditLogger auditLogger = new AuditLogger("", localTransactionId, GuidHolder.INSTANCE.getGlobalUid(),
                remoteAddress);
        try {
            if (auditLogger.isEnabled()) {
                auditLogger.paramPut(PatternLogger.LOG_ID, Integer.valueOf(localTransactionId));
                auditLogger.paramPut(PatternLogger.SESSION_ID, sessionID);
                auditLogger.paramPut(PatternLogger.CLIENT_IP, remoteAddress);
            }
            if (transactionLogger.isEnabled()) {
                transactionLogger.paramPut(PatternLogger.LOG_ID, Integer.valueOf(localTransactionId));
                transactionLogger.paramPut(PatternLogger.SESSION_ID, sessionID);
                transactionLogger.paramPut(PatternLogger.CLIENT_IP, remoteAddress);
            }
            OCSPRespBuilder responseGenerator = new OCSPRespBuilder();
            OcspResponseInformation ocspResponseInformation = null;
            try {
                byte[] requestBytes = checkAndGetRequestBytes(request, httpMethod);
                X509Certificate[] requestCertificates = (X509Certificate[]) request
                        .getAttribute("javax.servlet.request.X509Certificate");
                ocspResponseInformation = integratedOcspResponseGeneratorSession.getOcspResponse(requestBytes,
                        requestCertificates, remoteAddress, remoteHost, requestUrl, auditLogger, transactionLogger);
            } catch (MalformedRequestException e) {
                if (transactionLogger.isEnabled()) {
                    transactionLogger.paramPut(PatternLogger.PROCESS_TIME, PatternLogger.PROCESS_TIME);
                }
                if (auditLogger.isEnabled()) {
                    auditLogger.paramPut(PatternLogger.PROCESS_TIME, PatternLogger.PROCESS_TIME);
                }
                String errMsg = intres.getLocalizedMessage("ocsp.errorprocessreq", e.getMessage());
                log.info(errMsg);
                if (log.isDebugEnabled()) {
                    log.debug(errMsg, e);
                }
                // RFC 2560: responseBytes are not set on error.
                ocspResponseInformation = new OcspResponseInformation(
                        responseGenerator.build(OCSPRespBuilder.MALFORMED_REQUEST, null),
                        OcspConfiguration.getMaxAge(CertificateProfileConstants.CERTPROFILE_NO_PROFILE));
                if (transactionLogger.isEnabled()) {
                    transactionLogger.paramPut(TransactionLogger.STATUS, OCSPRespBuilder.MALFORMED_REQUEST);
                    transactionLogger.writeln();
                }
                if (auditLogger.isEnabled()) {
                    auditLogger.paramPut(AuditLogger.STATUS, OCSPRespBuilder.MALFORMED_REQUEST);
                }
            } catch (Throwable e) { // NOPMD, we really want to catch everything here to return internal error on unexpected errors
                if (transactionLogger.isEnabled()) {
                    transactionLogger.paramPut(IPatternLogger.PROCESS_TIME, IPatternLogger.PROCESS_TIME);
                }
                if (auditLogger.isEnabled()) {
                    auditLogger.paramPut(IPatternLogger.PROCESS_TIME, IPatternLogger.PROCESS_TIME);
                }
                final String errMsg = intres.getLocalizedMessage("ocsp.errorprocessreq", e.getMessage());
                log.info(errMsg);
                if (log.isDebugEnabled()) {
                    log.debug(errMsg, e);
                }
                // RFC 2560: responseBytes are not set on error.
                ocspResponseInformation = new OcspResponseInformation(
                        responseGenerator.build(OCSPRespBuilder.INTERNAL_ERROR, null),
                        OcspConfiguration.getMaxAge(CertificateProfileConstants.CERTPROFILE_NO_PROFILE));
                if (transactionLogger.isEnabled()) {
                    transactionLogger.paramPut(TransactionLogger.STATUS, OCSPRespBuilder.INTERNAL_ERROR);
                    transactionLogger.writeln();
                }
                if (auditLogger.isEnabled()) {
                    auditLogger.paramPut(AuditLogger.STATUS, OCSPRespBuilder.INTERNAL_ERROR);
                }
            }
            byte[] ocspResponseBytes = ocspResponseInformation.getOcspResponse();
            response.setContentType("application/ocsp-response");
            response.setContentLength(ocspResponseBytes.length);
            if (HttpMethod.GET.equals(httpMethod)) {
                addRfc5019CacheHeaders(request, response, ocspResponseInformation);
            } else {
                if (log.isDebugEnabled()) {
                    log.debug(
                            "Will not add RFC 5019 cache headers: \"clients MUST use the GET method (to enable OCSP response caching)\"");
                }
            }
            response.getOutputStream().write(ocspResponseBytes);
            response.getOutputStream().flush();
        } catch (Exception e) {
            log.error("", e);
            transactionLogger.flush();
            auditLogger.flush();
        }
    }

    /**
     * RFC 2560 does not specify how cache headers should be used, but RFC 5019 does. Therefore we will only
     * add the headers if the requirements of RFC 5019 is fulfilled: A GET-request, a single embedded reponse,
     * the response contains a nextUpdate and no nonce is present.
     * @param maxAge is the margin to Expire when using max-age in milliseconds 
     * @throws org.bouncycastle.cert.ocsp.OCSPException 
     */
    private void addRfc5019CacheHeaders(HttpServletRequest request, HttpServletResponse response,
            OcspResponseInformation ocspResponseInformation)
            throws IOException, org.bouncycastle.cert.ocsp.OCSPException {
        if (!ocspResponseInformation.shouldAddCacheHeaders()) {
            return;
        }
        if (ocspResponseInformation.getMaxAge() <= 0) {
            if (log.isDebugEnabled()) {
                log.debug(
                        "Will not add RFC 5019 cache headers: RFC 5019 6.2: max-age should be 'later than thisUpdate but earlier than nextUpdate'.");
            }
            return;
        }
        final long now = System.currentTimeMillis();
        final long thisUpdate = ocspResponseInformation.getThisUpdate();
        final long nextUpdate = ocspResponseInformation.getNextUpdate();
        // RFC 5019 6.2: Date: The date and time at which the OCSP server generated the HTTP response.
        // On JBoss AS the "Date"-header is cached for 1 second, so this value will be overwritten and off by up to a second 
        response.setDateHeader("Date", now);
        // RFC 5019 6.2: Last-Modified: date and time at which the OCSP responder last modified the response. == thisUpdate
        response.setDateHeader("Last-Modified", thisUpdate);
        // RFC 5019 6.2: Expires: This date and time will be the same as the nextUpdate timestamp in the OCSP response itself.
        response.setDateHeader("Expires", nextUpdate); // This is overridden by max-age on HTTP/1.1 compatible components
        // RFC 5019 6.2: This profile RECOMMENDS that the ETag value be the ASCII HEX representation of the SHA1 hash of the OCSPResponse structure.
        response.setHeader("ETag", "\"" + ocspResponseInformation.getResponseHeader() + "\"");
        if (ocspResponseInformation.isExplicitNoCache()) {
            // Note that using no-cache here is not conforming to RFC5019, but with more recent CABForum discussions it seems RFC5019 will not
            // be followed, or will be changed. (See ECA-3289)
            response.setHeader("Cache-Control", "no-cache, must-revalidate"); //HTTP 1.1
            response.setHeader("Pragma", "no-cache"); //HTTP 1.0 
        } else {
            // Max age is retrieved in milliseconds, but it must be in seconds in the cache-control header
            long maxAge = ocspResponseInformation.getMaxAge();
            if (maxAge >= (nextUpdate - thisUpdate)) {
                maxAge = nextUpdate - thisUpdate - 1;
                log.warn(intres.getLocalizedMessage("ocsp.shrinkmaxage", maxAge));
            }
            response.setHeader("Cache-Control",
                    "max-age=" + (maxAge / 1000L) + ",public,no-transform,must-revalidate");
        }
    }

    /**
     * Reads the request bytes and verifies min and max size of the request. If an error occurs it throws a MalformedRequestException. 
     * Can get request bytes both from a HTTP GET and POST request
     * 
     * @param request
     * @param response
     * @return the request bytes or null if an error occured.
     * @throws IOException In case there is no stream to read
     * @throws MalformedRequestException 
     */
    private byte[] checkAndGetRequestBytes(HttpServletRequest request, HttpMethod httpMethod)
            throws IOException, MalformedRequestException {
        final byte[] ret;
        // Get the request data
        final int n = request.getContentLength();
        // Expect n might be -1 for HTTP GET requests
        if (log.isDebugEnabled()) {
            log.debug(">checkAndGetRequestBytes. Received " + httpMethod.name() + " request with content length: "
                    + n + " from " + request.getRemoteAddr());
        }
        if (n > LimitLengthASN1Reader.MAX_REQUEST_SIZE) {
            String msg = intres.getLocalizedMessage("ocsp.toolarge", LimitLengthASN1Reader.MAX_REQUEST_SIZE, n);
            log.info(msg);
            throw new MalformedRequestException(msg);
        }
        // So we passed basic tests, now we can read the bytes, but still keep an eye on the size
        // we can not fully trust the sent content length.
        if (HttpMethod.POST.equals(httpMethod)) {
            final ServletInputStream in = request.getInputStream(); // ServletInputStream does not have to be closed, container handles this
            LimitLengthASN1Reader limitLengthASN1Reader = new LimitLengthASN1Reader(in, n);
            try {
                ret = limitLengthASN1Reader.readFirstASN1Object();
                if (n > ret.length) {
                    // The client is sending more data than the OCSP request. It might be slightly broken or trying to bog down the server on purpose.
                    // In the interest of not breaking existing systems that might have slightly broken clients we just log for a warning for now.
                    String msg = intres.getLocalizedMessage("ocsp.additionaldata", ret.length, n);
                    log.warn(msg);
                }
            } finally {
                limitLengthASN1Reader.close();
            }
        } else if (HttpMethod.GET.equals(httpMethod)) {
            // GET request
            final StringBuffer url = request.getRequestURL();
            // RFC2560 A.1.1 says that request longer than 255 bytes SHOULD be sent by POST, we support GET for longer requests anyway.
            if (url.length() <= LimitLengthASN1Reader.MAX_REQUEST_SIZE) {
                final String decodedRequest;
                try {
                    // We have to extract the pathInfo manually, to avoid multiple slashes being converted to a single
                    // According to RFC 2396 2.2 chars only have to encoded if they conflict with the purpose, so
                    // we can for example expect both '/' and "%2F" in the request.
                    final String fullServletpath = request.getContextPath() + request.getServletPath();
                    final int paramIx = Math.max(url.indexOf(fullServletpath), 0) + fullServletpath.length() + 1;
                    final String requestString = paramIx < url.length() ? url.substring(paramIx) : "";
                    decodedRequest = URLDecoder.decode(requestString, "UTF-8").replaceAll(" ", "+");
                } catch (Exception e) {
                    String msg = intres.getLocalizedMessage("ocsp.badurlenc");
                    log.info(msg);
                    throw new MalformedRequestException(e);
                }
                if (decodedRequest != null && decodedRequest.length() > 0) {
                    if (log.isDebugEnabled()) {
                        // Don't log the request if it's too long, we don't want to cause denial of service by filling log files or buffers.
                        if (decodedRequest.length() < 2048) {
                            log.debug("decodedRequest: " + decodedRequest);
                        } else {
                            log.debug("decodedRequest too long to log: " + decodedRequest.length());
                        }
                    }
                    try {
                        ret = Base64.decode(decodedRequest.getBytes());
                    } catch (Exception e) {
                        String msg = intres.getLocalizedMessage("ocsp.badurlenc");
                        log.info(msg);
                        throw new MalformedRequestException(e);
                    }
                } else {
                    String msg = intres.getLocalizedMessage("ocsp.missingreq");
                    log.info(msg);
                    throw new MalformedRequestException(msg);
                }
            } else {
                String msg = intres.getLocalizedMessage("ocsp.toolarge", LimitLengthASN1Reader.MAX_REQUEST_SIZE,
                        url.length());
                log.info(msg);
                throw new MalformedRequestException(msg);
            }
        } else {
            // Strange, an unknown method
            String msg = intres.getLocalizedMessage("ocsp.unknownmethod", request.getMethod());
            log.info(msg);
            throw new MalformedRequestException(msg);
        }
        // Make a final check that we actually received something
        if (ret == null || ret.length == 0) {
            String msg = intres.getLocalizedMessage("ocsp.emptyreq", request.getRemoteAddr());
            log.info(msg);
            throw new MalformedRequestException(msg);
        }
        return ret;
    }
}