mitm.common.security.ca.handlers.comodo.ComodoCertificateRequestHandler.java Source code

Java tutorial

Introduction

Here is the source code for mitm.common.security.ca.handlers.comodo.ComodoCertificateRequestHandler.java

Source

/*
 * Copyright (c) 2010-2012, Martijn Brinkers, Djigzo.
 * 
 * This file is part of Djigzo email encryption.
 *
 * Djigzo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License 
 * version 3, 19 November 2007 as published by the Free Software 
 * Foundation.
 *
 * Djigzo 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public 
 * License along with Djigzo. If not, see <http://www.gnu.org/licenses/>
 *
 * Additional permission under GNU AGPL version 3 section 7
 * 
 * If you modify this Program, or any covered work, by linking or 
 * combining it with aspectjrt.jar, aspectjweaver.jar, tyrex-1.0.3.jar, 
 * freemarker.jar, dom4j.jar, mx4j-jmx.jar, mx4j-tools.jar, 
 * spice-classman-1.0.jar, spice-loggerstore-0.5.jar, spice-salt-0.8.jar, 
 * spice-xmlpolicy-1.0.jar, saaj-api-1.3.jar, saaj-impl-1.3.jar, 
 * wsdl4j-1.6.1.jar (or modified versions of these libraries), 
 * containing parts covered by the terms of Eclipse Public License, 
 * tyrex license, freemarker license, dom4j license, mx4j license,
 * Spice Software License, Common Development and Distribution License
 * (CDDL), Common Public License (CPL) the licensors of this Program grant 
 * you additional permission to convey the resulting work.
 */
package mitm.common.security.ca.handlers.comodo;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.SecureRandom;

import mitm.common.properties.HierarchicalPropertiesException;
import mitm.common.security.KeyAndCertificate;
import mitm.common.security.KeyAndCertificateImpl;
import mitm.common.security.KeyEncoderException;
import mitm.common.security.SecurityFactory;
import mitm.common.security.SecurityFactoryFactory;
import mitm.common.security.ca.CAException;
import mitm.common.security.ca.CertificateRequest;
import mitm.common.security.ca.CertificateRequestHandler;
import mitm.common.security.ca.CertificateRequestHandlerRegistry;
import mitm.common.security.certificate.X500PrincipalUtils;
import mitm.common.security.crypto.Encryptor;
import mitm.common.util.Check;
import mitm.common.util.MiscStringUtils;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.text.StrBuilder;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An implementation of a CertificateRequestHandler does uses Comodo's EPKI.
 * 
 * @author Martijn Brinkers
 *
 */
public class ComodoCertificateRequestHandler implements CertificateRequestHandler {
    private final static Logger logger = LoggerFactory.getLogger(ComodoCertificateRequestHandler.class);

    public static String NAME = "Comodo";

    /*
     * Provided the Comodo settings (like account settings etc.)
     */
    private final ComodoSettingsProvider settingsProvider;

    /*
     * The Comodo connection settings (like URLs, timeouts etc.)
     */
    private final ComodoConnectionSettings connectionSettings;

    /*
     * The encryptor which is responsible for encryping/decrypting the keypair.
     */
    private final Encryptor encryptor;

    /*
     * Factory used to create security object instances
     */
    private final SecurityFactory securityFactory;

    /*
     * Used by the KeyPairGenerator
     * 
     * Note: SecureRandom is not thread safe.
     */
    private final SecureRandom randomSource;

    /*
     * Will be used to persistently store data in the CertificateRequest 
     */
    private static class DataWrapper {
        private final static int VERSION = 1;

        /*
         * The current state
         */
        private ComodoRequestState state = ComodoRequestState.INITIALIZING;

        /*
         * The order number returned from ApplyCustomClientCert
         */
        private String orderNumber;

        /*
         * True if the request was authorized. This is only used when autoAuthorize is enabled 
         */
        private boolean authorized;

        public static byte[] encode(DataWrapper wrapper) throws IOException {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream output = new ObjectOutputStream(bos);

            output.writeInt(VERSION);
            output.writeObject(wrapper.state);
            output.writeObject(wrapper.orderNumber);
            output.writeBoolean(wrapper.authorized);
            output.close();

            return bos.toByteArray();
        }

        public static DataWrapper decode(byte[] encoded) throws IOException, ClassNotFoundException {
            DataWrapper wrapper = new DataWrapper();

            if (encoded == null) {
                return wrapper;
            }

            ObjectInputStream input = new ObjectInputStream(new ByteArrayInputStream(encoded));

            int version = input.readInt();

            if (version != VERSION) {
                throw new IOException("Unexpected version. Expected " + VERSION + " but got " + version);
            }

            wrapper.state = (ComodoRequestState) input.readObject();
            wrapper.orderNumber = (String) input.readObject();
            wrapper.authorized = input.readBoolean();

            return wrapper;
        }

        public ComodoRequestState getState() {
            return state;
        }

        public void setState(ComodoRequestState state) {
            this.state = state;
        }

        public String getOrderNumber() {
            return orderNumber;
        }

        public void setOrderNumber(String orderNumber) {
            this.orderNumber = orderNumber;
        }

        public boolean isAuthorized() {
            return authorized;
        }

        public void setAuthorized(boolean authorized) {
            this.authorized = authorized;
        }
    }

    public ComodoCertificateRequestHandler(CertificateRequestHandlerRegistry registry,
            ComodoSettingsProvider settingsProvider, ComodoConnectionSettings connectionSettings,
            Encryptor encryptor) throws CAException {
        Check.notNull(settingsProvider, "settingsProvider");
        Check.notNull(connectionSettings, "connectionSettings");
        Check.notNull(encryptor, "encryptor");

        this.settingsProvider = settingsProvider;
        this.connectionSettings = connectionSettings;
        this.encryptor = encryptor;

        securityFactory = SecurityFactoryFactory.getSecurityFactory();

        try {
            randomSource = securityFactory.createSecureRandom();
        } catch (NoSuchAlgorithmException e) {
            throw new CAException("Error creating secure random.");
        } catch (NoSuchProviderException e) {
            throw new CAException("Error creating secure random.");
        }

        if (registry != null) {
            registry.registerHandler(this);
        }
    }

    @Override
    public String getCertificateHandlerName() {
        return NAME;
    }

    @Override
    public boolean isEnabled() {
        boolean enabled = false;

        try {
            enabled = isEnabled(settingsProvider.getSettings());
        } catch (HierarchicalPropertiesException e) {
            logger.error("Error getting Comodo settings. Comodo Certificate Request Handler will not be enabled.",
                    e);
        }

        return enabled;
    }

    @Override
    public boolean isInstantlyIssued() {
        return false;
    }

    private boolean isEnabled(ComodoSettings settings) {
        boolean enabled = false;

        try {

            if (StringUtils.isNotEmpty(settings.getLoginName())
                    && StringUtils.isNotEmpty(settings.getLoginPassword())
                    && StringUtils.isNotEmpty(settings.getAP())) {
                enabled = true;
            } else {
                logger.debug("Comodo Certificate Request Handler will not be enabled. Not all required parameters "
                        + "are set.");
            }

        } catch (HierarchicalPropertiesException e) {
            logger.error("Error getting Comodo settings. Comodo Certificate Request Handler will not be enabled.",
                    e);
        }

        return enabled;
    }

    private void assertEnabled(ComodoSettings settings) throws CAException {
        if (!isEnabled(settings)) {
            throw new CAException("Not all required Comodo parameters are set.");
        }
    }

    private void handleInitializing(CertificateRequest request, DataWrapper data) {
        logger.debug("handling state: " + data.getState());

        /*
         * First step. Suspend until next time. The first call to handleRequest is done on a mail thread.
         * To make sure mail handling isn't stalled the actual request will be done on a separate thread. 
         */
        data.setState(ComodoRequestState.WAITING_FOR_REQUEST);
    }

    /*
     * Note: Random source is not thread safe so we need to synchronize.
     */
    private synchronized KeyPair generateKeyPair(int keyLength)
            throws NoSuchAlgorithmException, NoSuchProviderException {
        KeyPairGenerator keyPairGenerator = securityFactory.createKeyPairGenerator("RSA");

        keyPairGenerator.initialize(keyLength, randomSource);

        return keyPairGenerator.generateKeyPair();
    }

    private ContentSigner getContentSigner(String signatureAlgorithm, PrivateKey privateKey)
            throws OperatorCreationException {
        JcaContentSignerBuilder contentSignerBuilder = new JcaContentSignerBuilder(signatureAlgorithm);

        contentSignerBuilder.setProvider(securityFactory.getSensitiveProvider());

        return contentSignerBuilder.build(privateKey);
    }

    private void handleWaitingForRequest(CertificateRequest request, DataWrapper data)
            throws HierarchicalPropertiesException, CAException {
        logger.debug("handling state: " + data.getState());

        try {
            ComodoSettings settings = settingsProvider.getSettings();

            assertEnabled(settings);

            KeyPair keyPair = request.getKeyPair(encryptor);

            if (keyPair == null) {
                keyPair = generateKeyPair(request.getKeyLength());
            }

            /* 
             * We must store the generated keypair.
             */
            request.setKeyPair(keyPair, encryptor);

            PKCS10CertificationRequestBuilder requestBuilder = new PKCS10CertificationRequestBuilder(
                    X500PrincipalUtils.toX500Name(request.getSubject()),
                    SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded()));

            PKCS10CertificationRequest pkcs10 = requestBuilder
                    .build(getContentSigner("SHA1WithRSA", keyPair.getPrivate()));

            String base64PKCS10 = MiscStringUtils.toAsciiString(Base64.encodeBase64(pkcs10.getEncoded()));

            ApplyCustomClientCert applier = new ApplyCustomClientCert(connectionSettings);

            applier.setAP(settings.getAP());
            applier.setCACertificateID(settings.getCACertificateID());
            applier.setDays(request.getValidity());
            applier.setPkcs10(base64PKCS10);

            boolean success = applier.apply();

            if (success) {
                logger.info("Certificate request for user " + request.getEmail() + " was sent. Order number: "
                        + applier.getOrderNumber());

                data.setOrderNumber(applier.getOrderNumber());

                data.setState(settings.isAutoAuthorize() ? ComodoRequestState.WAITING_FOR_AUTHORIZATION
                        : ComodoRequestState.WAITING_FOR_RETRIEVAL);

                request.setInfo("Order number: " + applier.getOrderNumber());
            } else {
                String errorMessage = "Error requesting certificate. Message: " + applier.getErrorMessage();

                logger.warn(errorMessage);

                request.setLastMessage(MiscStringUtils.restrictLength(errorMessage, 1024));
            }
        } catch (OperatorCreationException e) {
            throw new CAException("Error requesting a certificate", e);
        } catch (NoSuchAlgorithmException e) {
            throw new CAException("Error requesting a certificate", e);
        } catch (NoSuchProviderException e) {
            throw new CAException("Error requesting a certificate", e);
        } catch (KeyEncoderException e) {
            throw new CAException("Error encrypting the key pair", e);
        } catch (CustomClientCertException e) {
            throw new CAException("Error requesting a certificate", e);
        } catch (IOException e) {
            throw new CAException("Error requesting a certificate", e);
        }
    }

    private void handleWaitingForAuthorization(CertificateRequest request, DataWrapper data)
            throws CAException, HierarchicalPropertiesException {
        logger.debug("handling state: " + data.getState());

        try {
            ComodoSettings settings = settingsProvider.getSettings();

            assertEnabled(settings);

            if (!settings.isAutoAuthorize()) {
                /*
                 * Can only happen if Auto authorize is disabled during a request procedure.
                 */
                logger.warn("Auto authorize is disabled.");

                data.setState(ComodoRequestState.WAITING_FOR_RETRIEVAL);

                return;
            }

            AutoAuthorize authorizer = new AutoAuthorize(connectionSettings);

            authorizer.setLoginName(settings.getLoginName());
            authorizer.setLoginPassword(settings.getLoginPassword());
            authorizer.setOrderNumber(data.getOrderNumber());

            boolean success = authorizer.authorize();

            if (success) {
                logger.info("Certificate for user " + request.getEmail() + " with order number "
                        + data.getOrderNumber() + " was authorized.");

                data.setAuthorized(true);
                data.setState(ComodoRequestState.WAITING_FOR_RETRIEVAL);
            } else {
                if (authorizer.getErrorCode() == CustomClientStatusCode.NOT_WAITING_FOR_AUTHORIZATION) {
                    /*
                     * This can happen if the request was authorized via the web
                     */
                    logger.info("Certificate for user " + request.getEmail() + " with order number "
                            + data.getOrderNumber() + " was already authorized.");

                    /*
                     * We assume that the request was already authorized using the web.
                     */
                    data.setAuthorized(true);
                    data.setState(ComodoRequestState.WAITING_FOR_RETRIEVAL);
                } else {
                    String message = "Error authorizing certificate for user " + request.getEmail()
                            + " with order number " + data.getOrderNumber() + ". Message: "
                            + authorizer.getErrorMessage();

                    logger.warn(message);

                    request.setLastMessage(MiscStringUtils.restrictLength(message, 1024));
                }
            }
        } catch (CustomClientCertException e) {
            throw new CAException("Error authorizing a certificate", e);
        }
    }

    private KeyAndCertificate handleWaitingForRetrieval(CertificateRequest request, DataWrapper data)
            throws CAException {
        logger.debug("handling state: " + data.getState());

        try {
            KeyAndCertificate keyAndCertificate = null;

            ComodoSettings settings = settingsProvider.getSettings();

            assertEnabled(settings);

            CollectCustomClientCert collector = new CollectCustomClientCert(connectionSettings);

            collector.setLoginName(settings.getLoginName());
            collector.setLoginPassword(settings.getLoginPassword());
            collector.setOrderNumber(data.getOrderNumber());

            boolean success = collector.collectCertificate();

            if (success) {
                if (collector.getErrorCode() == CustomClientStatusCode.CERTIFICATES_ATTACHED) {
                    logger.info("Certificate for user " + request.getEmail() + " with order number "
                            + data.getOrderNumber() + " was collected.");

                    if (collector.getCertificate() == null) {
                        throw new CAException("Certificate is null");
                    }

                    KeyPair keyPair = request.getKeyPair(encryptor);

                    if (keyPair == null) {
                        throw new CAException("keyPair is null");
                    }

                    keyAndCertificate = new KeyAndCertificateImpl(keyPair.getPrivate(), collector.getCertificate());

                    data.setState(ComodoRequestState.FINISHED);
                } else if (collector.getErrorCode() == CustomClientStatusCode.SUCCESSFUL) {
                    /*
                     * CustomClientStatusCode.SUCCESSFUL means "being processed by Comodo" for CCC API. 
                     */
                    String message = "certificate for user " + request.getEmail() + " with order number "
                            + data.getOrderNumber() + " is being processed by Comodo.";

                    logger.info(message);

                    request.setLastMessage(MiscStringUtils.restrictLength(message, 1024));

                    /*
                     * It can happen that when the certificate was requested, autoAuthorize was disabled and
                     * then enabled. If autoAuthorize is true and the request was not yet authorized the next 
                     * state should be authorize. 
                     */
                    if (settings.isAutoAuthorize() && !data.isAuthorized()) {
                        logger.info("Auto authorize is enabled. The request however, is not yet authorized. "
                                + "Going to auto authorize the request.");

                        data.setState(ComodoRequestState.WAITING_FOR_AUTHORIZATION);
                    }
                } else {
                    /*
                     * Should not happen
                     */
                    String message = "Unexpected state for user " + request.getEmail() + " with order number "
                            + data.getOrderNumber() + ". State: " + collector.getErrorCode();

                    logger.info(message);

                    request.setLastMessage(MiscStringUtils.restrictLength(message, 1024));
                }
            } else {
                String message = "Error collecting certificate for user " + request.getEmail()
                        + " with order number " + data.getOrderNumber() + ". Message: "
                        + collector.getErrorMessage();

                logger.warn(message);

                request.setLastMessage(MiscStringUtils.restrictLength(message, 1024));
            }

            return keyAndCertificate;
        } catch (HierarchicalPropertiesException e) {
            throw new CAException("Error collecting a certificate", e);
        } catch (CustomClientCertException e) {
            throw new CAException("Error collecting a certificate", e);
        } catch (KeyEncoderException e) {
            throw new CAException("Error decrypting the key pair", e);
        }
    }

    @Override
    public KeyAndCertificate handleRequest(CertificateRequest request) throws CAException {
        try {
            KeyAndCertificate keyAndCertificate = null;

            /*
             * Get the data out of the general data store
             */
            DataWrapper data = DataWrapper.decode(request.getData());

            request.setLastMessage(null);

            switch (data.getState()) {
            case INITIALIZING:
                handleInitializing(request, data);
                break;
            case WAITING_FOR_REQUEST:
                handleWaitingForRequest(request, data);
                break;
            case WAITING_FOR_AUTHORIZATION:
                handleWaitingForAuthorization(request, data);
                break;
            case WAITING_FOR_RETRIEVAL:
                keyAndCertificate = handleWaitingForRetrieval(request, data);
                break;
            default:
                logger.warn("Unexpected state: " + data.getState());
            }

            /*
             * Append the current state to last message.
             */
            StrBuilder sb = new StrBuilder(256);

            sb.append(data.getState().getFriendlyname());

            if (StringUtils.isNotEmpty(request.getLastMessage())) {
                sb.append("; ").append(request.getLastMessage());
            }

            request.setLastMessage(sb.toString());
            /*
             * Place the updated data back
             */
            request.setData(DataWrapper.encode(data));

            return keyAndCertificate;
        } catch (IOException e) {
            throw new CAException(e);
        } catch (ClassNotFoundException e) {
            throw new CAException(e);
        } catch (HierarchicalPropertiesException e) {
            throw new CAException(e);
        }
    }
}