org.codice.ddf.security.crl.generator.CrlGenerator.java Source code

Java tutorial

Introduction

Here is the source code for org.codice.ddf.security.crl.generator.CrlGenerator.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.crl.generator;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.ByteSource;
import com.google.common.io.FileBackedOutputStream;
import ddf.security.common.audit.SecurityLogger;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.cert.CRL;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.core.Response;
import org.apache.commons.io.IOUtils;
import org.apache.cxf.jaxrs.client.WebClient;
import org.apache.wss4j.common.crypto.Merlin;
import org.codice.ddf.configuration.AbsolutePathResolver;
import org.codice.ddf.cxf.client.ClientFactoryFactory;
import org.codice.ddf.cxf.client.SecureCxfClientFactory;
import org.codice.ddf.platform.util.StandardThreadFactoryBuilder;
import org.codice.ddf.platform.util.properties.PropertiesLoader;
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;

/**
 * The CrlGenerator downloads and saves a Certificate Revocation List from a URL specified by {@code
 * crlLocationUrl}.
 */
public class CrlGenerator implements Runnable {
    private static final Logger LOGGER = LoggerFactory.getLogger(CrlGenerator.class);
    private static final PropertiesLoader PROPERTIES_LOADER = PropertiesLoader.getInstance();
    private static final String HTTPS = "https://";
    private static final int INITIAL_DELAY = 0;
    private static final int SCHEDULER_INTERVAL = 30;
    private static final int SHUTDOWN_TIMEOUT_SECONDS = 60;
    private static final int FILE_BACKED_STREAM_THRESHOLD = 1_000_000;
    static final String CRL_PROPERTY_KEY = Merlin.OLD_PREFIX + Merlin.X509_CRL_FILE;
    static final String DEM_CRL = "/localCrl.crl";
    static final String PEM_CRL = "/localCrl.pem";

    @VisibleForTesting
    static String issuerEncryptionPropertiesLocation = new AbsolutePathResolver(
            "etc/ws-security/issuer/encryption.properties").getPath();

    @VisibleForTesting
    static String issuerSignaturePropertiesLocation = new AbsolutePathResolver(
            "etc/ws-security/issuer/signature.properties").getPath();

    @VisibleForTesting
    static String serverEncryptionPropertiesLocation = new AbsolutePathResolver(
            "etc/ws-security/server/encryption.properties").getPath();

    @VisibleForTesting
    static String serverSignaturePropertiesLocation = new AbsolutePathResolver(
            "etc/ws-security/server/signature.properties").getPath();

    @VisibleForTesting
    static String crlFileLocation = new AbsolutePathResolver("etc/keystores").getPath();

    private final ClientFactoryFactory factory;
    private final EventAdmin eventAdmin;
    private final ScheduledExecutorService scheduler;
    private String crlLocationUrl;
    private boolean crlByUrlEnabled;
    private ScheduledFuture<?> handle;
    private Future<?> removalHandle;

    public CrlGenerator(ClientFactoryFactory factory, EventAdmin eventAdmin) {
        this.factory = factory;
        this.eventAdmin = eventAdmin;
        this.scheduler = Executors
                .newSingleThreadScheduledExecutor(StandardThreadFactoryBuilder.newThreadFactory("crlThread"));
    }

    @VisibleForTesting
    CrlGenerator(ClientFactoryFactory factory, EventAdmin eventAdmin, ScheduledExecutorService scheduler) {
        this.factory = factory;
        this.eventAdmin = eventAdmin;
        this.scheduler = scheduler;
    }

    /** Pulls down the CRL file provided via URL and writes it locally. */
    @Override
    public synchronized void run() {
        if (!crlByUrlEnabled || crlLocationUrl == null) {
            return;
        }

        if (!crlLocationUrl.startsWith(HTTPS)) {
            postErrorEvent("The provided URL was not an HTTPS URL.");
            return;
        }

        Object entity = getRemoteCrl();
        if (!(entity instanceof InputStream)) {
            postErrorEvent("Unable to download the remote CRL.");
            return;
        }

        FileBackedOutputStream fileBackedOutputStream = null;
        try {
            // Read the response content and get the byte source
            ByteSource byteSource;
            try (InputStream entityStream = (InputStream) entity) {
                fileBackedOutputStream = new FileBackedOutputStream(FILE_BACKED_STREAM_THRESHOLD);
                IOUtils.copy(entityStream, fileBackedOutputStream);
                fileBackedOutputStream.close();
                byteSource = fileBackedOutputStream.asByteSource();
            }

            File crlFile = getCrlFile(byteSource);
            // Verify that the CRL is valid
            if (!crlIsValid(byteSource)) {
                postErrorEvent("An error occurred while validating the CRL.");
                return;
            }
            writeCrlToFile(byteSource, crlFile);
        } catch (IOException e) {
            LOGGER.warn("Unable to copy CRL to local CRL. {}", e.getMessage());
            postErrorEvent("An error occurred while downloading the CRL.");
        } finally {
            // Cleanup temp file
            if (fileBackedOutputStream != null) {
                try {
                    fileBackedOutputStream.reset();
                } catch (IOException e) {
                    LOGGER.warn("Error occurred while deleting the temporary file. {}", e.getMessage());
                }
            }
        }
    }

    /**
     * Get the CRL from the given URL.
     *
     * @return - the response body
     */
    private Object getRemoteCrl() {
        SecureCxfClientFactory cxfClientFactory = factory.getSecureCxfClientFactory(crlLocationUrl,
                WebClient.class);
        WebClient client = cxfClientFactory.getWebClient();
        Response response = client.get();
        return response.getEntity();
    }

    /**
     * Determines the CRL encoding and creates a CRL file.
     *
     * @param byteSource - CRL content as a byte source
     * @return - the created file
     * @throws IOException
     */
    private File getCrlFile(ByteSource byteSource) throws IOException {
        // Determine the file extension
        File crlFile;
        try (InputStream inputStream = byteSource.slice(0, 300).openStream()) {
            if (IOUtils.toString(inputStream, StandardCharsets.UTF_8).contains("-----BEGIN")) {
                crlFile = new File(crlFileLocation + PEM_CRL);
            } else {
                crlFile = new File(crlFileLocation + DEM_CRL);
            }
        }
        return crlFile;
    }

    /**
     * Validates the given CRL by attempting to create a {@link CRL}
     *
     * @param byteSource - CRL byte source
     * @return - True if the CRL is valid. False if its invalid
     */
    private boolean crlIsValid(ByteSource byteSource) {
        try (InputStream inputStream = byteSource.openStream()) {
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            certificateFactory.generateCRL(inputStream);
        } catch (CertificateException | CRLException | IOException e) {
            LOGGER.warn("An error occurred while validating the CRL. {}", e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * Writes out the CRL to the given file and adds its path to the property files.
     *
     * @param byteSource - CRL byte source
     * @param crlFile - the file to write the CRL to
     */
    private void writeCrlToFile(ByteSource byteSource, File crlFile) {
        try {
            AccessController.doPrivileged((PrivilegedExceptionAction<Void>) () -> {
                // Write out to file
                try (OutputStream outStream = new FileOutputStream(crlFile);
                        InputStream inputStream = byteSource.openStream()) {
                    IOUtils.copy(inputStream, outStream);
                    SecurityLogger.audit("Copied the content of the CRl at {} to the local CRL at {}.",
                            crlLocationUrl, crlFile.getPath());
                    setCrlFileLocationInPropertiesFile(crlFile.getPath());
                }
                return null;
            });
        } catch (PrivilegedActionException e) {
            LOGGER.warn("Unable to save the CRL.");
            LOGGER.debug("Unable to save the CRL. {}", e.getCause());
            postErrorEvent("Unable to save the CRL.");
        }
    }

    /**
     * Sets the org.apache.ws.security.crypto.merlin.x509crl.file property in the signature.properties
     * and encryption.properties files.
     *
     * @param localCrlPath - CRL file path
     */
    @VisibleForTesting
    void setCrlFileLocationInPropertiesFile(String localCrlPath) {
        addProperty(issuerSignaturePropertiesLocation, localCrlPath);
        addProperty(issuerEncryptionPropertiesLocation, localCrlPath);
        addProperty(serverSignaturePropertiesLocation, localCrlPath);
        addProperty(serverEncryptionPropertiesLocation, localCrlPath);
        SecurityLogger.audit("Setting the {} property to {} as signature and encryption properties.",
                CRL_PROPERTY_KEY, localCrlPath);
    }

    /**
     * Adds the CRL file location to the given property file.
     *
     * @param propertyFilePath - Property file path
     * @param localCrlPath - CRL file path
     */
    private void addProperty(String propertyFilePath, String localCrlPath) {
        Properties properties = PROPERTIES_LOADER.loadPropertiesWithoutSystemPropertySubstitution(propertyFilePath,
                null);
        properties.put(CRL_PROPERTY_KEY, localCrlPath);
        try (OutputStream outputStream = new FileOutputStream(propertyFilePath)) {
            properties.store(outputStream, null);
        } catch (IOException e) {
            LOGGER.warn("Unable to add the {} property to the property file {}.", CRL_PROPERTY_KEY,
                    propertyFilePath);
        }
    }

    /**
     * Removes the org.apache.ws.security.crypto.merlin.x509crl.file property in the
     * signature.properties and encryption.properties files.
     */
    @VisibleForTesting
    void removeCrlFileLocationInPropertiesFile() {
        try {
            AccessController.doPrivileged((PrivilegedExceptionAction<Void>) () -> {
                removeProperty(issuerSignaturePropertiesLocation);
                removeProperty(issuerEncryptionPropertiesLocation);
                removeProperty(serverSignaturePropertiesLocation);
                removeProperty(serverEncryptionPropertiesLocation);
                SecurityLogger.audit("Removing the {} property from signature and encryption properties.",
                        CRL_PROPERTY_KEY);
                return null;
            });
        } catch (PrivilegedActionException e) {
            LOGGER.warn(
                    "Unable to remove the CRL property from the signature.properties and encryption.properties files.");
            LOGGER.debug(
                    "Unable to remove the CRL property from the signature.properties and encryption.properties files. {}",
                    e.getCause());
            postErrorEvent(
                    "Unable to remove the CRL property from the signature.properties and encryption.properties files.");
        }
    }

    /**
     * Removes the CRL file location from the given property file.
     *
     * @param propertyFilePath - Property file path
     */
    private void removeProperty(String propertyFilePath) {
        Properties properties = PROPERTIES_LOADER.loadPropertiesWithoutSystemPropertySubstitution(propertyFilePath,
                null);
        properties.remove(CRL_PROPERTY_KEY);
        try (OutputStream outputStream = new FileOutputStream(propertyFilePath)) {
            properties.store(outputStream, null);
        } catch (IOException e) {
            LOGGER.warn("Unable to remove the {} property to the property file {}.", CRL_PROPERTY_KEY,
                    propertyFilePath);
        }
    }

    /**
     * Sets the URL to download the CRL from and starts a task to download the CRL every 30 minutes.
     *
     * @param crlLocationUrl - CRL's URL location
     */
    public synchronized void setCrlLocationUrl(String crlLocationUrl) {
        this.crlLocationUrl = crlLocationUrl;
        if (handle != null) {
            handle.cancel(false);
        }
        handle = scheduler.scheduleAtFixedRate(this, INITIAL_DELAY, SCHEDULER_INTERVAL, TimeUnit.MINUTES);
    }

    /**
     * Enables or disables the CRL download. If it's disabled, starts a task to remove the CRL file
     * path from property files.
     *
     * @param crlByUrlEnabled - whether the feature is enabled or not
     */
    public synchronized void setCrlByUrlEnabled(boolean crlByUrlEnabled) {
        if (this.crlByUrlEnabled && !crlByUrlEnabled) {
            if (removalHandle != null) {
                removalHandle.cancel(false);
            }
            removalHandle = scheduler.submit(this::removeCrlFileLocationInPropertiesFile);
        }

        this.crlByUrlEnabled = crlByUrlEnabled;
    }

    /** Destroy method to shutdown the scheduler when the configuration is deleted. */
    public void destroy() {
        scheduler.shutdown();
        try {
            if (!scheduler.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
                scheduler.shutdownNow();
                if (!scheduler.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
                    LOGGER.error("CRL thread was unable to terminate successfully.");
                }
            }
        } catch (InterruptedException e) {
            scheduler.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    /**
     * Posts and error message to the Admin Console.
     *
     * @param errorMessage - The reason for the error.
     */
    private void postErrorEvent(String errorMessage) {
        String title = "Unable to download the Certificate Revocation List (CRL) from " + crlLocationUrl;
        Set<String> details = new HashSet<>();
        details.add(
                "The provided CRL could not be downloaded. Please check the provided URL and/or the contents of the given CRL.");
        details.add(errorMessage);
        details.add("To recover, resolve the issue and save the configuration.");
        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);
    }
}