org.signserver.client.cli.validationservice.ValidateCertificateCommand.java Source code

Java tutorial

Introduction

Here is the source code for org.signserver.client.cli.validationservice.ValidateCertificateCommand.java

Source

/*************************************************************************
 *                                                                       *
 *  SignServer: The OpenSource Automated Signing Server                  *
 *                                                                       *
 *  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.signserver.client.cli.validationservice;

import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import javax.net.ssl.SSLSocketFactory;
import org.apache.commons.cli.*;
import org.ejbca.util.CertTools;
import org.signserver.cli.CommandLineInterface;
import org.signserver.cli.spi.AbstractCommand;
import org.signserver.cli.spi.CommandFailureException;
import org.signserver.cli.spi.IllegalCommandArgumentsException;
import org.signserver.common.RequestAndResponseManager;
import org.signserver.common.SignServerUtil;
import org.signserver.protocol.ws.ProcessRequestWS;
import org.signserver.protocol.ws.ProcessResponseWS;
import org.signserver.protocol.ws.client.*;
import org.signserver.validationservice.common.ValidateRequest;
import org.signserver.validationservice.common.ValidateResponse;
import org.signserver.validationservice.common.Validation;
import org.signserver.validationservice.common.Validation.Status;

/**
 * TODO: Document!
 * 
 * @author Philip Vendil 13 sep 2008
 *
 * @version $Id: ValidateCertificateCommand.java 3706 2013-08-16 12:16:00Z malu9369 $
 */
public class ValidateCertificateCommand extends AbstractCommand {

    private static final int DEFAULT_PORT = 8080;
    private static final int DEFAULT_SSLPORT = 8442;

    /** System-specific new line characters. **/
    private static final String NL = System.getProperty("line.separator");

    /** The name of this command. */
    private static final String COMMAND = "validatecertificate";

    public static final String OPTION_HELP = "help";
    public static final String OPTION_SERVICE = "service";
    public static final String OPTION_CERT = "cert";
    public static final String OPTION_SILENT = "silent";
    public static final String OPTION_PEM = "pem";
    public static final String OPTION_DER = "der";
    public static final String OPTION_HOSTS = "hosts";
    public static final String OPTION_PORT = "port";
    public static final String OPTION_CERTPURPOSES = "certpurposes";
    public static final String OPTION_TRUSTSTORE = "truststore";
    public static final String OPTION_TRUSTSTOREPWD = "truststorepwd";
    public static final String OPTION_SERVLET = "servlet";
    public static final String OPTION_PROTOCOL = "protocol";

    public static final int RETURN_ERROR = CommandLineInterface.RETURN_ERROR;
    public static final int RETURN_BADARGUMENT = CommandLineInterface.RETURN_INVALID_ARGUMENTS;
    public static final int RETURN_VALID = 0;
    public static final int RETURN_REVOKED = 1;
    public static final int RETURN_NOTYETVALID = 2;
    public static final int RETURN_EXPIRED = 3;
    public static final int RETURN_DONTVERIFY = 4;
    public static final int RETURN_CAREVOKED = 5;
    public static final int RETURN_CANOTYETVALID = 6;
    public static final int RETURN_CAEXPIRED = 7;
    public static final int RETURN_BADCERTPURPOSE = 8;

    public static final String CRLF = "\r\n";
    private static final String BOUNDARY = "------------------signserver";

    /**
     * Protocols that can be used for accessing SignServer.
     */
    public static enum Protocol {
        /** The Web Services interface. */
        WEBSERVICES,
        /** HTTP servlet protocol. */
        HTTP,
    }

    private boolean pemFlag = false;
    private boolean derFlag = false;
    private boolean silentMode = false;
    private String[] hosts = null;
    private int port = DEFAULT_PORT;
    private File certPath = null;
    private String trustStorePath = null;
    private String trustStorePwd = null;
    private boolean useSSL = false;
    private String usages = null;
    private String service = null;
    private Protocol protocol;

    Options options = new Options();
    private String servlet;

    public ValidateCertificateCommand() {
        Option help = new Option(OPTION_HELP, false, "Display this info");
        Option silent = new Option(OPTION_SILENT, false, "Don't produce any output, only return value.");
        Option pem = new Option(OPTION_PEM, false, "Certificate is in PEM format (Default).");
        Option der = new Option(OPTION_DER, false, "Certificate is in DER format.");

        OptionBuilder.withArgName("service-name");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription("The name or id of the validation service to process request. (Required)");
        Option serviceOption = OptionBuilder.create(OPTION_SERVICE);

        OptionBuilder.withArgName("cert-file");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription("Path to certificate file (DER or PEM) (Required).");
        Option certOption = OptionBuilder.create(OPTION_CERT);

        OptionBuilder.withArgName("hosts");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription(
                "A ',' separated string containing the hostnames of the validation service nodes. Ex 'host1.someorg.org,host2.someorg.org'. When using the HTTP protocol, only one host name can be specified. (Required).");
        Option hostsOption = OptionBuilder.create(OPTION_HOSTS);

        OptionBuilder.withArgName("port");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription("Remote port of service (Default is 8080 or 8442 for SSL).");
        Option portOption = OptionBuilder.create(OPTION_PORT);

        OptionBuilder.withArgName("certpurposes");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription("A ',' separated string containing requested certificate purposes.");
        Option usagesOption = OptionBuilder.create(OPTION_CERTPURPOSES);

        OptionBuilder.withArgName("jks-file");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription("Path to JKS truststore containing trusted CA for SSL Server certificates.");
        Option truststore = OptionBuilder.create(OPTION_TRUSTSTORE);

        OptionBuilder.withArgName("password");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription("Password to unlock the truststore.");
        Option truststorepwd = OptionBuilder.create(OPTION_TRUSTSTOREPWD);

        OptionBuilder.withArgName("servlet-url");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription(
                "URL to the webservice servlet. Default: " + SignServerWSClientFactory.DEFAULT_WSDL_URL
                        + " when using the webservice protocol, otherwise /signserver/process");
        Option servlet = OptionBuilder.create(OPTION_SERVLET);

        OptionBuilder.withArgName("protocol");
        OptionBuilder.hasArg();
        OptionBuilder.withDescription("Protocol to use, either WEBSERVICES or HTTP. Default: WEBSERVICES.");
        Option protocol = OptionBuilder.create(OPTION_PROTOCOL);

        options.addOption(help);
        options.addOption(serviceOption);
        options.addOption(certOption);
        options.addOption(hostsOption);
        options.addOption(portOption);
        options.addOption(usagesOption);
        options.addOption(pem);
        options.addOption(der);
        options.addOption(silent);
        options.addOption(truststore);
        options.addOption(truststorepwd);
        options.addOption(servlet);
        options.addOption(protocol);
    }

    @Override
    public String getDescription() {
        return "Request a certificate to get validated";
    }

    @Override
    public String getUsages() {
        final StringBuilder footer = new StringBuilder();
        footer.append(NL).append("The following values is returned by the program that can be used when scripting.")
                .append(NL).append("  -2   : Error happened during execution").append(NL)
                .append("  -1   : Bad arguments").append(NL).append("   0   : Certificate is valid").append(NL)
                .append("   1   : Certificate is revoked").append(NL)
                .append("   2   : Certificate is not yet valid").append(NL)
                .append("   3   : Certificate have expired").append(NL)
                .append("   4   : Certificate doesn't verify").append(NL)
                .append("   5   : CA Certificate have been revoked").append(NL)
                .append("   6   : CA Certificate is not yet valid").append(NL)
                .append("   7   : CA Certificate have expired.").append(NL)
                .append("   8   : Certificate have no valid certificate purpose.").append(NL).append(NL)
                .append("Sample usages:").append(NL).append("a) ").append(COMMAND)
                .append(" -service CertValidationWorker -hosts localhost -cert").append(NL)
                .append("    certificate.pem").append(NL).append("b) ").append(COMMAND)
                .append(" -service 5806 -hosts localhost -cert certificate.pem").append(NL)
                .append("    -truststore p12/truststore.jks -truststorepwd changeit").append(NL).append("c) ")
                .append(COMMAND)
                .append(" -service CertValidationWorker -hosts localhost -cert certificate.pem -protocol HTTP")
                .append(NL);

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        final HelpFormatter formatter = new HelpFormatter();

        PrintWriter pw = new PrintWriter(bout);
        formatter.printHelp(pw, HelpFormatter.DEFAULT_WIDTH, "Usage: signclient validatecertificate <options>\n",
                null, options, HelpFormatter.DEFAULT_LEFT_PAD, HelpFormatter.DEFAULT_DESC_PAD, footer.toString(),
                false);
        pw.close();
        return bout.toString();
    }

    @Override
    public int execute(String... args) throws IllegalCommandArgumentsException, CommandFailureException {
        int result = RETURN_BADARGUMENT;
        try {
            SignServerUtil.installBCProvider();

            CommandLineParser parser = new GnuParser();
            try {
                CommandLine cmd = parser.parse(options, args);
                if (cmd.hasOption(OPTION_HELP)) {
                    printUsage();
                    return RETURN_BADARGUMENT;
                }

                silentMode = cmd.hasOption(OPTION_SILENT);
                derFlag = cmd.hasOption(OPTION_DER);
                pemFlag = cmd.hasOption(OPTION_PEM);

                if (derFlag && pemFlag) {
                    err.println("Error, only one of -pem and -der options can be specified.");
                    printUsage();
                    return RETURN_BADARGUMENT;
                }

                if (!derFlag) {
                    pemFlag = true;
                }

                if (cmd.hasOption(OPTION_SERVICE) && cmd.getOptionValue(OPTION_SERVICE) != null) {
                    service = cmd.getOptionValue(OPTION_SERVICE);
                } else {
                    err.println("Error, an name or id of the validation service must be specified with the -"
                            + OPTION_SERVICE + " option.");
                    printUsage();
                    return RETURN_BADARGUMENT;
                }

                if (cmd.hasOption(OPTION_TRUSTSTORE)) {
                    trustStorePath = cmd.getOptionValue(OPTION_TRUSTSTORE);
                    if (trustStorePath != null) {
                        File f = new File(trustStorePath);
                        if (!f.exists() || !f.canRead() || f.isDirectory()) {
                            err.println("Error, a path to the truststore must point to a readable JKS file.");
                            printUsage();
                            return RETURN_BADARGUMENT;
                        }
                    } else {
                        err.println("Error, a path to the truststore must be supplied to the -" + OPTION_TRUSTSTORE
                                + " option.");
                        printUsage();
                        return RETURN_BADARGUMENT;
                    }
                }

                if (cmd.hasOption(OPTION_TRUSTSTOREPWD)) {
                    trustStorePwd = cmd.getOptionValue(OPTION_TRUSTSTOREPWD);
                    if (trustStorePwd == null) {
                        err.println("Error, a truststore password must be supplied to the -" + OPTION_TRUSTSTOREPWD
                                + " option.");
                        printUsage();
                        return RETURN_BADARGUMENT;
                    }
                }

                if (trustStorePath == null ^ trustStorePwd == null) {
                    err.println("Error, if HTTPS is going to be used must both the options -" + OPTION_TRUSTSTORE
                            + " and -" + OPTION_TRUSTSTOREPWD + " be specified");
                    printUsage();
                    return RETURN_BADARGUMENT;
                }

                useSSL = trustStorePath != null;

                if (cmd.hasOption(OPTION_HOSTS) && cmd.getOptionValue(OPTION_HOSTS) != null) {
                    hosts = cmd.getOptionValue(OPTION_HOSTS).split(",");
                } else {
                    err.println("Error, at least one validation service host must be specified.");
                    printUsage();
                    return RETURN_BADARGUMENT;
                }

                if (cmd.hasOption(OPTION_PORT)) {
                    String portString = cmd.getOptionValue(OPTION_PORT);
                    if (portString != null) {
                        try {
                            port = Integer.parseInt(portString);
                        } catch (NumberFormatException e) {
                            err.println("Error, port value must be an integer for option -" + OPTION_PORT + ".");
                            printUsage();
                            return RETURN_BADARGUMENT;
                        }
                    } else {
                        err.println("Error, a port value must be supplied to the -" + OPTION_PORT + " option.");
                        printUsage();
                        return RETURN_BADARGUMENT;
                    }
                } else {
                    if (useSSL) {
                        port = DEFAULT_SSLPORT;
                    } else {
                        port = DEFAULT_PORT;
                    }
                }

                if (cmd.hasOption(OPTION_CERTPURPOSES)) {
                    if (cmd.getOptionValue(OPTION_CERTPURPOSES) != null) {
                        usages = cmd.getOptionValue(OPTION_CERTPURPOSES);
                    } else {
                        err.println("Error, at least one usage must be specified with the -" + OPTION_CERTPURPOSES
                                + " option.");
                        printUsage();
                        return RETURN_BADARGUMENT;
                    }
                }

                if (cmd.hasOption(OPTION_CERT) && cmd.getOptionValue(OPTION_CERT) != null) {
                    certPath = new File(cmd.getOptionValue(OPTION_CERT));
                    if (!certPath.exists() || !certPath.canRead() || certPath.isDirectory()) {
                        err.println("Error, the certificate file must exist and be readable by the user.");
                        printUsage();
                        return RETURN_BADARGUMENT;
                    }
                } else {
                    err.println("Error, the certificate to validate must be specified with the -" + OPTION_CERT
                            + " option.");
                    printUsage();
                    return RETURN_BADARGUMENT;
                }

                // set the default servlet URL value
                servlet = SignServerWSClientFactory.DEFAULT_WSDL_URL;

                if (cmd.hasOption(OPTION_SERVLET) && cmd.getOptionValue(OPTION_SERVLET) != null) {
                    servlet = cmd.getOptionValue(OPTION_SERVLET);
                }

                if (cmd.hasOption(OPTION_PROTOCOL)) {
                    protocol = Protocol.valueOf(cmd.getOptionValue(OPTION_PROTOCOL));
                    // override default servlet URL (if not set manually) for HTTP
                    if (Protocol.HTTP.equals(protocol) && !cmd.hasOption(OPTION_SERVLET)) {
                        servlet = "/signserver/process";
                    }
                } else {
                    protocol = Protocol.WEBSERVICES;
                }

            } catch (ParseException e) {
                err.println("Error occurred when parsing options.  Reason: " + e.getMessage());
                printUsage();
                return RETURN_BADARGUMENT;
            }

            if (args.length < 1) {
                printUsage();
                return RETURN_BADARGUMENT;
            }
            result = run();
        } catch (Exception e) {
            if (!e.getClass().getSimpleName().equals("ExitException")) {

                err.println("Error occured during validation : " + e.getClass().getName());
                if (e.getMessage() != null) {
                    err.println("  Message : " + e.getMessage());
                }
                result = RETURN_ERROR;
            }
        }
        return result;
    }

    private int run() throws Exception {

        // read certificate
        X509Certificate cert = null;
        FileInputStream fis = new FileInputStream(certPath);
        try {
            if (pemFlag) {
                Collection<?> certs = CertTools.getCertsFromPEM(fis);
                if (certs.iterator().hasNext()) {
                    cert = (X509Certificate) certs.iterator().next();
                }
            } else {
                byte[] data = new byte[fis.available()];
                fis.read(data, 0, fis.available());
                cert = (X509Certificate) CertTools.getCertfromByteArray(data);
            }
        } finally {
            fis.close();
        }

        if (cert == null) {
            println("Error, Certificate in file " + certPath + " not read succesfully.");
        }

        println("\n\nValidating certificate with: ");
        println("  Subject    : " + cert.getSubjectDN().toString());
        println("  Issuer     : " + cert.getIssuerDN().toString());
        println("  Valid From : " + cert.getNotBefore());
        println("  Valid To   : " + cert.getNotAfter());

        println("\n");

        // validate
        final ValidateResponse vresp;
        switch (protocol) {
        case WEBSERVICES:
            // set up trust
            SSLSocketFactory sslf = null;
            if (trustStorePath != null) {
                sslf = WSClientUtil.genCustomSSLSocketFactory(null, null, trustStorePath, trustStorePwd);
            }

            vresp = runWS(sslf, cert);
            break;
        case HTTP:
            vresp = runHTTP(cert);
            break;
        default:
            throw new IllegalArgumentException("Unknown protocol: " + protocol.toString());
        }
        ;

        // output result
        String certificatePurposes = vresp.getValidCertificatePurposes();
        println("Valid Certificate Purposes:\n  " + (certificatePurposes == null ? "" : certificatePurposes));
        Validation validation = vresp.getValidation();
        println("Certificate Status:\n  " + validation.getStatus());

        return getReturnValue(validation.getStatus());
    }

    /**
     * Run validation using the webservice interface.
     * 
     * @param sslf SSL socket factory
     * @param cert Certificate to validate
     * @return The validation response
     * @throws CertificateEncodingException
     * @throws IOException
     */
    private ValidateResponse runWS(final SSLSocketFactory sslf, final X509Certificate cert)
            throws CertificateEncodingException, IOException {
        SignServerWSClientFactory fact = new SignServerWSClientFactory();
        ISignServerWSClient client = fact.generateSignServerWSClient(
                SignServerWSClientFactory.CLIENTTYPE_CALLFIRSTNODEWITHSTATUSOK, hosts, useSSL,
                new LogErrorCallback(), port, SignServerWSClientFactory.DEFAULT_TIMEOUT, servlet, sslf);

        ValidateRequest vr = new ValidateRequest(cert, usages);

        ArrayList<ProcessRequestWS> requests = new ArrayList<ProcessRequestWS>();
        requests.add(new ProcessRequestWS(vr));
        List<ProcessResponseWS> response = client.process(service, requests);
        if (response == null) {
            throw new IOException(
                    "Error communicating with valdation servers, no server in the cluster seem available.");
        }
        ValidateResponse vresp = (ValidateResponse) RequestAndResponseManager
                .parseProcessResponse(response.get(0).getResponseData());

        return vresp;
    }

    private ValidateResponse runHTTP(final X509Certificate cert) throws Exception {

        final URL processServlet = new URL(useSSL ? "https" : "http", hosts[0], port, servlet);

        OutputStream out = null;
        InputStream in = null;

        try {
            final URLConnection conn = processServlet.openConnection();

            conn.setDoOutput(true);
            conn.setAllowUserInteraction(false);

            final StringBuilder sb = new StringBuilder();
            sb.append("--" + BOUNDARY);
            sb.append(CRLF);

            try {
                final int workerId = Integer.parseInt(service);

                sb.append("Content-Disposition: form-data; name=\"workerId\"");
                sb.append(CRLF);
                sb.append(CRLF);
                sb.append(workerId);
            } catch (NumberFormatException e) {
                sb.append("Content-Disposition: form-data; name=\"workerName\"");
                sb.append(CRLF);
                sb.append(CRLF);
                sb.append(service);
            }

            sb.append(CRLF);
            sb.append("--" + BOUNDARY);
            sb.append(CRLF);

            sb.append("Content-Disposition: form-data; name=\"processType\"");
            sb.append(CRLF);
            sb.append(CRLF);
            sb.append("validateCertificate");
            sb.append(CRLF);
            sb.append("--" + BOUNDARY);
            sb.append(CRLF);
            sb.append("Content-Disposition: form-data; name=\"datafile\"");
            sb.append("; filename=\"");
            sb.append(certPath.getAbsolutePath());
            sb.append("\"");
            sb.append(CRLF);

            sb.append("Content-Type: application/octet-stream");
            sb.append(CRLF);
            sb.append("Content-Transfer-Encoding: binary");
            sb.append(CRLF);
            sb.append(CRLF);

            conn.addRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);

            out = conn.getOutputStream();

            out.write(sb.toString().getBytes());

            out.write(cert.getEncoded());

            out.write(("\r\n--" + BOUNDARY + "--\r\n").getBytes());
            out.flush();

            // Get the response
            in = conn.getInputStream();
            final ByteArrayOutputStream os = new ByteArrayOutputStream();
            int len;
            final byte[] buf = new byte[1024];
            while ((len = in.read(buf)) > 0) {
                os.write(buf, 0, len);
            }
            os.close();

            // read string from response
            final String response = os.toString();
            final String[] responseParts = response.split(";");

            // last part of the response string can by empty (revocation date)
            if (responseParts.length < 4 || responseParts.length > 5) {
                throw new IOException("Malformed HTTP response");
            }

            final String revocationDateString = responseParts.length == 4 ? null : responseParts[4];
            final Date revocationDate = revocationDateString != null && revocationDateString.length() > 0
                    ? new Date(Integer.valueOf(revocationDateString))
                    : null;
            final Validation validation = new Validation(cert, null, Validation.Status.valueOf(responseParts[0]),
                    responseParts[2], revocationDate, Integer.valueOf(responseParts[3]));
            final ValidateResponse validateResponse = new ValidateResponse(validation, responseParts[1].split(","));

            return validateResponse;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
            }
            if (in != null) {
                try {
                    in.close();
                } catch (IOException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
    }

    private void println(String string) {
        if (!silentMode) {
            out.println(string);
        }

    }

    private int getReturnValue(Status status) {
        if (status == Status.VALID) {
            return RETURN_VALID;
        }
        if (status == Status.REVOKED) {
            return RETURN_REVOKED;
        }
        if (status == Status.NOTYETVALID) {
            return RETURN_NOTYETVALID;
        }
        if (status == Status.EXPIRED) {
            return RETURN_EXPIRED;
        }
        if (status == Status.DONTVERIFY) {
            return RETURN_DONTVERIFY;
        }
        if (status == Status.CAREVOKED) {
            return RETURN_CAREVOKED;
        }
        if (status == Status.CANOTYETVALID) {
            return RETURN_CANOTYETVALID;
        }
        if (status == Status.CAEXPIRED) {
            return RETURN_CAEXPIRED;
        }
        if (status == Status.BADCERTPURPOSE) {
            return RETURN_BADCERTPURPOSE;
        }
        return RETURN_ERROR;
    }

    class LogErrorCallback implements IFaultCallback {

        @SuppressWarnings("synthetic-access")
        public void addCommunicationError(ICommunicationFault error) {
            final String s = "Error communication with host : " + error.getHostName() + ", "
                    + error.getDescription();
            if (error.getThrowed() != null) {
                out.println(s);
                error.getThrowed().printStackTrace();
            } else {
                out.println(s);
            }
        }
    }

    private void printUsage() {
        out.println(getUsages());
    }

}