org.ejbca.ui.cmpclient.commands.CrmfRequestCommand.java Source code

Java tutorial

Introduction

Here is the source code for org.ejbca.ui.cmpclient.commands.CrmfRequestCommand.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.commands;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.Signature;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1GeneralizedTime;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.cmp.CMPCertificate;
import org.bouncycastle.asn1.cmp.CertOrEncCert;
import org.bouncycastle.asn1.cmp.CertRepMessage;
import org.bouncycastle.asn1.cmp.CertResponse;
import org.bouncycastle.asn1.cmp.CertifiedKeyPair;
import org.bouncycastle.asn1.cmp.ErrorMsgContent;
import org.bouncycastle.asn1.cmp.PKIBody;
import org.bouncycastle.asn1.cmp.PKIHeaderBuilder;
import org.bouncycastle.asn1.cmp.PKIMessage;
import org.bouncycastle.asn1.cmp.PKIStatus;
import org.bouncycastle.asn1.cmp.PKIStatusInfo;
import org.bouncycastle.asn1.crmf.AttributeTypeAndValue;
import org.bouncycastle.asn1.crmf.CRMFObjectIdentifiers;
import org.bouncycastle.asn1.crmf.CertReqMessages;
import org.bouncycastle.asn1.crmf.CertReqMsg;
import org.bouncycastle.asn1.crmf.CertRequest;
import org.bouncycastle.asn1.crmf.CertTemplateBuilder;
import org.bouncycastle.asn1.crmf.OptionalValidity;
import org.bouncycastle.asn1.crmf.POPOSigningKey;
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.ExtensionsGenerator;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.cesecore.certificates.util.AlgorithmConstants;
import org.cesecore.certificates.util.AlgorithmTools;
import org.cesecore.keys.util.KeyTools;
import org.cesecore.util.CertTools;
import org.ejbca.config.CmpConfiguration;
import org.ejbca.ui.cli.infrastructure.command.CommandResult;
import org.ejbca.ui.cli.infrastructure.parameter.Parameter;
import org.ejbca.ui.cli.infrastructure.parameter.ParameterContainer;
import org.ejbca.ui.cli.infrastructure.parameter.enums.MandatoryMode;
import org.ejbca.ui.cli.infrastructure.parameter.enums.ParameterMode;
import org.ejbca.ui.cli.infrastructure.parameter.enums.StandaloneMode;
import org.ejbca.ui.cmpclient.CmpClientMessageHelper;

public class CrmfRequestCommand extends CmpCommandBase {

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

    private static final String COMMAND = "crmf";

    private static final String CMP_ALIAS_KEY = "--alias";
    private static final String SUBJECTDN_KEY = "--dn";
    private static final String ISSUERDN_KEY = "--issuer";
    private static final String DESTINATION_KEY = "--dest";
    private static final String AUTHENTICATION_MODULE_KEY = "--authmodule";
    private static final String AUTHENTICATION_PARAM_KEY = "--authparam";
    private static final String KEYSTORE_KEY = "--keystore";
    private static final String KEYSTOREPWD_KEY = "--keystorepwd";
    private static final String ALTNAME_KEY = "--altname";
    private static final String SERNO_KEY = "--serno";
    private static final String INCLUDE_POPO_KEY = "--includepopo";
    private static final String HOST_KEY = "--host";
    private static final String URL_KEY = "--url";
    private static final String VERBOSE_KEY = "--v";

    //Register all parameters
    {
        registerParameter(new Parameter(CMP_ALIAS_KEY, "CMP Configuration Alias", MandatoryMode.OPTIONAL,
                StandaloneMode.ALLOW, ParameterMode.ARGUMENT, "The CMP configuration alias. Default value: cmp"));
        registerParameter(new Parameter(SUBJECTDN_KEY, "SubjectDN", MandatoryMode.MANDATORY, StandaloneMode.ALLOW,
                ParameterMode.ARGUMENT, "The certificate's SubjectDN."));
        registerParameter(new Parameter(ISSUERDN_KEY, "IssuerDN", MandatoryMode.MANDATORY, StandaloneMode.ALLOW,
                ParameterMode.ARGUMENT, "The certificate's issuerDN"));
        registerParameter(new Parameter(DESTINATION_KEY, "Destination Directory", MandatoryMode.OPTIONAL,
                StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "The path to the directory where the newly issued certificate is stored. Default is './dest/'."));
        registerParameter(new Parameter(AUTHENTICATION_MODULE_KEY, "Authentication Module", MandatoryMode.OPTIONAL,
                StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "The authentication module used when creating the request. Default value: "
                        + CmpConfiguration.AUTHMODULE_HMAC + ". " + "Possible values: "
                        + CmpConfiguration.AUTHMODULE_REG_TOKEN_PWD + ", " + CmpConfiguration.AUTHMODULE_HMAC
                        + " or " + CmpConfiguration.AUTHMODULE_ENDENTITY_CERTIFICATE + ".\n" + "\nWhen using "
                        + CmpConfiguration.AUTHMODULE_HMAC
                        + " authentication module: In RA mode, the value of the authentication parameter "
                        + "should be the HMAC shared secret. In Client mode, it should be the end entity password (stored in clear text in EJBCA database)\n"
                        + "\nWhen using " + CmpConfiguration.AUTHMODULE_ENDENTITY_CERTIFICATE
                        + " authentication module, the value of the authentication "
                        + "parameter should be the friendlyname in the keystore for the certificate that will be attached in the extraCerts field\n"
                        + "\nIf you are using " + CmpConfiguration.AUTHMODULE_DN_PART_PWD
                        + ", set the SubjectDN part and the password in '" + SUBJECTDN_KEY + "' directly. "
                        + "For example, if you want the subjectDN to be 'CN=foo,C=SE' and you want the password 'foo123' to be specified in the DN part 'OU', set the SubjectDN "
                        + "as follows: '" + SUBJECTDN_KEY + " CN=foo,C=SE,OU=foo123'"));
        registerParameter(new Parameter(AUTHENTICATION_PARAM_KEY, "Authentication Parameter",
                MandatoryMode.OPTIONAL, StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "The authentication parameter is the parameter for the authentication module. Default value: foo123\n"
                        + "\nWhen using " + CmpConfiguration.AUTHMODULE_HMAC
                        + " authentication module: In RA mode, this value should be the HMAC shared secret. "
                        + "In Client mode, it should be the end entity password (stored in clear text in EJBCA database)\n"
                        + "\nWhen using " + CmpConfiguration.AUTHMODULE_ENDENTITY_CERTIFICATE
                        + " authentication module, this value should be the friendlyname in the keystore for "
                        + "the certificate that will be attached in the extraCerts field\n" + "\nIf you are using "
                        + CmpConfiguration.AUTHMODULE_DN_PART_PWD + ", set the SubjectDN part and the password in '"
                        + SUBJECTDN_KEY + "' directly. "
                        + "For example, if you want the subjectDN to be 'CN=foo,C=SE' and you want the password 'foo123' to be specified in the DN part 'OU', set the SubjectDN "
                        + "as follows: '" + SUBJECTDN_KEY + " CN=foo,C=SE,OU=foo123'"));
        registerParameter(new Parameter(KEYSTORE_KEY, "Path to the keystore", MandatoryMode.OPTIONAL,
                StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "The path to the keystore containing the certificate and private key used to sign the request. Mandatory when "
                        + CmpConfiguration.AUTHMODULE_ENDENTITY_CERTIFICATE + " authentication Module is used"));
        registerParameter(new Parameter(KEYSTOREPWD_KEY, "Keystore password", MandatoryMode.OPTIONAL,
                StandaloneMode.ALLOW, ParameterMode.ARGUMENT,
                "The password to the keystore containing the private key used to sign the request. Mandatory when "
                        + CmpConfiguration.AUTHMODULE_ENDENTITY_CERTIFICATE + " authentication Module is used"));
        registerParameter(new Parameter(ALTNAME_KEY, "SubjectAlternativeName", MandatoryMode.OPTIONAL,
                StandaloneMode.ALLOW, ParameterMode.ARGUMENT, "The certificate's SubjectAlternativeName."));
        registerParameter(
                new Parameter(SERNO_KEY, "Certificate Serialnumber", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW,
                        ParameterMode.ARGUMENT, "The custom certificate serialnumber in Hexadecimal format."));
        registerParameter(new Parameter(INCLUDE_POPO_KEY, "Include Proof-of-Possession", MandatoryMode.OPTIONAL,
                StandaloneMode.FORBID, ParameterMode.FLAG,
                "If present, a Proof-of-Possession is included in the CMP request"));
        registerParameter(new Parameter(HOST_KEY, "Host", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW,
                ParameterMode.ARGUMENT, "The name or IP adress to the CMP server. Default value is 'localhost'"));
        registerParameter(new Parameter(URL_KEY, "URL", MandatoryMode.OPTIONAL, StandaloneMode.ALLOW,
                ParameterMode.ARGUMENT,
                "The whole URL to the CMP server. This URL must include the CMP configuration alias. Default: http://localhost:8080/ejbca/publicweb/cmp/<CmpConfigurationAlias>"));
        registerParameter(new Parameter(VERBOSE_KEY, "Verbose", MandatoryMode.OPTIONAL, StandaloneMode.FORBID,
                ParameterMode.FLAG, "Prints out extra messages while executing"));
    }

    @Override
    public String getMainCommand() {
        return COMMAND;
    }

    @Override
    public String getCommandDescription() {
        return "Sends a CRMF request and stores the returned certificate in a local directory. "
                + "The certificate file name will have the format <CN>.pem";
    }

    @Override
    protected CommandResult execute(ParameterContainer parameters) {
        try {
            final PKIMessage pkimessage = generatePKIMessage(parameters);

            String authmodule = parameters.get(AUTHENTICATION_MODULE_KEY);
            String authparam = parameters.get(AUTHENTICATION_PARAM_KEY);
            if (authmodule == null) {
                authmodule = CmpConfiguration.AUTHMODULE_HMAC;
                log.info("Using default authentication module: " + CmpConfiguration.AUTHMODULE_HMAC);
            }
            if (authparam == null) {
                authparam = "foo123";
                log.info("Using default value for authentication parameter: " + authparam);
            }
            final PKIMessage protectedPKIMessage = CmpClientMessageHelper.getInstance().createProtectedMessage(
                    pkimessage, authmodule, authparam, parameters.get(KEYSTORE_KEY),
                    parameters.get(KEYSTOREPWD_KEY), parameters.containsKey(VERBOSE_KEY));

            byte[] requestBytes = CmpClientMessageHelper.getInstance().getRequestBytes(protectedPKIMessage);

            byte[] responseBytes = CmpClientMessageHelper.getInstance().sendCmpHttp(requestBytes, 200,
                    parameters.get(CMP_ALIAS_KEY), parameters.get(HOST_KEY), parameters.get(URL_KEY));

            return handleCMPResponse(responseBytes, parameters);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return CommandResult.CLI_FAILURE;
    }

    @Override
    public PKIMessage generatePKIMessage(final ParameterContainer parameters) throws Exception {

        final boolean verbose = parameters.containsKey(VERBOSE_KEY);

        final X500Name userDN = new X500Name(parameters.get(SUBJECTDN_KEY));
        final X500Name issuerDN = new X500Name(parameters.get(ISSUERDN_KEY));

        String authmodule = parameters.get(AUTHENTICATION_MODULE_KEY);
        String endentityPassword = "";
        if (authmodule != null && StringUtils.equals(authmodule, CmpConfiguration.AUTHMODULE_REG_TOKEN_PWD)) {
            endentityPassword = parameters.containsKey(AUTHENTICATION_PARAM_KEY)
                    ? parameters.get(AUTHENTICATION_PARAM_KEY)
                    : "foo123";
        }

        String altNames = parameters.get(ALTNAME_KEY);
        String serno = parameters.get(SERNO_KEY);
        BigInteger customCertSerno = null;
        if (serno != null) {
            customCertSerno = new BigInteger(serno, 16);
        }
        boolean includePopo = parameters.containsKey(INCLUDE_POPO_KEY);

        if (verbose) {
            log.info("Creating CRMF request with: SubjectDN=" + userDN.toString());
            log.info("Creating CRMF request with: IssuerDN=" + issuerDN.toString());
            log.info("Creating CRMF request with: AuthenticationModule=" + authmodule);
            log.info("Creating CRMF request with: EndEntityPassword=" + endentityPassword);
            log.info("Creating CRMF request with: SubjectAltName=" + altNames);
            log.info("Creating CRMF request with: CustomCertSerno="
                    + (customCertSerno == null ? "" : customCertSerno.toString(16)));
            log.info("Creating CRMF request with: IncludePopo=" + includePopo);
        }

        final KeyPair keys = KeyTools.genKeys("1024", AlgorithmConstants.KEYALGORITHM_RSA);
        final byte[] nonce = CmpClientMessageHelper.getInstance().createSenderNonce();
        final byte[] transid = CmpClientMessageHelper.getInstance().createSenderNonce();

        // We should be able to back date the start time when allow validity
        // override is enabled in the certificate profile
        Calendar cal = Calendar.getInstance();
        cal.add(Calendar.DAY_OF_WEEK, -1);
        cal.set(Calendar.MILLISECOND, 0); // Certificates don't use milliseconds
        // in validity
        Date notBefore = cal.getTime();
        cal.add(Calendar.DAY_OF_WEEK, 3);
        cal.set(Calendar.MILLISECOND, 0); // Certificates don't use milliseconds
        org.bouncycastle.asn1.x509.Time nb = new org.bouncycastle.asn1.x509.Time(notBefore);
        // in validity
        Date notAfter = cal.getTime();
        org.bouncycastle.asn1.x509.Time na = new org.bouncycastle.asn1.x509.Time(notAfter);

        ASN1EncodableVector optionalValidityV = new ASN1EncodableVector();
        optionalValidityV.add(new DERTaggedObject(true, 0, nb));
        optionalValidityV.add(new DERTaggedObject(true, 1, na));
        OptionalValidity myOptionalValidity = OptionalValidity.getInstance(new DERSequence(optionalValidityV));

        CertTemplateBuilder myCertTemplate = new CertTemplateBuilder();
        myCertTemplate.setValidity(myOptionalValidity);
        if (issuerDN != null) {
            myCertTemplate.setIssuer(issuerDN);
        }
        myCertTemplate.setSubject(userDN);
        byte[] bytes = keys.getPublic().getEncoded();
        ByteArrayInputStream bIn = new ByteArrayInputStream(bytes);
        ASN1InputStream dIn = new ASN1InputStream(bIn);
        SubjectPublicKeyInfo keyInfo = new SubjectPublicKeyInfo((ASN1Sequence) dIn.readObject());
        dIn.close();
        myCertTemplate.setPublicKey(keyInfo);

        // Create standard extensions
        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
        ASN1OutputStream dOut = new ASN1OutputStream(bOut);
        ExtensionsGenerator extgen = new ExtensionsGenerator();
        if (altNames != null) {
            GeneralNames san = CertTools.getGeneralNamesFromAltName(altNames);
            dOut.writeObject(san);
            byte[] value = bOut.toByteArray();
            extgen.addExtension(Extension.subjectAlternativeName, false, value);
        }

        // KeyUsage
        int bcku = 0;
        bcku = KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.nonRepudiation;
        KeyUsage ku = new KeyUsage(bcku);
        extgen.addExtension(Extension.keyUsage, false, new DERBitString(ku));

        // Make the complete extension package
        Extensions exts = extgen.generate();

        myCertTemplate.setExtensions(exts);
        if (customCertSerno != null) {
            // Add serialNumber to the certTemplate, it is defined as a MUST NOT be used in RFC4211, but we will use it anyway in order
            // to request a custom certificate serial number (something not standard anyway)
            myCertTemplate.setSerialNumber(new ASN1Integer(customCertSerno));
        }

        CertRequest myCertRequest = new CertRequest(4, myCertTemplate.build(), null);

        // POPO
        /*
         * PKMACValue myPKMACValue = new PKMACValue( new AlgorithmIdentifier(new
         * ASN1ObjectIdentifier("8.2.1.2.3.4"), new DERBitString(new byte[] { 8,
         * 1, 1, 2 })), new DERBitString(new byte[] { 12, 29, 37, 43 }));
         * 
         * POPOPrivKey myPOPOPrivKey = new POPOPrivKey(new DERBitString(new
         * byte[] { 44 }), 2); //take choice pos tag 2
         * 
         * POPOSigningKeyInput myPOPOSigningKeyInput = new POPOSigningKeyInput(
         * myPKMACValue, new SubjectPublicKeyInfo( new AlgorithmIdentifier(new
         * ASN1ObjectIdentifier("9.3.3.9.2.2"), new DERBitString(new byte[] { 2,
         * 9, 7, 3 })), new byte[] { 7, 7, 7, 4, 5, 6, 7, 7, 7 }));
         */
        ProofOfPossession myProofOfPossession = null;
        if (includePopo) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            DEROutputStream mout = new DEROutputStream(baos);
            mout.writeObject(myCertRequest);
            mout.close();
            byte[] popoProtectionBytes = baos.toByteArray();
            String sigalg = AlgorithmTools.getSignAlgOidFromDigestAndKey(null, keys.getPrivate().getAlgorithm())
                    .getId();
            Signature sig = Signature.getInstance(sigalg, "BC");
            sig.initSign(keys.getPrivate());
            sig.update(popoProtectionBytes);
            DERBitString bs = new DERBitString(sig.sign());
            POPOSigningKey myPOPOSigningKey = new POPOSigningKey(null,
                    new AlgorithmIdentifier(new ASN1ObjectIdentifier(sigalg)), bs);
            myProofOfPossession = new ProofOfPossession(myPOPOSigningKey);
        } else {
            // raVerified POPO (meaning there is no POPO)
            myProofOfPossession = new ProofOfPossession();
        }

        AttributeTypeAndValue av = new AttributeTypeAndValue(CRMFObjectIdentifiers.id_regCtrl_regToken,
                new DERUTF8String(endentityPassword));
        AttributeTypeAndValue[] avs = { av };

        CertReqMsg myCertReqMsg = new CertReqMsg(myCertRequest, myProofOfPossession, avs);

        CertReqMessages myCertReqMessages = new CertReqMessages(myCertReqMsg);

        PKIHeaderBuilder myPKIHeader = new PKIHeaderBuilder(2, new GeneralName(userDN), new GeneralName(issuerDN));

        myPKIHeader.setMessageTime(new ASN1GeneralizedTime(new Date()));
        // senderNonce
        myPKIHeader.setSenderNonce(new DEROctetString(nonce));
        // TransactionId
        myPKIHeader.setTransactionID(new DEROctetString(transid));
        myPKIHeader.setProtectionAlg(null);
        myPKIHeader.setSenderKID(new byte[0]);

        PKIBody myPKIBody = new PKIBody(0, myCertReqMessages); // initialization
        // request
        PKIMessage myPKIMessage = new PKIMessage(myPKIHeader.build(), myPKIBody);

        return myPKIMessage;
    }

    @Override
    public CommandResult handleCMPResponse(byte[] response, final ParameterContainer parameters) throws Exception {
        String dest = parameters.get(DESTINATION_KEY);
        if (dest == null) {
            dest = "dest";
            new File("./" + dest).mkdirs();
            log.info("Using default destination directory: ./dest/");
        }

        PKIMessage respObject = null;
        ASN1InputStream asn1InputStream = new ASN1InputStream(new ByteArrayInputStream(response));
        try {
            respObject = PKIMessage.getInstance(asn1InputStream.readObject());
        } finally {
            asn1InputStream.close();
        }
        if (respObject == null) {
            log.error("ERROR. Cannot construct the response object");
            return CommandResult.FUNCTIONAL_FAILURE;
        }

        PKIBody body = respObject.getBody();
        int tag = body.getType();

        if (tag == PKIBody.TYPE_INIT_REP) {
            CertRepMessage c = (CertRepMessage) body.getContent();
            CertResponse resp = c.getResponse()[0];
            PKIStatusInfo status = resp.getStatus();
            if (status.getStatus().intValue() == PKIStatus.GRANTED) {
                final X509Certificate cert = getCertFromResponse(resp);
                final ArrayList<Certificate> certs = new ArrayList<>();
                certs.add(cert);
                final byte[] certBytes = CertTools.getPemFromCertificateChain(certs);

                String certFileName = getDestinationCertFile(dest, parameters.get(SUBJECTDN_KEY));
                final FileOutputStream fos = new FileOutputStream(new File(certFileName));
                fos.write(certBytes);
                fos.close();
                log.info("CRMF request successful. Received certificate stored in " + certFileName);
                return CommandResult.SUCCESS;
            } else {
                final String errMsg = status.getStatusString().getStringAt(0).getString();
                log.error("Recieved CRMF response with status '" + status.getStatus().intValue()
                        + "' and error message: " + errMsg);
            }
        } else if (tag == PKIBody.TYPE_ERROR) {
            ErrorMsgContent err = (ErrorMsgContent) body.getContent();
            final String errMsg = err.getPKIStatusInfo().getStatusString().getStringAt(0).getString();
            log.error("Revceived CMP Error Message: " + errMsg);
        } else {
            log.error("Received PKIMessage with body tag " + tag);
        }
        return CommandResult.FUNCTIONAL_FAILURE;
    }

    private X509Certificate getCertFromResponse(final CertResponse resp) throws Exception {
        final CertifiedKeyPair kp = resp.getCertifiedKeyPair();
        final CertOrEncCert cc = kp.getCertOrEncCert();
        final CMPCertificate cmpcert = cc.getCertificate();
        return (X509Certificate) CertTools.getCertfromByteArray(cmpcert.getEncoded());
    }

    private String getDestinationCertFile(final String destDirectory, final String subjectDN) {
        return destDirectory + (StringUtils.endsWith(destDirectory, "/") ? "" : "/")
                + CertTools.getPartFromDN(subjectDN, "CN") + ".pem";
    }

    @Override
    public String getFullHelpText() {
        return "";
    }

    @Override
    protected Logger getLogger() {
        return log;
    }

}