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

Java tutorial

Introduction

Here is the source code for org.signserver.module.tsa.MSAuthCodeTimeStampSignerTest.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.math.BigInteger;
import java.security.KeyPair;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import junit.framework.TestCase;
import static junit.framework.TestCase.assertEquals;
import org.apache.log4j.Logger;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1Set;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.cms.ContentInfo;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.Time;
import org.bouncycastle.asn1.x509.X509Extension;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.util.encoders.Base64;
import org.ejbca.util.CertTools;
import org.signserver.common.CryptoTokenOfflineException;
import org.signserver.common.GenericSignRequest;
import org.signserver.common.GenericSignResponse;
import org.signserver.common.IllegalRequestException;
import org.signserver.common.ProcessRequest;
import org.signserver.common.RequestContext;
import org.signserver.common.SignServerUtil;
import org.signserver.common.WorkerConfig;
import org.signserver.ejb.interfaces.IGlobalConfigurationSession;
import org.signserver.server.SignServerContext;
import org.signserver.server.ZeroTimeSource;
import org.signserver.server.cryptotokens.HardCodedCryptoToken;
import org.signserver.server.cryptotokens.HardCodedCryptoTokenAliases;
import org.signserver.server.cryptotokens.ICryptoToken;
import org.signserver.server.log.LogMap;
import org.signserver.test.utils.builders.CertBuilder;
import org.signserver.test.utils.builders.CertExt;
import org.signserver.test.utils.builders.CryptoUtils;

import org.signserver.test.utils.mock.GlobalConfigurationSessionMock;
import org.signserver.test.utils.mock.MockedCryptoToken;
import org.signserver.test.utils.mock.WorkerSessionMock;
import org.signserver.testutils.TestUtils;

/**
 * 
 * Unit test testing the functionallity of the MSAuthCodeTimeStampSigner by
 * using a prerecorded request from the "signtool" CLI tool from Microsoft's SDK.
 * The tests checks that the response contains the right content type, timestamp is correctly set 
 * and uses the signature algorithm as set.
 *
 * @author Marcus Lundblad
 * @version $Id: MSAuthCodeTimeStampSignerTest.java 5740 2015-02-19 15:17:02Z netmackan $
 */
public class MSAuthCodeTimeStampSignerTest extends TestCase {

    /** Logger for this class */
    private static final Logger LOG = Logger.getLogger(MSAuthCodeTimeStampSignerTest.class);

    private static int SIGNER_ID = 1000;
    private static int REQUEST_ID = 42;
    private static final String REQUEST_DATA = "MIIBIwYKKwYBBAGCNwMCATCCARMGCSqGSIb3DQEHAaCCAQQEggEAVVSpOKf9zJYc"
            + "tyvqgeHfO9JkobPYihUZcW9TbYzAUiJGEsElNCnLUaO0+MZG0TS7hlzqKKvrdXc7"
            + "O/8C7c8YyjYF5YrLiaYS8cw3VbaQ2M1NWsLGzxF1pxsR9sMDJvfrryPaWj4eTi3Y"
            + "UqRNS+GTa4quX4xbmB0KqMpCtrvuk4S9cgaJGwxmSE7N3omzvERTUxp7nVSHtms5"
            + "lVMb082JFlABT1/o2mL5O6qFG119JeuS1+ZiL1AEy//gRs556OE1TB9UEQU2bFUm"
            + "zBD4VHvkOOB/7X944v9lmK5y9sFv+vnf/34catL1A+ZNLwtd1Qq2VirqJxRK/T61" + "QoSWj4rGpw==";

    private static final String SIGNED_DATA_OID = "1.2.840.113549.1.7.2";
    private static final String CONTENT_TYPE_OID = "1.2.840.113549.1.9.3";
    private static final String SIGNING_TIME_OID = "1.2.840.113549.1.9.5";
    private static final String MESSAGE_DIGEST_OID = "1.2.840.113549.1.9.4";
    private static final String SHA1_OID = "1.3.14.3.2.26";
    private static final String SHA256_OID = "2.16.840.1.101.3.4.2.1";

    @Override
    protected void setUp() throws Exception {
        super.setUp();
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    /**
     * Performs test using specified signature algorithm, digest algorithm and with the optional SigningCertificate attribute included or not included.
     * 
     * The SigningCertificate attribute is specified in RFC 2634.
     * 
     * SigningCertificate ::=  SEQUENCE {
     *  certs        SEQUENCE OF ESSCertID,
     *  policies     SEQUENCE OF PolicyInformation OPTIONAL
     * }
     *
     * id-aa-signingCertificate OBJECT IDENTIFIER ::= { iso(1)
     *  member-body(2) us(840) rsadsi(113549) pkcs(1) pkcs9(9)
     *  smime(16) id-aa(2) 12 }
     *
     * ESSCertID ::=  SEQUENCE {
     *   certHash                 Hash,
     *   issuerSerial             IssuerSerial OPTIONAL
     * }
     * Hash ::= OCTET STRING -- SHA1 hash of entire certificate
     *
     * IssuerSerial ::= SEQUENCE {
     *   issuer                   GeneralNames,
     *   serialNumber             CertificateSerialNumber
     * }
     * 
     * @param signingAlgo Signature algorithm to use
     * @param expectedDigestOID Expected digest OID
     * @param requestData Request data to test with
     * @param includeSigningCertAttr If true, include and test the SigningCertificate attribute
     * @throws Exception
     */
    private void testProcessDataWithAlgo(final String signingAlgo, final String expectedDigestOID,
            final byte[] requestData, final boolean includeSigningCertAttr, final String includeCertificateLevels)
            throws Exception {
        SignServerUtil.installBCProvider();

        final String CRYPTOTOKEN_CLASSNAME = "org.signserver.server.cryptotokens.HardCodedCryptoToken";

        final ProcessRequest signRequest;

        final GlobalConfigurationSessionMock globalConfig = new GlobalConfigurationSessionMock();
        final WorkerSessionMock workerMock = new WorkerSessionMock(globalConfig);

        final WorkerConfig config = new WorkerConfig();
        config.setProperty("NAME", "TestMSAuthCodeTimeStampSigner");
        config.setProperty("AUTHTYPE", "NOAUTH");
        config.setProperty("TIMESOURCE", "org.signserver.server.ZeroTimeSource");
        config.setProperty("SIGNATUREALGORITHM", signingAlgo);
        config.setProperty("DEFAULTKEY", HardCodedCryptoTokenAliases.KEY_ALIAS_1);

        if (includeSigningCertAttr) {
            config.setProperty("INCLUDE_SIGNING_CERTIFICATE_ATTRIBUTE", "true");
        }

        if (includeCertificateLevels != null) {
            config.setProperty(WorkerConfig.PROPERTY_INCLUDE_CERTIFICATE_LEVELS, includeCertificateLevels);
        }

        final MSAuthCodeTimeStampSigner worker = new MSAuthCodeTimeStampSigner() {
            @Override
            protected IGlobalConfigurationSession.IRemote getGlobalConfigurationSession() {
                return globalConfig;
            }
        };

        workerMock.setupWorker(SIGNER_ID, CRYPTOTOKEN_CLASSNAME, config, worker);
        workerMock.reloadConfiguration(SIGNER_ID);

        // if the INCLUDE_CERTIFICATE_LEVELS property has been set,
        // check that it gives a not supported error
        if (includeCertificateLevels != null) {
            final List<String> errors = worker.getFatalErrors();

            assertTrue("Should contain config error",
                    errors.contains(WorkerConfig.PROPERTY_INCLUDE_CERTIFICATE_LEVELS + " is not supported."));
            return;
        }

        // create sample hard-coded request
        signRequest = new GenericSignRequest(REQUEST_ID, requestData);

        final RequestContext requestContext = new RequestContext();
        GenericSignResponse resp = (GenericSignResponse) workerMock.process(SIGNER_ID, signRequest, requestContext);

        // check that the response contains the needed attributes
        byte[] buf = resp.getProcessedData();
        ASN1Sequence asn1seq = ASN1Sequence.getInstance(Base64.decode(buf));

        ASN1ObjectIdentifier oid = ASN1ObjectIdentifier.getInstance(asn1seq.getObjectAt(0));
        ASN1TaggedObject ato = ASN1TaggedObject.getInstance(asn1seq.getObjectAt(1));

        assertEquals("Invalid OID in response", SIGNED_DATA_OID, oid.getId());

        ASN1Sequence asn1seq1 = ASN1Sequence.getInstance(ato.getObject());

        ASN1Set asn1set = ASN1Set.getInstance(asn1seq1.getObjectAt(4));
        ASN1Sequence asn1seq2 = ASN1Sequence.getInstance(asn1set.getObjectAt(0));
        ASN1TaggedObject ato1 = ASN1TaggedObject.getInstance(asn1seq2.getObjectAt(3));
        ASN1Sequence asn1seq3 = ASN1Sequence.getInstance(ato1.getObject());
        ASN1Sequence asn1seq4 = ASN1Sequence.getInstance(asn1seq3.getObjectAt(0));
        ASN1Sequence asn1seq5 = ASN1Sequence.getInstance(asn1seq3.getObjectAt(1));
        ASN1Sequence asn1seq6 = ASN1Sequence.getInstance(asn1seq3.getObjectAt(2));

        final X509Certificate cert = (X509Certificate) CertTools
                .getCertfromByteArray(HardCodedCryptoToken.certbytes1);
        // expected serial number
        final BigInteger sn = cert.getSerialNumber();

        // if INCLUDE_SIGNING_CERTIFICATE_ATTRIBUTE is set to false, the attribute should not be included
        if (!includeSigningCertAttr) {
            assertEquals("Number of attributes", 3, asn1seq3.size());
        } else {
            final ASN1Sequence scAttr = ASN1Sequence.getInstance(asn1seq3.getObjectAt(3));
            TestUtils.checkSigningCertificateAttribute(scAttr, cert);
        }

        ASN1ObjectIdentifier ctOID = ASN1ObjectIdentifier.getInstance(asn1seq4.getObjectAt(0));
        assertEquals("Invalid OID for content type", CONTENT_TYPE_OID, ctOID.getId());

        ASN1ObjectIdentifier stOID = ASN1ObjectIdentifier.getInstance(asn1seq5.getObjectAt(0));
        assertEquals("Invalid OID for signing time", SIGNING_TIME_OID, stOID.getId());

        ASN1ObjectIdentifier mdOID = ASN1ObjectIdentifier.getInstance(asn1seq6.getObjectAt(0));
        assertEquals("Invalid OID for content type", MESSAGE_DIGEST_OID, mdOID.getId());

        // get signing time from response
        ASN1Set set = ASN1Set.getInstance(asn1seq5.getObjectAt(1));
        ASN1Encodable t = set.getObjectAt(0);
        Time t2 = Time.getInstance(t);
        Date d = t2.getDate();

        // the expected time (the "starting point" of time according to java.util.Date, consistent with the behavior of ZeroTimeSource
        Date d0 = new Date(0);

        assertEquals("Unexpected signing time in response", d0, d);

        // check expected signing algo
        ASN1Set set1 = ASN1Set.getInstance(asn1seq1.getObjectAt(1));
        ASN1Sequence asn1seq7 = ASN1Sequence.getInstance(set1.getObjectAt(0));
        ASN1ObjectIdentifier algOid = ASN1ObjectIdentifier.getInstance(asn1seq7.getObjectAt(0));

        assertEquals("Unexpected digest OID in response", expectedDigestOID, algOid.getId());

        // check that the request is included
        final CMSSignedData signedData = new CMSSignedData(asn1seq.getEncoded());
        final byte[] content = (byte[]) signedData.getSignedContent().getContent();

        final ASN1Sequence seq = ASN1Sequence.getInstance(Base64.decode(requestData));
        final ASN1Sequence seq2 = ASN1Sequence.getInstance(seq.getObjectAt(1));
        final ASN1TaggedObject tag = ASN1TaggedObject.getInstance(seq2.getObjectAt(1));
        final ASN1OctetString data = ASN1OctetString.getInstance(tag.getObject());

        assertTrue("Contains request data", Arrays.equals(data.getOctets(), content));

        // check the signing certificate
        final X509Certificate signercert = (X509Certificate) resp.getSignerCertificate();
        assertEquals("Serial number", sn, signercert.getSerialNumber());
        assertEquals("Issuer", cert.getIssuerDN(), signercert.getIssuerDN());

        // check ContentInfo, according to the Microsoft specification, the contentInfo in the response is
        // identical to the contentInfo in the request
        final ContentInfo expCi = new ContentInfo(seq2);
        final ContentInfo ci = new ContentInfo(ASN1Sequence.getInstance(asn1seq1.getObjectAt(2)));

        assertEquals("Content info should match the request", expCi, ci);

        // Get signers
        final Collection signers = signedData.getSignerInfos().getSigners();
        final SignerInformation signer = (SignerInformation) signers.iterator().next();

        // Verify using the signer's certificate
        assertTrue("Verification using signer certificate", signer.verify(signercert.getPublicKey(), "BC"));

        // Check that the time source is being logged
        LogMap logMap = LogMap.getInstance(requestContext);
        assertEquals("timesource", ZeroTimeSource.class.getSimpleName(), logMap.get("TSA_TIMESOURCE"));

        assertNotNull("response", logMap.get(ITimeStampLogger.LOG_TSA_TIMESTAMPRESPONSE_ENCODED));
        assertEquals("log line doesn't contain newlines", -1,
                logMap.get(ITimeStampLogger.LOG_TSA_TIMESTAMPRESPONSE_ENCODED).lastIndexOf('\n'));
    }

    /**
     * Test of processData method, of class MSAuthCodeTimeStampSigner.
     */
    public void testProcessDataSHA1withRSA() throws Exception {
        testProcessDataWithAlgo("SHA1withRSA", SHA1_OID, REQUEST_DATA.getBytes(), false, null);
    }

    public void testProcessDataSHA256withRSA() throws Exception {
        testProcessDataWithAlgo("SHA256withRSA", SHA256_OID, REQUEST_DATA.getBytes(), false, null);
    }

    /**
     * Test with requestData with zero length. Shall give an IllegalRequestException.
     * @throws Exception
     */
    public void testEmptyRequest() throws Exception {
        try {
            testProcessDataWithAlgo("SHA1withRSA", SHA1_OID, new byte[0], false, null);
        } catch (IllegalRequestException e) {
            // expected
        } catch (Exception e) {
            fail("Unexpected exception thrown: " + e.getClass().getName());
        }
    }

    /**
     * Test with an invalid requestData. Shall give an IllegalRequestException.
     * @throws Exception
     */
    public void testBogusRequest() throws Exception {
        try {
            testProcessDataWithAlgo("SHA1withRSA", SHA1_OID, "bogus request".getBytes(), false, null);
        } catch (IllegalRequestException e) {
            // expected
        } catch (Exception e) {
            fail("Unexpected exception thrown: " + e.getClass().getName());
        }
    }

    /**
     * Test with a null requestData. Shall give an IllegalRequestException.
     * @throws Exception
     */
    public void testNullRequest() throws Exception {
        try {
            testProcessDataWithAlgo("SHA1withRSA", SHA1_OID, null, false, null);
        } catch (IllegalRequestException e) {
            // expected
        } catch (Exception e) {
            fail("Unexpected exception thrown: " + e.getClass().getName());
        }
    }

    /**
     * Test with the signingCertificate attribute included.
     * 
     * @throws Exception
     */
    public void testIncludeSigningCertificateAttribute() throws Exception {
        testProcessDataWithAlgo("SHA1withRSA", SHA1_OID, REQUEST_DATA.getBytes(), true, null);
    }

    /**
     * Test that setting INCLUDE_CERTIFICATE_LEVELS gives
     * a config error, as this is not supported by this
     * signer.
     * 
     * @throws Exception
     */
    public void test0IncludeCertificateLevelsNotPermitted() throws Exception {
        testProcessDataWithAlgo("SHA1withRSA", SHA1_OID, null, false, "2");
    }

    /**
     * Test that setting a signer certificate with no extended key usage
     * results in a configuration error.
     * 
     * @throws Exception 
     */
    public void testWithNoEKU() throws Exception {
        testWithEKUs(null, false, true, "Missing extended key usage timeStamping");
    }

    /**
     * Test that setting a signer certificate with extended key usage
     * timeStamping set as non-critical results in a configuration error.
     * 
     * @throws Exception 
     */
    public void testWithTimestampingEKUNoCritical() throws Exception {
        testWithEKUs(new KeyPurposeId[] { KeyPurposeId.id_kp_timeStamping }, false, true,
                "The extended key usage extension must be present and marked as critical");
    }

    /**
     * Test that setting a signer certificate with extended key usage
     * timeStamping set as critical results in no configuration error.
     * 
     * @throws Exception 
     */
    public void testWithTimestampingEKUCritical() throws Exception {
        testWithEKUs(new KeyPurposeId[] { KeyPurposeId.id_kp_timeStamping }, true, false, null);
    }

    /**
     * Test that setting a signer certificate with additional extended key usage
     * in addition to timeStaming results in a configuration error.
     * 
     * @throws Exception 
     */
    public void testWithAdditionalEKU() throws Exception {
        testWithEKUs(new KeyPurposeId[] { KeyPurposeId.id_kp_timeStamping, KeyPurposeId.id_kp_emailProtection },
                true, true, "No other extended key usages than timeStamping is allowed");
    }

    /**
     * Internal helper method setting up a mocked signer with configurable
     * signer certificate extended key usages and expected fatal errors.
     * 
     * @param ekus Array of extended keyusages, null if no extended key usage should be set
     * @param critical True if the extended key usage should be marked as critical
     * @param expectedFailure True if fatal errors is expected to contain errors
     * @param expectedErrorMessage Error message expected in the list of fatal
     *                             error, if null or empty, don't check error message
     * @throws Exception 
     */
    private void testWithEKUs(final KeyPurposeId[] ekus, final boolean critical, final boolean expectedFailure,
            final String expectedErrorMessage) throws Exception {
        final KeyPair signerKeyPair = CryptoUtils.generateRSA(1024);
        final String signatureAlgorithm = "SHA1withRSA";
        final CertBuilder certBuilder = new CertBuilder().setSelfSignKeyPair(signerKeyPair).setNotBefore(new Date())
                .setSignatureAlgorithm(signatureAlgorithm);

        if (ekus != null && ekus.length > 0) {
            certBuilder.addExtension(
                    new CertExt(X509Extension.extendedKeyUsage, critical, new ExtendedKeyUsage(ekus)));
        }

        final Certificate[] certChain = new Certificate[] {
                new JcaX509CertificateConverter().getCertificate(certBuilder.build()) };
        final Certificate signerCertificate = certChain[0];
        final MockedCryptoToken token = new MockedCryptoToken(signerKeyPair.getPrivate(), signerKeyPair.getPublic(),
                signerCertificate, Arrays.asList(certChain), "BC");

        final MSAuthCodeTimeStampSigner instance = new MockedMSAuthCodeTimeStampSigner(token);

        instance.init(1, new WorkerConfig(), new SignServerContext(), null);

        final List<String> fatalErrors = instance.getFatalErrors();

        if (expectedFailure) {
            assertFalse("Should report fatal error", fatalErrors.isEmpty());
        }

        if (expectedErrorMessage != null && !expectedErrorMessage.isEmpty()) {
            assertTrue("Should contain error: " + fatalErrors, fatalErrors.contains(expectedErrorMessage));
        }
    }

    /**
     * Mocked signer using a mocked crypto token.
     * 
     */
    private static class MockedMSAuthCodeTimeStampSigner extends MSAuthCodeTimeStampSigner {
        private final MockedCryptoToken mockedToken;

        /**
         * Create a mocked signer using the provided mocked token.
         * 
         * @param mockedToken 
         */
        public MockedMSAuthCodeTimeStampSigner(final MockedCryptoToken mockedToken) {
            this.mockedToken = mockedToken;
        }

        @Override
        public Certificate getSigningCertificate(final ProcessRequest request, final RequestContext context)
                throws CryptoTokenOfflineException {
            return mockedToken.getCertificate(ICryptoToken.PURPOSE_SIGN);
        }

        @Override
        public List<Certificate> getSigningCertificateChain(final ProcessRequest request,
                final RequestContext context) throws CryptoTokenOfflineException {
            return mockedToken.getCertificateChain(ICryptoToken.PURPOSE_SIGN);
        }

        @Override
        public ICryptoToken getCryptoToken() {
            return mockedToken;
        }
    }
}