org.ejbca.core.protocol.cmp.CrmfRequestMessage.java Source code

Java tutorial

Introduction

Here is the source code for org.ejbca.core.protocol.cmp.CrmfRequestMessage.java

Source

/*************************************************************************
 *                                                                       *
 *  EJBCA Community: The OpenSource Certificate Authority                *
 *                                                                       *
 *  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.ejbca.core.protocol.cmp;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.spec.X509EncodedKeySpec;
import java.util.Date;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.cmp.PKIBody;
import org.bouncycastle.asn1.cmp.PKIHeader;
import org.bouncycastle.asn1.cmp.PKIMessage;
import org.bouncycastle.asn1.crmf.CertReqMessages;
import org.bouncycastle.asn1.crmf.CertReqMsg;
import org.bouncycastle.asn1.crmf.CertRequest;
import org.bouncycastle.asn1.crmf.CertTemplate;
import org.bouncycastle.asn1.crmf.OptionalValidity;
import org.bouncycastle.asn1.crmf.POPOSigningKey;
import org.bouncycastle.asn1.crmf.POPOSigningKeyInput;
import org.bouncycastle.asn1.crmf.ProofOfPossession;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.Time;
import org.bouncycastle.cms.CMSSignedGenerator;
import org.bouncycastle.util.Arrays;
import org.cesecore.util.Base64;
import org.cesecore.util.CeSecoreNameStyle;
import org.cesecore.util.CertTools;
import org.ejbca.core.protocol.cmp.authentication.RegTokenPasswordExtractor;

/**
 * Certificate request message (crmf) according to RFC4211.
 * - Supported POPO: 
 * -- raVerified (null), i.e. no POPO verification is done, it should be configurable if the CA should allow this or require a real POPO
 * -- Self signature, using the key in CertTemplate, or POPOSigningKeyInput (name and public key), option 2 and 3 in RFC4211, section "4.1.  Signature Key POP"
 * 
 * @author tomas
 * @version $Id: CrmfRequestMessage.java 20482 2014-12-18 10:10:21Z mikekushner $
 */
public class CrmfRequestMessage extends BaseCmpMessage implements ICrmfRequestMessage {

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

    /**
     * Determines if a de-serialized file is compatible with this class.
     *
     * Maintainers must change this value if and only if the new version
     * of this class is not compatible with old versions. See Sun docs
     * for <a href=http://java.sun.com/products/jdk/1.1/docs/guide
     * /serialization/spec/version.doc.html> details. </a>
     *
     */
    static final long serialVersionUID = 1002L;

    private int requestType = 0;
    private int requestId = 0;
    private String b64SenderNonce = null;
    private String b64TransId = null;
    /** Default CA DN */
    private String defaultCADN = null;
    private boolean allowRaVerifyPopo = false;
    private String extractUsernameComponent = null;
    /** manually set username */
    private String username = null;
    /** manually set password */
    private String password = null;

    /** Because PKIMessage is not serializable we need to have the serializable bytes save as well, so 
     * we can restore the PKIMessage after serialization/deserialization. */
    private byte[] pkimsgbytes = null;
    private transient CertReqMsg req = null;

    /** Because CertReqMsg is not serializable we may need to encode/decode bytes if the object is lost during deserialization. */
    private CertReqMsg getReq() {
        if (req == null) {
            init();
        }
        return this.req;
    }

    /** preferred digest algorithm to use in replies, if applicable */
    private String preferredDigestAlg = CMSSignedGenerator.DIGEST_SHA1;

    public CrmfRequestMessage() {

    }

    /**
     * 
     * @param msg PKIMessage
     * @param defaultCA possibility to enforce a certain CA, instead of taking the CA subject DN from the request, if set to null the CA subject DN is taken from the request
     * @param allowRaVerifyPopo true if we allows the user/RA to specify the POP should not be verified
     * @param extractUsernameComponent Defines which component from the DN should be used as username in EJBCA. Can be CN, UID or nothing. Null means that the username should have been pre-set, or that here it is the same as CN.
     */
    public CrmfRequestMessage(final PKIMessage msg, final String defaultCADN, final boolean allowRaVerifyPopo,
            final String extractUsernameComponent) {
        if (log.isTraceEnabled()) {
            log.trace(">CrmfRequestMessage");
        }
        setPKIMessage(msg);
        this.defaultCADN = defaultCADN;
        this.allowRaVerifyPopo = allowRaVerifyPopo;
        this.extractUsernameComponent = extractUsernameComponent;
        init();
        if (log.isTraceEnabled()) {
            log.trace("<CrmfRequestMessage");
        }
    }

    public PKIMessage getPKIMessage() {
        if (getMessage() == null) {
            try {
                setMessage(PKIMessage
                        .getInstance(new ASN1InputStream(new ByteArrayInputStream(pkimsgbytes)).readObject()));
            } catch (IOException e) {
                log.error("Error decoding bytes for PKIMessage: ", e);
            }
        }
        return getMessage();
    }

    public void setPKIMessage(final PKIMessage msg) {
        try {
            this.pkimsgbytes = msg.toASN1Primitive().getEncoded();
        } catch (IOException e) {
            log.error("Error getting encoded bytes from PKIMessage: ", e);
        }
        setMessage(msg);
    }

    private void init() {

        final PKIBody body = getPKIMessage().getBody();
        final PKIHeader header = getPKIMessage().getHeader();
        requestType = body.getType();
        final CertReqMessages msgs = getCertReqFromTag(body, requestType);

        try {
            this.req = msgs.toCertReqMsgArray()[0];
        } catch (Exception e) {
            this.req = CmpMessageHelper.getNovosecCertReqMsg(msgs);
        }

        requestId = this.req.getCertReq().getCertReqId().getValue().intValue();

        ASN1OctetString os = header.getTransactionID();
        if (os != null) {
            byte[] val = os.getOctets();
            if (val != null) {
                setTransactionId(new String(Base64.encode(val)));
            }
        }
        os = header.getSenderNonce();
        if (os != null) {
            byte[] val = os.getOctets();
            if (val != null) {
                setSenderNonce(new String(Base64.encode(val)));
            }
        }
        setRecipient(header.getRecipient());
        setSender(header.getSender());
    }

    @Override
    public PublicKey getRequestPublicKey()
            throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException {
        final CertRequest request = getReq().getCertReq();
        final CertTemplate templ = request.getCertTemplate();
        final SubjectPublicKeyInfo keyInfo = templ.getPublicKey();
        final PublicKey pk = getPublicKey(keyInfo, "BC");
        return pk;
    }

    private PublicKey getPublicKey(final SubjectPublicKeyInfo subjectPKInfo, final String provider)
            throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException {
        try {
            final X509EncodedKeySpec xspec = new X509EncodedKeySpec(new DERBitString(subjectPKInfo).getBytes());
            final AlgorithmIdentifier keyAlg = subjectPKInfo.getAlgorithm();
            return KeyFactory.getInstance(keyAlg.getAlgorithm().getId(), provider).generatePublic(xspec);
        } catch (java.security.spec.InvalidKeySpecException e) {
            final InvalidKeyException newe = new InvalidKeyException("Error decoding public key.");
            newe.initCause(e);
            throw newe;
        } catch (IOException e) {
            final InvalidKeyException newe = new InvalidKeyException("Error decoding public key.");
            newe.initCause(e);
            throw newe;
        }
    }

    /** force a password, i.e. ignore the password in the request
     */
    public void setPassword(final String pwd) {
        this.password = pwd;
    }

    @Override
    public String getPassword() {
        if (password != null) {
            return this.password;
        }

        RegTokenPasswordExtractor regTokenExtractor = new RegTokenPasswordExtractor();

        if (regTokenExtractor.verifyOrExtract(getPKIMessage(), null)) {
            this.password = regTokenExtractor.getAuthenticationString();
        } else {
            if (log.isDebugEnabled()) {
                log.debug(regTokenExtractor.getErrorMessage());
            }
        }
        return this.password;
    }

    /** force a username, i.e. ignore the DN/username in the request
     */
    public void setUsername(final String username) {
        this.username = username;
    }

    @Override
    public String getUsername() {
        String ret = null;
        if (username != null) {
            ret = username;
        } else {
            // We can configure which part of the users DN should be used as username in EJBCA, for example CN or UID
            String component = extractUsernameComponent;
            if (StringUtils.isEmpty(component)) {
                component = "CN";
            }
            String name = CertTools.getPartFromDN(getRequestDN(), component);
            if (name == null) {
                log.error("No component " + component + " in DN: " + getRequestDN());
            } else {
                ret = name;
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Username is: " + ret);
        }
        return ret;
    }

    public void setIssuerDN(final String issuer) {
        this.defaultCADN = issuer;
    }

    @Override
    public String getIssuerDN() {
        String ret = null;
        final CertTemplate templ = getReq().getCertReq().getCertTemplate();
        final X500Name name = templ.getIssuer();
        if (name != null) {
            ret = CertTools.stringToBCDNString(name.toString());
        } else {
            ret = defaultCADN;
        }
        if (log.isDebugEnabled()) {
            log.debug("Issuer DN is: " + ret);
        }
        return ret;
    }

    @Override
    public BigInteger getSerialNo() {
        return null;
    }

    @Override
    public String getCRLIssuerDN() {
        return null;
    }

    @Override
    public BigInteger getCRLSerialNo() {
        return null;
    }

    /** Gets a requested certificate serial number of the subject. This is a standard field in the CertTemplate in the request.
     * However the standard RFC 4211, section 5 (CertRequest syntax) says it MUST not be used. 
     * Requesting custom certificate serial numbers is a very non-standard procedure anyhow, so we use it anyway. 
     * 
     * @return BigInteger the requested custom certificate serial number or null, normally this should return null.
     */
    public BigInteger getSubjectCertSerialNo() {
        BigInteger ret = null;
        final CertRequest request = getReq().getCertReq();
        final CertTemplate templ = request.getCertTemplate();
        final ASN1Integer serno = templ.getSerialNumber();
        if (serno != null) {
            ret = serno.getValue();
        }
        return ret;
    }

    @Override
    public String getRequestDN() {
        String ret = null;
        final X500Name name = getRequestX500Name();
        if (name != null) {
            ret = CertTools.stringToBCDNString(name.toString());
        }
        if (log.isDebugEnabled()) {
            log.debug("Request DN is: " + ret);
        }
        return ret;
    }

    @Override
    public X500Name getRequestX500Name() {
        final CertTemplate templ = getReq().getCertReq().getCertTemplate();
        X500Name name = templ.getSubject();
        if (name != null) {
            name = new X500Name(new CeSecoreNameStyle(), name);
        }
        if (log.isDebugEnabled()) {
            log.debug("Request X500Name is: " + name);
        }
        return name;
    }

    @Override
    public String getRequestAltNames() {
        String ret = null;
        final CertTemplate templ = getReq().getCertReq().getCertTemplate();
        final Extensions exts = templ.getExtensions();
        if (exts != null) {
            final Extension ext = exts.getExtension(Extension.subjectAlternativeName);
            if (ext != null) {
                ret = CertTools.getAltNameStringFromExtension(ext);
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Request altName is: " + ret);
        }
        return ret;
    }

    @Override
    public Date getRequestValidityNotBefore() {
        Date ret = null;
        final CertTemplate templ = getReq().getCertReq().getCertTemplate();
        final OptionalValidity val = templ.getValidity();
        if (val != null) {
            DERSequence valSeq = (DERSequence) val.toASN1Primitive();
            ASN1Encodable[] asn1a = valSeq.toArray();
            final Time time = Time.getInstance((ASN1TaggedObject) asn1a[0], true);
            if (time != null) {
                ret = time.getDate();
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Request validity notBefore is: " + (ret == null ? "null" : ret.toString()));
        }
        return ret;
    }

    @Override
    public Date getRequestValidityNotAfter() {
        Date ret = null;
        final CertTemplate templ = getReq().getCertReq().getCertTemplate();
        final OptionalValidity val = templ.getValidity();
        if (val != null) {
            DERSequence valSeq = (DERSequence) val.toASN1Primitive();
            ASN1Encodable[] asn1a = valSeq.toArray();
            final Time time = Time.getInstance((ASN1TaggedObject) asn1a[1], true);
            if (time != null) {
                ret = time.getDate();
            }
        }
        if (log.isDebugEnabled()) {
            log.debug("Request validity notAfter is: " + (ret == null ? "null" : ret.toString()));
        }
        return ret;
    }

    @Override
    public Extensions getRequestExtensions() {
        final CertTemplate templ = getReq().getCertReq().getCertTemplate();
        final Extensions exts = templ.getExtensions();
        if (log.isDebugEnabled()) {
            if (exts != null) {
                log.debug("Request contains extensions");
            } else {
                log.debug("Request does not contain extensions");
            }
        }
        return exts;
    }

    @Override
    public boolean verify() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException {
        boolean ret = false;
        final ProofOfPossession pop = getReq().getPopo();
        if (log.isDebugEnabled()) {
            log.debug("allowRaVerifyPopo: " + allowRaVerifyPopo);
            log.debug("pop.getRaVerified(): " + (pop.getType() == ProofOfPossession.TYPE_RA_VERIFIED));
        }
        if (allowRaVerifyPopo && (pop.getType() == ProofOfPossession.TYPE_RA_VERIFIED)) {
            ret = true;
        } else if (pop.getType() == ProofOfPossession.TYPE_SIGNING_KEY) {
            try {
                final POPOSigningKey sk = (POPOSigningKey) pop.getObject();
                final POPOSigningKeyInput pski = sk.getPoposkInput();
                ASN1Encodable protObject = pski;
                // Use of POPOSigningKeyInput or not, as described in RFC4211, section 4.1.
                if (pski == null) {
                    if (log.isDebugEnabled()) {
                        log.debug("Using CertRequest as POPO input because POPOSigningKeyInput is missing.");
                    }
                    protObject = getReq().getCertReq();
                } else {
                    // Assume POPOSigningKeyInput with the public key and name, MUST be the same as in the request according to RFC4211
                    if (log.isDebugEnabled()) {
                        log.debug("Using POPOSigningKeyInput as POPO input.");
                    }
                    final CertRequest req = getReq().getCertReq();
                    // If subject is present in cert template it must be the same as in POPOSigningKeyInput
                    final X500Name subject = req.getCertTemplate().getSubject();
                    if (subject != null && !subject.toString().equals(pski.getSender().getName().toString())) {
                        log.info("Subject '" + subject.toString() + "', is not equal to '"
                                + pski.getSender().toString() + "'.");
                        protObject = null; // pski is not a valid protection object
                    }
                    // If public key is present in cert template it must be the same as in POPOSigningKeyInput
                    final SubjectPublicKeyInfo pk = req.getCertTemplate().getPublicKey();
                    if (pk != null && !Arrays.areEqual(pk.getEncoded(), pski.getPublicKey().getEncoded())) {
                        log.info(
                                "Subject key in cert template, is not equal to subject key in POPOSigningKeyInput.");
                        protObject = null; // pski is not a valid protection object
                    }
                }
                // If a protectObject is present we extract the bytes and verify it
                if (protObject != null) {
                    final ByteArrayOutputStream bao = new ByteArrayOutputStream();
                    new DEROutputStream(bao).writeObject(protObject);
                    final byte[] protBytes = bao.toByteArray();
                    final AlgorithmIdentifier algId = sk.getAlgorithmIdentifier();
                    if (log.isDebugEnabled()) {
                        log.debug(
                                "POP protection bytes length: " + (protBytes != null ? protBytes.length : "null"));
                        log.debug("POP algorithm identifier is: " + algId.getAlgorithm().getId());
                    }
                    final Signature sig = Signature.getInstance(algId.getAlgorithm().getId(), "BC");
                    sig.initVerify(getRequestPublicKey());
                    sig.update(protBytes);
                    final DERBitString bs = sk.getSignature();
                    ret = sig.verify(bs.getBytes());
                    if (log.isDebugEnabled()) {
                        log.debug("POP verify returns: " + ret);
                    }
                }
            } catch (IOException e) {
                log.error("Error encoding CertReqMsg: ", e);
            } catch (SignatureException e) {
                log.error("SignatureException verifying POP: ", e);
            }
        }
        return ret;
    }

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

    @Override
    public void setKeyInfo(final Certificate cert, final PrivateKey key, final String provider) {
    }

    @Override
    public int getErrorNo() {
        return 0;
    }

    @Override
    public String getErrorText() {
        return null;
    }

    public void setSenderNonce(final String b64nonce) {
        this.b64SenderNonce = b64nonce;
    }

    @Override
    public String getSenderNonce() {
        return b64SenderNonce;
    }

    public void setTransactionId(final String b64transid) {
        this.b64TransId = b64transid;
    }

    @Override
    public String getTransactionId() {
        return b64TransId;
    }

    @Override
    public byte[] getRequestKeyInfo() {
        return null;
    }

    @Override
    public String getPreferredDigestAlg() {
        return preferredDigestAlg;
    }

    public void setPreferredDigestAlg(String digestAlgo) {
        if (StringUtils.isNotEmpty(digestAlgo)) {
            preferredDigestAlg = digestAlgo;
        }
    }

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

    @Override
    public int getRequestType() {
        return requestType;
    }

    @Override
    public int getRequestId() {
        return requestId;
    }

    // Returns the subject DN from the request, used from CrmfMessageHandler
    public String getSubjectDN() {
        String ret = null;
        final CertTemplate templ = getReq().getCertReq().getCertTemplate();
        final X500Name name = templ.getSubject();
        if (name != null) {
            ret = CertTools.stringToBCDNString(name.toString());
        }
        return ret;
    }

    private CertReqMessages getCertReqFromTag(final PKIBody body, final int tag) {
        CertReqMessages msgs = null;
        if (tag == 0 || tag == 2 || tag == 7 || tag == 9 || tag == 13) {
            msgs = (CertReqMessages) body.getContent();
        }
        return msgs;
    }

    @Override
    public void setResponseKeyInfo(PrivateKey key, String provider) {
        //These values are never used for this type of message
        if (log.isDebugEnabled()) {
            log.debug(
                    "Key and provider were set for a CrmfRequestMessage. These values are not used and will be ignored.");
        }
    }

}