org.signserver.module.tsa.MSAuthCodeTimeStampSigner.java Source code

Java tutorial

Introduction

Here is the source code for org.signserver.module.tsa.MSAuthCodeTimeStampSigner.java

Source

/*************************************************************************
 *                                                                       *
 *  SignServer: The OpenSource Automated Signing Server                  *
 *                                                                       *
 *  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.signserver.module.tsa;

import java.io.IOException;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.util.*;

import javax.persistence.EntityManager;

import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERInteger;
import org.bouncycastle.asn1.DERSet;
import org.bouncycastle.asn1.cms.AttributeTable;
import org.bouncycastle.asn1.cms.CMSAttributes;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.asn1.cms.Time;
import org.bouncycastle.asn1.ess.ESSCertID;
import org.bouncycastle.asn1.ess.SigningCertificate;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Attribute;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.IssuerSerial;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaCertStore;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSTypedData;
import org.bouncycastle.cms.DefaultSignedAttributeTableGenerator;
import org.bouncycastle.cms.SignerInfoGenerator;
import org.bouncycastle.cms.SignerInfoGeneratorBuilder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.ejbca.util.Base64;
import org.signserver.common.*;
import org.signserver.module.tsa.bc.MSAuthCodeCMSUtils;
import org.signserver.module.tsa.bc.TimeStampRequest;
import org.signserver.module.tsa.bc.TimeStampResponseGenerator;
import org.signserver.server.ITimeSource;
import org.signserver.server.WorkerContext;
import org.signserver.server.archive.Archivable;
import org.signserver.server.archive.DefaultArchivable;
import org.signserver.server.cryptotokens.ICryptoInstance;
import org.signserver.server.cryptotokens.ICryptoToken;
import org.signserver.server.log.LogMap;
import org.signserver.server.signers.BaseSigner;

/**
 * A Signer signing Time-stamp request compatible with Microsoft Authenticode
 *
 * Implements a ISigner and have the following properties:
 *
 * <table border="1">
 *  <tr>
 *      <td>TIMESOURCE</td>
 *      <td>
 *          property containing the classpath to the ITimeSource implementation
 *          that should be used. (default LocalComputerTimeSource)
 *      </td>
 *  </tr>
 *
 * </table>
 * 
 * Specifying a signer certificate (normally the SIGNERCERT property) is required 
 * as information from that certificate will be used to indicate which signer
 * signed the time-stamp token.
 * 
 * The SIGNERCERTCHAIN property contains all certificates included in the token 
 * if the client requests the certificates. The RFC specified that the signer 
 * certificate MUST be included in the list returned.
 * 
 *
 * @author Marcus Lundblad
 * @version $Id: MSAuthCodeTimeStampSigner.java 5977 2015-03-27 10:30:50Z netmackan $
 */
public class MSAuthCodeTimeStampSigner extends BaseSigner {

    /** Log4j instance for actual implementation class. */
    private static final Logger LOG = Logger.getLogger(MSAuthCodeTimeStampSigner.class);

    /** Random generator algorithm. */
    private static String algorithm = "SHA1PRNG";

    /** Random generator. */
    private transient SecureRandom random;

    private static final BigInteger LOWEST = new BigInteger("0080000000000000", 16);

    private static final BigInteger HIGHEST = new BigInteger("7FFFFFFFFFFFFFFF", 16);

    //Private Property constants
    public static final String TIMESOURCE = "TIMESOURCE";
    public static final String SIGNATUREALGORITHM = "SIGNATUREALGORITHM";
    public static final String ACCEPTEDALGORITHMS = "ACCEPTEDALGORITHMS";
    public static final String ACCEPTEDPOLICIES = "ACCEPTEDPOLICIES";
    public static final String ACCEPTEDEXTENSIONS = "ACCEPTEDEXTENSIONS";
    //public static final String DEFAULTDIGESTOID    = "DEFAULTDIGESTOID";
    public static final String DEFAULTTSAPOLICYOID = "DEFAULTTSAPOLICYOID";
    public static final String ACCURACYMICROS = "ACCURACYMICROS";
    public static final String ACCURACYMILLIS = "ACCURACYMILLIS";
    public static final String ACCURACYSECONDS = "ACCURACYSECONDS";
    public static final String ORDERING = "ORDERING";
    public static final String TSA = "TSA";
    public static final String REQUIREVALIDCHAIN = "REQUIREVALIDCHAIN";
    public static final String INCLUDE_SIGNING_CERTIFICATE_ATTRIBUTE = "INCLUDE_SIGNING_CERTIFICATE_ATTRIBUTE";

    private static final String dataOID = "1.2.840.113549.1.7.1";
    private static final String msOID = "1.3.6.1.4.1.311.3.2.1";

    private static final String DEFAULT_WORKERLOGGER = DefaultTimeStampLogger.class.getName();

    private static final String DEFAULT_TIMESOURCE = "org.signserver.server.LocalComputerTimeSource";

    private static final String DEFAULT_SIGNATUREALGORITHM = "SHA1withRSA";

    /** MIME type for the request data. **/
    private static final String REQUEST_CONTENT_TYPE = "application/octect-stream";

    /** MIME type for the response data. **/
    private static final String RESPONSE_CONTENT_TYPE = "application/octet-stream";

    private ITimeSource timeSource = null;
    private String signatureAlgo;

    private boolean validChain = true;

    private boolean includeSigningCertificateAttribute;

    private List<String> configErrors;

    @Override
    public void init(final int signerId, final WorkerConfig config, final WorkerContext workerContext,
            final EntityManager workerEntityManager) {
        super.init(signerId, config, workerContext, workerEntityManager);

        // Overrides the default worker logger to be this worker
        //  implementation's default instead of the WorkerSessionBean's
        if (config.getProperty("WORKERLOGGER") == null) {
            config.setProperty("WORKERLOGGER", DEFAULT_WORKERLOGGER);
        }

        // Check that the timestamp server is properly configured
        try {
            timeSource = getTimeSource();
            if (LOG.isDebugEnabled()) {
                LOG.debug("TimeStampSigner[" + signerId + "]: " + "Using TimeSource: "
                        + timeSource.getClass().getName());
            }

            signatureAlgo = config.getProperty(SIGNATUREALGORITHM);

            if (signatureAlgo == null) {
                signatureAlgo = DEFAULT_SIGNATUREALGORITHM;
            }
        } catch (SignServerException e) {
            LOG.error("Could not create time source: " + e.getMessage());
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("bctsp version: " + TimeStampResponseGenerator.class.getPackage().getImplementationVersion()
                    + ", " + TimeStampRequest.class.getPackage().getImplementationVersion());
        }

        // Validate certificates in signer certificate chain
        final String requireValidChain = config.getProperty(REQUIREVALIDCHAIN, Boolean.FALSE.toString());
        if (Boolean.parseBoolean(requireValidChain)) {
            validChain = validateChain();
        }

        includeSigningCertificateAttribute = Boolean
                .parseBoolean(config.getProperty(INCLUDE_SIGNING_CERTIFICATE_ATTRIBUTE, "false"));

        configErrors = new LinkedList<String>();

        if (hasSetIncludeCertificateLevels) {
            configErrors.add(WorkerConfig.PROPERTY_INCLUDE_CERTIFICATE_LEVELS + " is not supported.");
        }
    }

    /**
     * The main method performing the actual timestamp operation.
     * Expects the signRequest to be a GenericSignRequest contining a
     * TimeStampRequest
     *
     * @param signRequest
     * @param requestContext
     * @return the sign response
     * @see org.signserver.server.IProcessable#processData(org.signserver.common.ProcessRequest, org.signserver.common.RequestContext)
     */
    public ProcessResponse processData(final ProcessRequest signRequest, final RequestContext requestContext)
            throws IllegalRequestException, CryptoTokenOfflineException, SignServerException {

        // Log values
        final LogMap logMap = LogMap.getInstance(requestContext);

        try {
            final ISignRequest sReq = (ISignRequest) signRequest;
            final byte[] requestbytes = (byte[]) sReq.getRequestData();

            if (requestbytes == null || requestbytes.length == 0) {
                LOG.error("Request must contain data");
                throw new IllegalRequestException("Request must contain data");
            }

            // Check that the request contains a valid TimeStampRequest object.
            if (!(signRequest instanceof GenericSignRequest)) {
                final IllegalRequestException exception = new IllegalRequestException(
                        "Recieved request wasn't an expected GenericSignRequest. ");
                LOG.error("Received request wasn't an expected GenericSignRequest");
                throw exception;
            }

            if (!((sReq.getRequestData() instanceof TimeStampRequest)
                    || (sReq.getRequestData() instanceof byte[]))) {
                final IllegalRequestException exception = new IllegalRequestException(
                        "Recieved request data wasn't an expected TimeStampRequest. ");
                LOG.error("Received request data wasn't an expected TimeStampRequest");
                throw exception;
            }

            if (!validChain) {
                LOG.error("Certificate chain not correctly configured");
                throw new CryptoTokenOfflineException("Certificate chain not correctly configured");
            }

            ASN1Primitive asn1obj = ASN1Primitive.fromByteArray(Base64.decode(requestbytes));
            ASN1Sequence asn1seq = ASN1Sequence.getInstance(asn1obj);

            if (asn1seq.size() != 2) {
                LOG.error("Wrong structure, should be an ASN1Sequence with 2 elements");
                throw new IllegalRequestException("Wrong structure, should be an ASN1Sequence with 2 elements");
            }

            ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(asn1seq.getObjectAt(0));
            ASN1Sequence asn1seq1 = ASN1Sequence.getInstance(asn1seq.getObjectAt(1));

            final ContentInfo ci = new ContentInfo(asn1seq1);

            if (!oid.getId().equals(msOID)) {
                LOG.error("Invalid OID in request: " + oid.getId());
                throw new IllegalRequestException("Invalid OID in request: " + oid.getId());
            }

            if (asn1seq1.size() != 2) {
                LOG.error(
                        "Wrong structure, should be an ASN1Sequence with 2 elements as the value of element 0 in the outer ASN1Sequence");
                throw new IllegalRequestException(
                        "Wrong structure, should be an ASN1Sequence with 2 elements as the value of element 0 in the outer ASN1Sequence");
            }

            oid = ASN1ObjectIdentifier.getInstance(asn1seq1.getObjectAt(0));

            if (!oid.getId().equals(dataOID)) {
                throw new IllegalRequestException("Wrong contentType OID: " + oid.getId());
            }

            ASN1TaggedObject tag = ASN1TaggedObject.getInstance(asn1seq1.getObjectAt(1));

            if (tag.getTagNo() != 0) {
                throw new IllegalRequestException("Wrong tag no (should be 0): " + tag.getTagNo());
            }

            ASN1OctetString octets = ASN1OctetString.getInstance(tag.getObject());
            byte[] content = octets.getOctets();

            final ITimeSource timeSrc;
            final Date date;
            byte[] der;
            ICryptoInstance crypto = null;
            try {
                crypto = acquireCryptoInstance(ICryptoToken.PURPOSE_SIGN, signRequest, requestContext);

                // get signing cert certificate chain and private key
                List<Certificate> certList = this.getSigningCertificateChain(crypto);
                if (certList == null) {
                    throw new SignServerException("Null certificate chain. This signer needs a certificate.");
                }

                Certificate[] certs = (Certificate[]) certList.toArray(new Certificate[certList.size()]);

                // Sign
                X509Certificate x509cert = (X509Certificate) certs[0];

                timeSrc = getTimeSource();
                if (LOG.isDebugEnabled()) {
                    LOG.debug("TimeSource: " + timeSrc.getClass().getName());
                }
                date = timeSrc.getGenTime();

                if (date == null) {
                    throw new ServiceUnavailableException("Time source is not available");
                }

                ASN1EncodableVector signedAttributes = new ASN1EncodableVector();
                signedAttributes.add(new Attribute(CMSAttributes.signingTime, new DERSet(new Time(date))));

                if (includeSigningCertificateAttribute) {
                    try {
                        final DERInteger serial = new DERInteger(x509cert.getSerialNumber());
                        final X509CertificateHolder certHolder = new X509CertificateHolder(x509cert.getEncoded());
                        final X500Name issuer = certHolder.getIssuer();
                        final GeneralName name = new GeneralName(issuer);
                        final GeneralNames names = new GeneralNames(name);
                        final IssuerSerial is = new IssuerSerial(names, ASN1Integer.getInstance(serial));

                        final ESSCertID essCertid = new ESSCertID(
                                MessageDigest.getInstance("SHA-1").digest(x509cert.getEncoded()), is);
                        signedAttributes.add(new Attribute(PKCSObjectIdentifiers.id_aa_signingCertificate,
                                new DERSet(new SigningCertificate(essCertid))));
                    } catch (NoSuchAlgorithmException e) {
                        LOG.error("Can't find SHA-1 implementation: " + e.getMessage());
                        throw new SignServerException("Can't find SHA-1 implementation", e);
                    }
                }

                AttributeTable signedAttributesTable = new AttributeTable(signedAttributes);
                DefaultSignedAttributeTableGenerator signedAttributeGenerator = new DefaultSignedAttributeTableGenerator(
                        signedAttributesTable);

                final String provider = cryptoToken.getProvider(ICryptoToken.PROVIDERUSAGE_SIGN);

                SignerInfoGeneratorBuilder signerInfoBuilder = new SignerInfoGeneratorBuilder(
                        new JcaDigestCalculatorProviderBuilder().setProvider("BC").build());
                signerInfoBuilder.setSignedAttributeGenerator(signedAttributeGenerator);

                JcaContentSignerBuilder contentSigner = new JcaContentSignerBuilder(signatureAlgo);
                contentSigner.setProvider(provider);

                final SignerInfoGenerator sig = signerInfoBuilder.build(contentSigner.build(crypto.getPrivateKey()),
                        new X509CertificateHolder(x509cert.getEncoded()));

                JcaCertStore cs = new JcaCertStore(certList);

                CMSTypedData cmspba = new CMSProcessableByteArray(content);
                CMSSignedData cmssd = MSAuthCodeCMSUtils.generate(cmspba, true, Arrays.asList(sig),
                        MSAuthCodeCMSUtils.getCertificatesFromStore(cs), Collections.emptyList(), ci);

                der = ASN1Primitive.fromByteArray(cmssd.getEncoded()).getEncoded();
            } finally {
                releaseCryptoInstance(crypto, requestContext);
            }

            // Log values
            logMap.put(ITimeStampLogger.LOG_TSA_TIME, String.valueOf(date.getTime()));
            logMap.put(ITimeStampLogger.LOG_TSA_TIMESOURCE, timeSrc.getClass().getSimpleName());

            final String archiveId = createArchiveId(requestbytes,
                    (String) requestContext.get(RequestContext.TRANSACTION_ID));

            final GenericSignResponse signResponse;
            byte[] signedbytes = Base64.encode(der, false);

            logMap.put(ITimeStampLogger.LOG_TSA_TIMESTAMPRESPONSE_ENCODED, new String(signedbytes));

            final Collection<? extends Archivable> archivables = Arrays.asList(
                    new DefaultArchivable(Archivable.TYPE_REQUEST, REQUEST_CONTENT_TYPE, requestbytes, archiveId),
                    new DefaultArchivable(Archivable.TYPE_RESPONSE, RESPONSE_CONTENT_TYPE, signedbytes, archiveId));

            if (signRequest instanceof GenericServletRequest) {
                signResponse = new GenericServletResponse(sReq.getRequestID(), signedbytes,
                        getSigningCertificate(signRequest, requestContext), archiveId, archivables,
                        RESPONSE_CONTENT_TYPE);
            } else {
                signResponse = new GenericSignResponse(sReq.getRequestID(), signedbytes,
                        getSigningCertificate(signRequest, requestContext), archiveId, archivables);
            }

            // The client can be charged for the request
            requestContext.setRequestFulfilledByWorker(true);

            return signResponse;

        } catch (IOException e) {
            final IllegalRequestException exception = new IllegalRequestException("IOException: " + e.getMessage(),
                    e);
            LOG.error("IOException: ", e);
            logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage());
            throw exception;
        } catch (CMSException e) {
            final SignServerException exception = new SignServerException(e.getMessage(), e);
            LOG.error("CMSException: ", e);
            logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage());
            throw exception;
        } catch (OperatorCreationException e) {
            final SignServerException exception = new SignServerException(e.getMessage(), e);
            LOG.error("OperatorCreationException: ", e);
            logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage());
            throw exception;
        } catch (CertificateEncodingException e) {
            final SignServerException exception = new SignServerException(e.getMessage(), e);
            LOG.error("CertificateEncodingException: ", e);
            logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage());
            throw exception;
        } catch (ArrayIndexOutOfBoundsException e) {
            // the BC base64 decoder doesn't check the the base64 input length...
            final IllegalRequestException exception = new IllegalRequestException(
                    "ArrayIndexOutOfBoundsException: " + e.getMessage(), e);
            LOG.error("ArrayIndexOutOfBoundsException: ", e);
            logMap.put(ITimeStampLogger.LOG_TSA_EXCEPTION, exception.getMessage());
            throw exception;
        }
    }

    /**
     * @return a time source interface expected to provide accurate time
     */
    private ITimeSource getTimeSource() throws SignServerException {
        if (timeSource == null) {
            try {
                String classpath = this.config.getProperties().getProperty(TIMESOURCE);
                if (classpath == null) {
                    classpath = DEFAULT_TIMESOURCE;
                }

                final Class<?> implClass = Class.forName(classpath);
                final Object obj = implClass.newInstance();
                timeSource = (ITimeSource) obj;
                timeSource.init(config.getProperties());

            } catch (ClassNotFoundException e) {
                throw new SignServerException("Class not found", e);
            } catch (IllegalAccessException iae) {
                throw new SignServerException("Illegal access", iae);
            } catch (InstantiationException ie) {
                throw new SignServerException("Instantiation error", ie);
            }
        }

        return timeSource;
    }

    /** Generates a number of serial number bytes. The number returned should
     * be a positive number.
     *
     * @return a BigInteger with a new random serial number.
     */
    public BigInteger getSerno() {
        if (random == null) {
            try {
                random = SecureRandom.getInstance(algorithm);
            } catch (NoSuchAlgorithmException e) {
                LOG.error(e);
            }
        }

        final byte[] sernobytes = new byte[8];
        boolean ok = false;
        BigInteger serno = null;
        while (!ok) {
            random.nextBytes(sernobytes);
            serno = new BigInteger(sernobytes).abs();

            // Must be within the range 0080000000000000 - 7FFFFFFFFFFFFFFF
            if ((serno.compareTo(LOWEST) >= 0) && (serno.compareTo(HIGHEST) <= 0)) {
                ok = true;
            }
        }
        return serno;
    }

    /**
     * @return True if each certificate in the certificate chain can be verified 
     * by the next certificate (if any). This does not check that the last 
     * certificate is a trusted certificate as the root certificate is normally 
     * not included.
     */
    private boolean validateChain() {
        boolean result = true;
        try {
            final List<Certificate> signingCertificateChain = getSigningCertificateChain();
            if (signingCertificateChain != null) {
                List<Certificate> chain = (List<Certificate>) signingCertificateChain;
                for (int i = 0; i < chain.size(); i++) {
                    Certificate subject = chain.get(i);

                    // If we have the issuer we can validate the certificate
                    if (chain.size() > i + 1) {
                        Certificate issuer = chain.get(i + 1);
                        try {
                            subject.verify(issuer.getPublicKey(), "BC");
                        } catch (CertificateException ex) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject);
                            }
                            result = false;
                        } catch (NoSuchAlgorithmException ex) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject);
                            }
                            result = false;
                        } catch (InvalidKeyException ex) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject);
                            }
                            result = false;
                        } catch (NoSuchProviderException ex) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject);
                            }
                            result = false;
                        } catch (SignatureException ex) {
                            if (LOG.isDebugEnabled()) {
                                LOG.debug("Certificate could not be verified: " + ex.getMessage() + ": " + subject);
                            }
                            result = false;
                        }
                    }
                }
            } else {
                // This would be a bug
                LOG.error("Certificate chain was not an list!");
                result = false;
            }
        } catch (CryptoTokenOfflineException ex) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Unable to get signer certificate or chain: " + ex.getMessage());
            }
            result = false;
        }
        return result;
    }

    @Override
    protected List<String> getFatalErrors() {
        final List<String> result = new LinkedList<String>();
        result.addAll(super.getFatalErrors());
        result.addAll(configErrors);

        try {
            // Check signer certificate chain if required
            if (!validChain) {
                result.add("Not strictly valid chain and " + REQUIREVALIDCHAIN + " specified");
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Signer " + workerId + ": " + REQUIREVALIDCHAIN
                            + " specified but the chain was not found valid");
                }
            }

            // Check if certificat has the required EKU
            final Certificate certificate = getSigningCertificate();
            try {
                if (certificate instanceof X509Certificate) {
                    final X509Certificate cert = (X509Certificate) certificate;
                    final List<String> ekus = cert.getExtendedKeyUsage();

                    if (ekus == null || !ekus.contains(KeyPurposeId.id_kp_timeStamping.getId())) {
                        result.add("Missing extended key usage timeStamping");
                    }
                    if (cert.getCriticalExtensionOIDs() == null || !cert.getCriticalExtensionOIDs()
                            .contains(org.bouncycastle.asn1.x509.X509Extension.extendedKeyUsage.getId())) {
                        result.add("The extended key usage extension must be present and marked as critical");
                    }
                    // if extended key usage contains timeStamping and also other
                    // usages
                    if (ekus != null && ekus.contains(KeyPurposeId.id_kp_timeStamping.getId()) && ekus.size() > 1) {
                        result.add("No other extended key usages than timeStamping is allowed");
                    }
                } else {
                    result.add("Unsupported certificate type");
                }
            } catch (CertificateParsingException ex) {
                result.add("Unable to parse certificate");
                if (LOG.isDebugEnabled()) {
                    LOG.debug("Signer " + workerId + ": Unable to parse certificate: " + ex.getMessage());
                }
            }
        } catch (CryptoTokenOfflineException ex) {
            result.add("No signer certificate available");
            if (LOG.isDebugEnabled()) {
                LOG.debug("Signer " + workerId + ": Could not get signer certificate: " + ex.getMessage());
            }
        }

        // check time source
        if (timeSource.getGenTime() == null) {
            result.add("Time source not available");
            if (LOG.isDebugEnabled()) {
                LOG.debug("Signer " + workerId + ": time source not available");
            }
        }

        return result;
    }

}