org.codice.ddf.security.ocsp.checker.OcspChecker.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.security.ocsp.checker.OcspChecker.java

Source

/**
 * Copyright (c) Codice Foundation
 *
 * <p>This 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 3 of
 * the License, or any later version.
 *
 * <p>This program 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 Lesser General Public License for more details. A copy of the GNU Lesser General Public
 * License is distributed along with this program and can be found at
 * <http://www.gnu.org/licenses/lgpl.html>.
 */
package org.codice.ddf.security.ocsp.checker;

import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

import com.google.common.annotations.VisibleForTesting;
import ddf.security.SecurityConstants;
import ddf.security.common.audit.SecurityLogger;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.AccessController;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.Nullable;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.core.Response;
import org.apache.cxf.jaxrs.client.WebClient;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.RevokedStatus;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.cert.ocsp.UnknownStatus;
import org.bouncycastle.operator.DigestCalculator;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.bouncycastle.x509.extension.X509ExtensionUtil;
import org.codice.ddf.cxf.client.ClientFactoryFactory;
import org.codice.ddf.cxf.client.SecureCxfClientFactory;
import org.codice.ddf.security.OcspService;
import org.codice.ddf.system.alerts.NoticePriority;
import org.codice.ddf.system.alerts.SystemNotice;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OcspChecker implements OcspService {
    private static final Logger LOGGER = LoggerFactory.getLogger(OcspChecker.class);
    private static final String NOT_VERIFIED_MSG = " The certificate status could not be verified.";
    private static final String CONTINUING_MSG = " Continuing OCSP check.";

    private final ClientFactoryFactory factory;
    private final EventAdmin eventAdmin;

    private boolean ocspEnabled; // metatype value
    private List<String> ocspServerUrls; // metatype value

    public OcspChecker(ClientFactoryFactory factory, EventAdmin eventAdmin) {
        this.factory = factory;
        this.eventAdmin = eventAdmin;
    }

    /**
     * Checks whether the given {@param certs} are revoked or not against the configured OCSP server
     * urls + the optionally given OCSP server url in the given {@param certs}.
     *
     * @param certs - an array of certificates to verify.
     * @return true if the certificates are good or if they could not be properly checked against the
     *     OCSP server. Returns false if any of them are revoked.
     */
    @Override
    public boolean passesOcspCheck(X509Certificate[] certs) {
        if (!ocspEnabled) {
            LOGGER.debug("OCSP check is not enabled. Skipping.");
            return true;
        }

        for (X509Certificate cert : certs) {
            try {
                Certificate certificate = convertToBouncyCastleCert(cert);
                OCSPReq ocspRequest = generateOcspRequest(certificate);
                Map<String, CertificateStatus> ocspStatuses = sendOcspRequests(cert, ocspRequest);
                String revokedStatusUrl = getFirstRevokedStatusUrl(ocspStatuses);
                if (revokedStatusUrl != null) {
                    SecurityLogger.audit("Certificate {} has been revoked by the OCSP server at URL {}.", cert,
                            revokedStatusUrl);
                    LOGGER.warn("Certificate {} has been revoked by the OCSP server at URL {}.", cert,
                            revokedStatusUrl);
                    return false;
                }
            } catch (OcspCheckerException e) {
                postErrorEvent(e.getMessage());
            }
        }
        // If an error occurred, the certificates will not be validated and will be permitted.
        // An alert will be posted to the admin console.
        return true;
    }

    /**
     * Converts a {@link java.security.cert.X509Certificate} to a {@link Certificate}.
     *
     * @param cert - the X509Certificate to convert.
     * @return a {@link Certificate}.
     * @throws OcspCheckerException after posting an alert to the admin console, if any error occurs.
     */
    @VisibleForTesting
    Certificate convertToBouncyCastleCert(X509Certificate cert) throws OcspCheckerException {
        try {
            byte[] data = cert.getEncoded();
            return Certificate.getInstance(data);
        } catch (CertificateEncodingException e) {
            throw new OcspCheckerException(
                    "Unable to convert X509 certificate to a Bouncy Castle certificate." + NOT_VERIFIED_MSG, e);
        }
    }

    /**
     * Creates an {@link OCSPReq} to send to the OCSP server for the given certificate.
     *
     * @param cert - the certificate to verify
     * @return the created OCSP request
     * @throws OcspCheckerException after posting an alert to the admin console, if any error occurs
     */
    @VisibleForTesting
    OCSPReq generateOcspRequest(Certificate cert) throws OcspCheckerException {
        try {
            X509CertificateHolder issuerCert = resolveIssuerCertificate(cert);

            JcaDigestCalculatorProviderBuilder digestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder();
            DigestCalculatorProvider digestCalculatorProvider = digestCalculatorProviderBuilder.build();
            DigestCalculator digestCalculator = digestCalculatorProvider.get(CertificateID.HASH_SHA1);

            CertificateID certId = new CertificateID(digestCalculator, issuerCert,
                    cert.getSerialNumber().getValue());

            OCSPReqBuilder ocspReqGenerator = new OCSPReqBuilder();
            ocspReqGenerator.addRequest(certId);
            return ocspReqGenerator.build();

        } catch (OCSPException | OperatorCreationException e) {
            throw new OcspCheckerException("Unable to create an OCSP request." + NOT_VERIFIED_MSG, e);
        }
    }

    /**
     * Returns an {@link X509CertificateHolder} containing the issuer of the passed in {@param cert}.
     * Search is performed in the system truststore.
     *
     * @param cert - the {@link Certificate} to get the issuer from.
     * @return {@link X509CertificateHolder} containing the issuer of the passed in {@param cert}.
     * @throws OcspCheckerException if the issuer cannot be resolved.
     */
    private X509CertificateHolder resolveIssuerCertificate(Certificate cert) throws OcspCheckerException {
        X500Name issuerName = cert.getIssuer();

        String trustStorePath = AccessController
                .doPrivileged((PrivilegedAction<String>) SecurityConstants::getTruststorePath);
        String trustStorePass = AccessController
                .doPrivileged((PrivilegedAction<String>) SecurityConstants::getTruststorePassword);

        if (isBlank(trustStorePath) || isBlank(trustStorePass)) {
            throw new OcspCheckerException("Problem retrieving truststore properties." + NOT_VERIFIED_MSG);
        }

        KeyStore truststore;

        try (InputStream truststoreInputStream = new FileInputStream(trustStorePath)) {
            truststore = SecurityConstants.newTruststore();
            truststore.load(truststoreInputStream, trustStorePass.toCharArray());
            SecurityLogger.audit("Truststore on path {} was read by {}.", trustStorePath,
                    this.getClass().getSimpleName());
        } catch (CertificateException | IOException | KeyStoreException | NoSuchAlgorithmException e) {
            throw new OcspCheckerException(String.format("Problem loading truststore on path %s", trustStorePath),
                    e);
        }

        try {
            return getCertFromTruststoreWithX500Name(issuerName, truststore);
        } catch (OcspCheckerException e) {
            throw new OcspCheckerException(
                    "Problem finding the certificate issuer in truststore." + NOT_VERIFIED_MSG, e);
        }
    }

    /**
     * Returns an {@link X509CertificateHolder} containing the issuer of the given {@param name}.
     * Search is performed in the given {@param truststore}.
     *
     * @param name - the {@link X500Name} of the issuer.
     * @param truststore - the {@link KeyStore} to check.
     * @return {@link X509CertificateHolder} of the certificate with the given {@param name}.
     * @throws OcspCheckerException if the {@param name} cannot be found in the {@param truststore}.
     */
    private X509CertificateHolder getCertFromTruststoreWithX500Name(X500Name name, KeyStore truststore)
            throws OcspCheckerException {
        Enumeration<String> aliases;

        try {
            aliases = truststore.aliases();
        } catch (KeyStoreException e) {
            throw new OcspCheckerException("Problem getting aliases from truststore." + NOT_VERIFIED_MSG, e);
        }

        while (aliases.hasMoreElements()) {
            String currentAlias = aliases.nextElement();

            try {
                java.security.cert.Certificate currentCert = truststore.getCertificate(currentAlias);
                X509CertificateHolder currentCertHolder = new X509CertificateHolder(currentCert.getEncoded());
                X500Name currentName = currentCertHolder.getSubject();
                if (name.equals(currentName)) {
                    return currentCertHolder;
                }
            } catch (CertificateEncodingException | IOException | KeyStoreException e) {
                LOGGER.debug("Problem loading truststore certificate." + CONTINUING_MSG, e);
            }
        }

        throw new OcspCheckerException(
                String.format("Could not find cert matching X500Name of %s.", name) + NOT_VERIFIED_MSG);
    }

    /**
     * Sends the {@param ocspReq} request to all configured {@code cspServerUrls} & the OCSP server
     * urls optionally given in the given {@param cert}.
     *
     * @param cert - the {@link X509Certificate} to check.
     * @param ocspRequest - the {@link OCSPReq} to send.
     * @return a {@link List} of {@link OCSPResp}s for every configured {@code ocspServerUrls} & the
     *     OCSP server * urls optionally given in the given {@param cert}. Problematic responses are
     *     represented as null values.
     */
    @VisibleForTesting
    Map<String, CertificateStatus> sendOcspRequests(X509Certificate cert, OCSPReq ocspRequest) {
        Set<String> urlsToCheck = new HashSet<>();
        if (ocspServerUrls != null) {
            urlsToCheck.addAll(ocspServerUrls);
        }

        // try and pull an OCSP server url off of the cert
        urlsToCheck.addAll(getOcspUrlsFromCert(cert));

        Map<String, CertificateStatus> ocspStatuses = new HashMap<>();

        for (String ocspServerUrl : urlsToCheck) {
            if (isNotBlank(ocspServerUrl)) {
                try {
                    SecureCxfClientFactory cxfClientFactory = factory.getSecureCxfClientFactory(ocspServerUrl,
                            WebClient.class);
                    WebClient client = cxfClientFactory.getWebClient().accept("application/ocsp-response")
                            .type("application/ocsp-request");

                    Response response = client.post(ocspRequest.getEncoded());
                    OCSPResp ocspResponse = createOcspResponse(response);
                    ocspStatuses.put(ocspServerUrl, getStatusFromOcspResponse(ocspResponse));
                    continue;
                } catch (IOException | OcspCheckerException | ProcessingException e) {
                    LOGGER.debug("Problem with the response from the OCSP Server at URL {}." + CONTINUING_MSG,
                            ocspServerUrl, e);
                }
            }
            ocspStatuses.put(ocspServerUrl, new UnknownStatus()); // if ocspServerUrl is null or if there was an exception
        }

        return ocspStatuses;
    }

    /**
     * Attempts to grab additional OCSP server urls off of the given {@param cert}.
     *
     * @param - the {@link X509Certificate} to check.
     * @return {@link List} of additional OCSP server urls found on the given {@param cert}.
     */
    private List<String> getOcspUrlsFromCert(X509Certificate cert) {
        List<String> ocspUrls = new ArrayList<>();

        try {
            byte[] authorityInfoAccess = cert.getExtensionValue(Extension.authorityInfoAccess.getId());

            if (authorityInfoAccess == null) {
                return ocspUrls;
            }

            AuthorityInformationAccess authorityInformationAccess = AuthorityInformationAccess
                    .getInstance(X509ExtensionUtil.fromExtensionValue(authorityInfoAccess));

            if (authorityInformationAccess == null) {
                return ocspUrls;
            }

            for (AccessDescription description : authorityInformationAccess.getAccessDescriptions()) {
                GeneralName accessLocation = description.getAccessLocation();
                if (accessLocation.getTagNo() == GeneralName.uniformResourceIdentifier)
                    ocspUrls.add(((DERIA5String) accessLocation.getName()).getString());
            }
        } catch (IOException e) {
            LOGGER.debug("Problem retrieving the OCSP server url(s) from the certificate." + CONTINUING_MSG, e);
        }

        return ocspUrls;
    }

    /**
     * Creates a {@link OCSPResp} from the given {@param response}.
     *
     * @param response - the {@link Response} to convert.
     * @return an {@link OCSPResp} of the given {@param response}.
     * @throws OcspCheckerException if any error occurs.
     */
    private OCSPResp createOcspResponse(Response response) throws OcspCheckerException {
        Object entity = response.getEntity();

        if (!(entity instanceof InputStream)) {
            throw new OcspCheckerException("Response did not contain an entity of type InputStream.");
        }

        try (InputStream inputStream = (InputStream) entity) {
            return new OCSPResp(inputStream);
        } catch (IOException e) {
            throw new OcspCheckerException("Problem converting the HTTP Response to an OCSPResp.", e);
        }
    }

    /**
     * Gets the {@link CertificateStatus} from the given {@param ocspResponse}.
     *
     * @param ocspResponse - the {@link OCSPResp} to get the {@link CertificateStatus} from.
     * @return the {@link CertificateStatus} from the given {@param ocspResponse}. Returns an {@link
     *     UnknownStatus} if the status could not be found.
     */
    private CertificateStatus getStatusFromOcspResponse(OCSPResp ocspResponse) {
        try {
            BasicOCSPResp basicResponse = (BasicOCSPResp) ocspResponse.getResponseObject();

            if (basicResponse == null) {
                return new UnknownStatus();
            }

            SingleResp[] singleResps = basicResponse.getResponses();
            if (singleResps == null) {
                return new UnknownStatus();
            }

            SingleResp response = Arrays.stream(singleResps).findFirst().orElse(null);
            if (response == null) {
                return new UnknownStatus();
            }

            return response.getCertStatus();

        } catch (OCSPException e) {
            return new UnknownStatus();
        }
    }

    /**
     * Check if any {@link CertificateStatus} in the given {@param ocspStatuses} are revoked.
     *
     * @param ocspStatuses - a {@link Map} of OCSP URLs and their respective {@link
     *     CertificateStatus}.
     * @return the URL of the first revoked status, or null if no revoked status was found.
     */
    private @Nullable String getFirstRevokedStatusUrl(Map<String, CertificateStatus> ocspStatuses) {
        return ocspStatuses.entrySet().stream().filter(entry -> entry.getValue() instanceof RevokedStatus)
                .map(Entry::getKey).findFirst().orElse(null);
    }

    /**
     * Posts and error message to the Admin Console.
     *
     * @param errorMessage - The reason for the error.
     */
    private void postErrorEvent(String errorMessage) {
        String title = "Problem checking the revocation status of the Certificate through OCSP.";
        Set<String> details = new HashSet<>();
        details.add(
                "An error occurred while checking the revocation status of a Certificate against an Online Certificate Status Protocol (OCSP) server. "
                        + "Please resolve the error to resume validating certificates against the OCSP server.");
        details.add(errorMessage);
        eventAdmin.postEvent(new Event(SystemNotice.SYSTEM_NOTICE_BASE_TOPIC + "crl",
                new SystemNotice(this.getClass().getName(), NoticePriority.CRITICAL, title, details)
                        .getProperties()));
        SecurityLogger.audit(title);
        SecurityLogger.audit(errorMessage);
        LOGGER.debug(errorMessage);
    }

    public void setOcspEnabled(boolean ocspEnabled) {
        this.ocspEnabled = ocspEnabled;
    }

    public void setOcspServerUrls(List<String> ocspServerUrls) {
        this.ocspServerUrls = ocspServerUrls;
    }

    /**
     * Custom exception usually thrown after an unexpected error occurred while validating a
     * certificate. An alert should be posted to the admin console first.
     */
    class OcspCheckerException extends Exception {
        public OcspCheckerException() {
            super();
        }

        public OcspCheckerException(Exception cause) {
            super(cause);
        }

        public OcspCheckerException(String msg) {
            super(msg);
        }

        public OcspCheckerException(String msg, Exception cause) {
            super(msg, cause);
        }
    }
}