/************************************************************************* * * * 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.configuration.GlobalConfigurationSessionLocal; import org.cesecore.keys.token.CryptoTokenOfflineException; import org.cesecore.util.Base64; import org.cesecore.util.GUIDGenerator; import org.cesecore.util.StringTools; import org.ejbca.config.AvailableProtocolsConfiguration; import org.ejbca.config.AvailableProtocolsConfiguration.AvailableProtocols; 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 29592 2018-08-09 07:50:12Z anatom $ */ 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; @EJB private GlobalConfigurationSessionLocal globalConfigurationSession; @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())); } // This setting is cached and must be cleared on config update OcspConfiguration.clearAcceptedSignatureAlgorithmCache(); 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 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("doPost()"); } if (!protocolEnabled) { log.info("OCSP Protocol is disabled"); response.sendError(HttpServletResponse.SC_FORBIDDEN, "OCSP is disabled"); return; } 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("= (nextUpdate - thisUpdate)) { if (log.isDebugEnabled()) { log.debug("max-age ("+maxAge+") is >= (nextUpdate - thisUpdate): ("+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; } }