org.ejbca.ui.cmpclient.CmpClientMessageHelper.java Source code

Java tutorial

Introduction

Here is the source code for org.ejbca.ui.cmpclient.CmpClientMessageHelper.java

Source

/*************************************************************************
 *                                                                       *
 *  EJBCA - Proprietary Modules: Enterprise Certificate Authority        *
 *                                                                       *
 *  Copyright (c), PrimeKey Solutions AB. All rights reserved.           *
 *  The use of the Proprietary Modules are subject to specific           * 
 *  commercial license terms.                                            *
 *                                                                       *
 *************************************************************************/
package org.ejbca.ui.cmpclient;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.util.Random;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERNull;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.cmp.CMPCertificate;
import org.bouncycastle.asn1.cmp.PBMParameter;
import org.bouncycastle.asn1.cmp.PKIBody;
import org.bouncycastle.asn1.cmp.PKIHeader;
import org.bouncycastle.asn1.cmp.PKIHeaderBuilder;
import org.bouncycastle.asn1.cmp.PKIMessage;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.cesecore.certificates.util.AlgorithmTools;
import org.cesecore.util.CertTools;
import org.ejbca.config.CmpConfiguration;

public class CmpClientMessageHelper {

    private static final Logger log = Logger.getLogger(CmpClientMessageHelper.class);

    public static CmpClientMessageHelper getInstance() {
        return new CmpClientMessageHelper();
    }

    public PKIMessage createProtectedMessage(PKIMessage pkimessage, String authModule, String authParameter,
            final String keystorePath, final String keystorepwd, final boolean verbose) throws InvalidKeyException,
            NoSuchAlgorithmException, NoSuchProviderException, UnrecoverableKeyException, KeyStoreException,
            CertificateException, FileNotFoundException, IOException, SecurityException, SignatureException {

        if (StringUtils.equalsIgnoreCase(authModule, CmpConfiguration.AUTHMODULE_REG_TOKEN_PWD)) {
            if (verbose) {
                log.info("Authentication module used is " + CmpConfiguration.AUTHMODULE_REG_TOKEN_PWD + ". "
                        + "PKI message returned as is.");
            }
            return pkimessage;
        }

        if (StringUtils.equalsIgnoreCase(authModule, CmpConfiguration.AUTHMODULE_HMAC)) {
            if (verbose) {
                log.info("Creating protected PKIMessage using: authentication module=" + authModule
                        + ", authentication parameter=" + authParameter);
            }
            return protectPKIMessageWithHMAC(pkimessage, false, authParameter, 567);
        }

        if (StringUtils.equalsIgnoreCase(authModule, CmpConfiguration.AUTHMODULE_ENDENTITY_CERTIFICATE)) {
            if (verbose) {
                log.info("Creating protected PKIMessage using authentication module: " + authModule);
                log.info("Certificate in extraCerts field should be issued by: " + authParameter);
                log.info("Keystore: " + keystorePath + "  -  Keystore password: " + keystorepwd);
            }

            final KeyStore keystore = getKeystore(keystorePath, keystorepwd);
            Certificate extraCert = getCertFromKeystore(keystore, authParameter);
            //Object[] adminData = getAdminDataFromKeystore(keystore, keystorepwd, authParameter, verbose);
            //Certificate adminCert = (Certificate) adminData[0];
            if (verbose) {
                log.info("Certificate to be attached in the extraCerts field extracted from keystore. "
                        + "Certificate SubjectDN: " + CertTools.getSubjectDN(extraCert)
                        + " - Certificate issuerDN: " + CertTools.getIssuerDN(extraCert) + " - "
                        + "Certificate serialnumber: " + CertTools.getSerialNumberAsString(extraCert)
                        + " - Certificate fingerprint: " + CertTools.getFingerprintAsString(extraCert));
            }

            PrivateKey signKey = (PrivateKey) getKeyFromKeystore(keystore, keystorepwd, authParameter);

            CMPCertificate[] extraCerts = getCMPCerts(extraCert);
            AlgorithmIdentifier pAlg = new AlgorithmIdentifier(PKCSObjectIdentifiers.sha1WithRSAEncryption);
            return buildCertBasedPKIProtection(pkimessage, extraCerts, signKey, pAlg.getAlgorithm().getId(), "BC",
                    verbose);
        }

        log.info("Unrecognized authentication module: " + authModule);
        return null;
    }

    private PKIMessage protectPKIMessageWithHMAC(PKIMessage msg, boolean badObjectId, String password,
            int iterations) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException {
        // Create the PasswordBased protection of the message
        PKIHeaderBuilder head = getHeaderBuilder(msg.getHeader());
        // SHA1
        AlgorithmIdentifier owfAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.3.14.3.2.26"));
        // 567 iterations
        int iterationCount = iterations;
        ASN1Integer iteration = new ASN1Integer(iterationCount);
        // HMAC/SHA1
        AlgorithmIdentifier macAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier("1.2.840.113549.2.7"));
        byte[] salt = "foo123".getBytes();
        DEROctetString derSalt = new DEROctetString(salt);

        // Create the new protected return message
        String objectId = "1.2.840.113533.7.66.13";
        if (badObjectId) {
            objectId += ".7";
        }
        PBMParameter pp = new PBMParameter(derSalt, owfAlg, iteration, macAlg);
        AlgorithmIdentifier pAlg = new AlgorithmIdentifier(new ASN1ObjectIdentifier(objectId), pp);
        head.setProtectionAlg(pAlg);
        PKIHeader header = head.build();
        // Calculate the protection bits
        byte[] raSecret = password.getBytes();
        byte[] basekey = new byte[raSecret.length + salt.length];
        System.arraycopy(raSecret, 0, basekey, 0, raSecret.length);
        for (int i = 0; i < salt.length; i++) {
            basekey[raSecret.length + i] = salt[i];
        }
        // Construct the base key according to rfc4210, section 5.1.3.1
        MessageDigest dig = MessageDigest.getInstance(owfAlg.getAlgorithm().getId(), "BC");
        for (int i = 0; i < iterationCount; i++) {
            basekey = dig.digest(basekey);
            dig.reset();
        }
        // For HMAC/SHA1 there is another oid, that is not known in BC, but the
        // result is the same so...
        String macOid = macAlg.getAlgorithm().getId();
        PKIBody body = msg.getBody();
        byte[] protectedBytes = getProtectedBytes(header, body);
        Mac mac = Mac.getInstance(macOid, "BC");
        SecretKey key = new SecretKeySpec(basekey, macOid);
        mac.init(key);
        mac.reset();
        mac.update(protectedBytes, 0, protectedBytes.length);
        byte[] out = mac.doFinal();
        DERBitString bs = new DERBitString(out);

        return new PKIMessage(header, body, bs);
    }

    private PKIHeaderBuilder getHeaderBuilder(PKIHeader head) {
        PKIHeaderBuilder builder = new PKIHeaderBuilder(head.getPvno().getValue().intValue(), head.getSender(),
                head.getRecipient());
        builder.setFreeText(head.getFreeText());
        builder.setGeneralInfo(head.getGeneralInfo());
        builder.setMessageTime(head.getMessageTime());
        builder.setRecipKID((DEROctetString) head.getRecipKID());
        builder.setRecipNonce(head.getRecipNonce());
        builder.setSenderKID(head.getSenderKID());
        builder.setSenderNonce(head.getSenderNonce());
        builder.setTransactionID(head.getTransactionID());
        return builder;
    }

    /**
     * Converts the header and the body of a PKIMessage to an ASN1Encodable and 
     * returns the as a byte array
     *  
     * @param header
     * @param body
     * @return the PKIMessage's header and body in byte array
     */
    private byte[] getProtectedBytes(PKIHeader header, PKIBody body) {
        byte[] res = null;
        ASN1EncodableVector v = new ASN1EncodableVector();
        v.add(header);
        v.add(body);
        ASN1Encodable protectedPart = new DERSequence(v);
        try {
            ByteArrayOutputStream bao = new ByteArrayOutputStream();
            DEROutputStream out = new DEROutputStream(bao);
            out.writeObject(protectedPart);
            res = bao.toByteArray();
        } catch (Exception ex) {
            log.error(ex.getLocalizedMessage(), ex);
        }
        return res;
    }

    private PKIMessage buildCertBasedPKIProtection(PKIMessage pKIMessage, CMPCertificate[] extraCerts,
            PrivateKey key, String digestAlg, String provider, boolean verbose) throws NoSuchProviderException,
            NoSuchAlgorithmException, SecurityException, SignatureException, InvalidKeyException {
        // Select which signature algorithm we should use for the response, based on the digest algorithm and key type.
        ASN1ObjectIdentifier oid = AlgorithmTools.getSignAlgOidFromDigestAndKey(digestAlg, key.getAlgorithm());
        if (verbose) {
            log.info("Selected signature alg oid: " + oid.getId() + ", key algorithm: " + key.getAlgorithm());
        }
        // According to PKCS#1 AlgorithmIdentifier for RSA-PKCS#1 has null Parameters, this means a DER Null (asn.1 encoding of null), not Java null.
        // For the RSA signature algorithms specified above RFC3447 states "...the parameters MUST be present and MUST be NULL."
        PKIHeaderBuilder headerBuilder = getHeaderBuilder(pKIMessage.getHeader());
        AlgorithmIdentifier pAlg = null;
        if ("RSA".equalsIgnoreCase(key.getAlgorithm())) {
            pAlg = new AlgorithmIdentifier(oid, DERNull.INSTANCE);
        } else {
            pAlg = new AlgorithmIdentifier(oid);
        }
        headerBuilder.setProtectionAlg(pAlg);
        // Most PKCS#11 providers don't like to be fed an OID as signature algorithm, so 
        // we use BC classes to translate it into a signature algorithm name instead
        PKIHeader head = headerBuilder.build();
        String signatureAlgorithmName = AlgorithmTools.getAlgorithmNameFromOID(oid);
        if (verbose) {
            log.info("Signing CMP message with signature alg: " + signatureAlgorithmName);
        }
        Signature sig = Signature.getInstance(signatureAlgorithmName, provider);
        sig.initSign(key);
        sig.update(getProtectedBytes(head, pKIMessage.getBody()));

        if ((extraCerts != null) && (extraCerts.length > 0)) {
            pKIMessage = new PKIMessage(head, pKIMessage.getBody(), new DERBitString(sig.sign()), extraCerts);
        } else {
            pKIMessage = new PKIMessage(head, pKIMessage.getBody(), new DERBitString(sig.sign()));
        }
        return pKIMessage;
    }

    private KeyStore getKeystore(final String keystorePath, final String keystorePassword)
            throws NoSuchAlgorithmException, CertificateException, FileNotFoundException, IOException,
            KeyStoreException {
        KeyStore keystore = KeyStore.getInstance("PKCS12");
        keystore.load(new FileInputStream(keystorePath), keystorePassword.toCharArray());
        return keystore;
    }

    private Certificate getCertFromKeystore(final KeyStore keystore, final String alias) throws KeyStoreException {
        Certificate cert = keystore.getCertificate(alias);
        if (cert == null) {
            log.error("getAdminDataFromKeystore: Cannot obtain admin certificate from the keystore.");
            System.exit(2);
        }
        return cert;
    }

    private Key getKeyFromKeystore(final KeyStore keystore, final String keystorepwd, final String alias)
            throws UnrecoverableKeyException, KeyStoreException, NoSuchAlgorithmException {
        Key key = keystore.getKey(alias, keystorepwd.toCharArray());
        if (key == null) {
            log.error("getAdminDataFromKeystore: Cannot obtain admin key from the keystore.");
            System.exit(2);
        }
        return key;
    }

    private CMPCertificate[] getCMPCerts(Certificate cert) throws CertificateEncodingException, IOException {
        ASN1InputStream ins = new ASN1InputStream(cert.getEncoded());
        ASN1Primitive pcert = ins.readObject();
        ins.close();
        org.bouncycastle.asn1.x509.Certificate c = org.bouncycastle.asn1.x509.Certificate
                .getInstance(pcert.toASN1Primitive());
        CMPCertificate[] res = { new CMPCertificate(c) };
        return res;
    }

    public byte[] getRequestBytes(PKIMessage pkimessage) throws IOException {
        ByteArrayOutputStream bao = new ByteArrayOutputStream();
        DEROutputStream out = new DEROutputStream(bao);
        out.writeObject(pkimessage);
        byte[] ba = bao.toByteArray();
        return ba;
    }

    public byte[] sendCmpHttp(final byte[] message, final int httpRespCode, String cmpAlias, String host,
            final String fullURL) throws IOException {

        String urlString = fullURL;
        if (urlString == null) {
            if (host == null) {
                host = "127.0.0.1";
                log.info("Using default CMP Server IP address: localhost");
            }
            final String httpReqPath = "http://" + host + ":8080/ejbca";
            final String resourceCmp = "publicweb/cmp";
            if (cmpAlias == null) {
                cmpAlias = "cmp";
                log.info("Using default CMP alias: " + cmpAlias);
            }
            urlString = httpReqPath + '/' + resourceCmp + '/' + cmpAlias;
        }

        log.info("Using CMP URL: " + urlString);
        URL url = new URL(urlString);
        final HttpURLConnection con = (HttpURLConnection) url.openConnection();
        con.setDoOutput(true);
        con.setRequestMethod("POST");
        con.setRequestProperty("Content-type", "application/pkixcmp");
        con.connect();
        // POST it
        OutputStream os = con.getOutputStream();
        os.write(message);
        os.close();

        final int conResponseCode = con.getResponseCode();
        if (conResponseCode != httpRespCode) {
            log.info("Unexpected HTTP response code: " + conResponseCode);
        }
        // Only try to read the response if we expected a 200 (ok) response
        if (httpRespCode != 200) {
            return null;
        }
        // Some appserver (Weblogic) responds with
        // "application/pkixcmp; charset=UTF-8"
        final String conContentType = con.getContentType();
        if (conContentType == null) {
            log.error("No content type in response.");
            System.exit(1);
        }
        if (!StringUtils.equals("application/pkixcmp", conContentType)) {
            log.info("Content type is not 'application/pkixcmp'");
        }
        // Check that the CMP respone has the cache-control headers as specified in 
        // http://tools.ietf.org/html/draft-ietf-pkix-cmp-transport-protocols-14
        final String cacheControl = con.getHeaderField("Cache-Control");
        if (cacheControl == null) {
            log.error("'Cache-Control' header is not present.");
            System.exit(1);
        }
        if (!StringUtils.equals("no-cache", cacheControl)) {
            log.error("Cache-Control is not 'no-cache'");
            System.exit(1);
        }
        final String pragma = con.getHeaderField("Pragma");
        if (pragma == null) {
            log.error("'Pragma' header is not present.");
            System.exit(1);
        }
        if (!StringUtils.equals("no-cache", pragma)) {
            log.error("Pragma is not 'no-cache'");
            System.exit(1);
        }
        // Now read in the bytes
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // This works for small requests, and CMP requests are small enough
        InputStream in = con.getInputStream();
        int b = in.read();
        while (b != -1) {
            baos.write(b);
            b = in.read();
        }
        baos.flush();
        in.close();
        byte[] respBytes = baos.toByteArray();
        if ((respBytes == null) || (respBytes.length <= 0)) {
            log.error("No response from server");
            System.exit(1);
        }
        return respBytes;
    }

    /** Creates a 16 bytes random sender nonce
     * 
     * @return byte array of length 16
     */
    public byte[] createSenderNonce() {
        // Sendernonce is a random number
        byte[] senderNonce = new byte[16];
        Random randomSource;
        randomSource = new Random();
        randomSource.nextBytes(senderNonce);
        return senderNonce;
    }

}