Java tutorial
/* * 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); } } }