com.otterca.common.crypto.acceptance.X509CertificateBuilderAcceptanceTest.java Source code

Java tutorial

Introduction

Here is the source code for com.otterca.common.crypto.acceptance.X509CertificateBuilderAcceptanceTest.java

Source

/*
 * This code was written by Bear Giles <bgiles@otterca.com>and he
 * licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Any contributions made by others are licensed to this project under
 * one or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * 
 * Copyright (c) 2012 Bear Giles <bgiles@otterca.com>
 */
package com.otterca.common.crypto.acceptance;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;

import java.io.IOException;
import java.math.BigInteger;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;

import javax.naming.InvalidNameException;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.codec.digest.DigestUtils;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.otterca.common.crypto.GeneralSubtree;
import com.otterca.common.crypto.X509CertificateBuilder;
import com.otterca.common.crypto.X509CertificateBuilderException;
import com.otterca.common.crypto.X509CertificateBuilderImpl;
import com.otterca.common.crypto.X509CertificateUtil;
import com.otterca.common.crypto.X509CertificateUtilImpl;

/**
 * Acceptance test for X509CertificateBuilder. This class should have no
 * dependency on this implementation other than the constructor. Technically it
 * should be in a separate module since it can be reused by other crypto API
 * implementations but in practice it's not worth the effort while there's only
 * a single implementation.
 * 
 * @author bgiles@otterca.com
 */
public class X509CertificateBuilderAcceptanceTest {
    // standard values
    private static final String PRIVATE_KEY_USAGE_PERIOD_OID = "2.5.29.16";
    private static final String NAME_CONSTRAINTS_OID = "2.5.29.30";
    private static final String INHIBIT_ANY_POLICY_OID = "2.5.29.54";
    private static final String AUTHORITY_INFO_ACCESS_OID = "1.3.6.1.5.5.7.1.1";
    private static final String SUBJECT_INFO_ACCESS_OID = "1.3.6.1.5.5.7.1.11";

    // test values
    private static final String SUBJECT_NAME = "CN=subject";
    private static final String ISSUER_NAME = "CN=issuer";
    private static final String GRANDFATHER_NAME = "CN=grandfather";
    // private static final char[] KEY_PASSWORD = "password".toCharArray();

    private final com.otterca.common.crypto.GeneralName.URI expectedGeneralNameUri1;
    private final com.otterca.common.crypto.GeneralName.URI expectedGeneralNameUri2;
    private final com.otterca.common.crypto.GeneralName.Directory expectedGeneralNameDir;
    private final com.otterca.common.crypto.GeneralName.Email expectedGeneralNameEmail;
    private final com.otterca.common.crypto.GeneralName.DNS expectedGeneralNameDns;
    private final com.otterca.common.crypto.GeneralName.IpAddress expectedGeneralNameIpAddress;

    private final X509CertificateUtil certUtil;
    private final Calendar notBefore;
    private final Calendar notAfter;
    private final KeyPair keyPair;
    private final KeyPair issuerKeyPair;
    private final KeyPair grandfatherKeyPair;
    private BigInteger serial = BigInteger.ONE;
    private X509CertificateBuilder builder;

    /**
     * Default constructor.
     * 
     * @throws Exception
     */
    protected X509CertificateBuilderAcceptanceTest() throws GeneralSecurityException, InvalidNameException,
            URISyntaxException, UnknownHostException, IOException {
        certUtil = new X509CertificateUtilImpl();

        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

        // create key pairs. this is for testing so we use 512-bit keys for
        // speed.
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA");
        keyPairGen.initialize(512);
        keyPair = keyPairGen.generateKeyPair();
        issuerKeyPair = keyPairGen.generateKeyPair();
        grandfatherKeyPair = keyPairGen.generateKeyPair();

        notBefore = Calendar.getInstance();
        notBefore.set(Calendar.MINUTE, 0);
        notBefore.set(Calendar.SECOND, 0);
        notBefore.set(Calendar.MILLISECOND, 0);
        notAfter = Calendar.getInstance();
        notAfter.setTime(notBefore.getTime());
        notAfter.add(Calendar.YEAR, 1);

        expectedGeneralNameUri1 = new com.otterca.common.crypto.GeneralName.URI("http://example.com");
        expectedGeneralNameUri2 = new com.otterca.common.crypto.GeneralName.URI("ldap://example.net");
        expectedGeneralNameDir = new com.otterca.common.crypto.GeneralName.Directory("C=US,ST=AK,C=Anchorage");
        expectedGeneralNameEmail = new com.otterca.common.crypto.GeneralName.Email("bob@example.com");
        expectedGeneralNameDns = new com.otterca.common.crypto.GeneralName.DNS("example.com");
        expectedGeneralNameIpAddress = new com.otterca.common.crypto.GeneralName.IpAddress("127.0.0.1");
    }

    /**
     * Per-method initialization - get fresh certificate builder.
     */
    @BeforeMethod
    public void init() throws GeneralSecurityException {
        builder = new X509CertificateBuilderImpl();
    }

    /**
     * Populate a builder with standard values used in testing.
     * 
     * @param builder
     */
    public void populate(X509CertificateBuilder builder) {
        serial = serial.add(BigInteger.ONE);
        builder.setSerialNumber(serial);
        builder.setSubject(SUBJECT_NAME);
        builder.setIssuer(SUBJECT_NAME);
        builder.setNotBefore(notBefore.getTime());
        builder.setNotAfter(notAfter.getTime());
        builder.setPublicKey(keyPair.getPublic());
    }

    /**
     * Test builder with issuer certificate.
     * 
     * @throws Exception
     */
    @Test
    public void testBuilderCertWithValidIssuer() throws GeneralSecurityException {
        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(ISSUER_NAME);
        builder.setPublicKey(issuerKeyPair.getPublic());
        builder.setBasicConstraints(true);

        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        // perform basic validation.
        issuer.verify(issuerKeyPair.getPublic());

        // verify the basics
        assertEquals(issuer.getSerialNumber(), serial);
        assertEquals(issuer.getSubjectDN().getName(), ISSUER_NAME);
        assertEquals(issuer.getIssuerDN().getName(), ISSUER_NAME);
        assertEquals(issuer.getNotBefore(), notBefore.getTime());
        assertEquals(issuer.getNotAfter(), notAfter.getTime());
        // assertEquals(issuer.getPublicKey(), issuerKeyPair.getPublic());
        // FIXME: returns null

        builder.reset();

        // create subject certificate
        populate(builder);
        builder.setIssuer(issuer);

        X509Certificate cert = builder.build(keyPair.getPrivate());

        // perform basic validation.
        cert.verify(keyPair.getPublic());

        // verify the basics
        assertEquals(cert.getSerialNumber(), serial);
        assertEquals(cert.getSubjectDN().getName(), SUBJECT_NAME);
        assertEquals(cert.getIssuerDN().getName(), ISSUER_NAME);
        assertEquals(cert.getNotBefore(), notBefore.getTime());
        assertEquals(cert.getNotAfter(), notAfter.getTime());
        // assertEquals(cert.getPublicKey(), keyPair.getPublic()); FIXME:
        // returns null
    }

    /**
     * Test builder with nonsigning issuer certificate.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderCertWithNonSigningIssuer() throws GeneralSecurityException {
        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(ISSUER_NAME);
        builder.setPublicKey(issuerKeyPair.getPublic());

        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        builder.reset();

        // create subject certificate
        populate(builder);
        builder.setIssuer(issuer);

        builder.build(keyPair.getPrivate());

        // FIXME
        // assertTrue(
        // cert.getNonCriticalExtensionOIDs().contains(X509Extensions.NameConstraints.getId()),
        // "certificate does not contain expected Name Constraints extension");
    }

    /**
     * Test builder with expired issuer certificate.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderCertWithExpiredIssuer() throws GeneralSecurityException {
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        cal.set(Calendar.YEAR, 2000);

        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(ISSUER_NAME);
        builder.setNotBefore(cal.getTime());
        cal.add(Calendar.YEAR, 1);
        builder.setNotAfter(cal.getTime());
        builder.setPublicKey(issuerKeyPair.getPublic());

        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        builder.reset();

        // create subject certificate
        populate(builder);
        builder.setIssuer(issuer);

        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder with not-yet-valid issuer certificate.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderCertIssuerNotYetValid() throws GeneralSecurityException {
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
        cal.add(Calendar.YEAR, 10);

        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(ISSUER_NAME);
        builder.setNotBefore(cal.getTime());
        cal.add(Calendar.YEAR, 1);
        builder.setNotAfter(cal.getTime());
        builder.setPublicKey(issuerKeyPair.getPublic());

        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        builder.reset();

        // create subject certificate
        populate(builder);
        builder.setIssuer(issuer);

        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder with bad notBefore/notAfter values.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderCertWithBadDates() throws GeneralSecurityException {
        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(ISSUER_NAME);
        builder.setPublicKey(issuerKeyPair.getPublic());

        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        builder.reset();

        // create subject certificate
        populate(builder);
        builder.setSubject(SUBJECT_NAME);
        builder.setIssuer(issuer);

        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder with unlimited pathlength value.
     * 
     * @throws Exception
     */
    @Test
    public void testBuilderCertWithUnlimitedPathLength() throws GeneralSecurityException {
        // create grandparent certificate
        populate(builder);
        builder.setSubject(GRANDFATHER_NAME);
        builder.setIssuer(GRANDFATHER_NAME);
        builder.setPublicKey(grandfatherKeyPair.getPublic());
        builder.setBasicConstraints(true);

        X509Certificate grandfather = builder.build(grandfatherKeyPair.getPrivate());

        assertEquals(grandfather.getBasicConstraints(), Integer.MAX_VALUE);

        builder.reset();

        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(grandfather);
        builder.setBasicConstraints(true, 0);
        builder.setPublicKey(issuerKeyPair.getPublic());

        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        assertEquals(issuer.getBasicConstraints(), 0);
    }

    /**
     * Test builder with sufficent pathlength value.
     * 
     * @throws Exception
     */
    @Test
    public void testBuilderCertWithSufficientPathLength() throws GeneralSecurityException {
        // create grandparent certificate
        populate(builder);
        builder.setSubject(GRANDFATHER_NAME);
        builder.setIssuer(GRANDFATHER_NAME);
        builder.setPublicKey(grandfatherKeyPair.getPublic());
        builder.setBasicConstraints(true, 1);

        X509Certificate grandfather = builder.build(grandfatherKeyPair.getPrivate());

        assertEquals(grandfather.getBasicConstraints(), 1);

        builder.reset();

        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(grandfather);
        builder.setBasicConstraints(true, 0);
        builder.setPublicKey(issuerKeyPair.getPublic());

        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        assertEquals(issuer.getBasicConstraints(), 0);

        builder.reset();

        // create subject certificate
        populate(builder);
        builder.setSubject(SUBJECT_NAME);
        builder.setIssuer(issuer);

        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertEquals(cert.getBasicConstraints(), -1);
    }

    /**
     * Test builder with insufficent pathlength value.
     * 
     * @throws Exception
     *             // assertTrue( //
     *             cert.getNonCriticalExtensionOIDs().contains(
     *             X509Extensions.NameConstraints.getId()), //
     *             "certificate does not contain expected Name Constraints extension"
     *             );
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderCertWithBadPathLength() throws GeneralSecurityException {
        // create grandparent certificate
        populate(builder);
        builder.setSubject(GRANDFATHER_NAME);
        builder.setIssuer(GRANDFATHER_NAME);
        builder.setPublicKey(grandfatherKeyPair.getPublic());
        builder.setBasicConstraints(true, 0);

        X509Certificate grandfather = builder.build(grandfatherKeyPair.getPrivate());

        assertEquals(grandfather.getBasicConstraints(), 0);

        builder.reset();

        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(grandfather);
        builder.setBasicConstraints(true, 0);
        builder.setPublicKey(issuerKeyPair.getPublic());

        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        assertEquals(issuer.getBasicConstraints(), -1);

        builder.reset();

        // create subject certificate
        populate(builder);
        builder.setIssuer(issuer);

        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder when missing serial number.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderMissingSerialNumber() throws GeneralSecurityException {
        serial = serial.add(BigInteger.ONE);
        builder.setSubject(SUBJECT_NAME);
        builder.setIssuer(SUBJECT_NAME);
        builder.setNotBefore(notBefore.getTime());
        builder.setNotAfter(notAfter.getTime());
        builder.setPublicKey(keyPair.getPublic());
        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder when missing subject.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderMissingSubject() throws GeneralSecurityException {
        serial = serial.add(BigInteger.ONE);
        builder.setSerialNumber(serial);
        builder.setIssuer(ISSUER_NAME);
        builder.setNotBefore(notBefore.getTime());
        builder.setNotAfter(notAfter.getTime());
        builder.setPublicKey(keyPair.getPublic());
        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder when missing issuer.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderMissingIssuer() throws GeneralSecurityException {
        serial = serial.add(BigInteger.ONE);
        builder.setSerialNumber(serial);
        builder.setSubject(SUBJECT_NAME);
        builder.setNotBefore(notBefore.getTime());
        builder.setNotAfter(notAfter.getTime());
        builder.setPublicKey(keyPair.getPublic());
        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder when missing 'not before' date.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderMissingNotBefore() throws GeneralSecurityException {
        serial = serial.add(BigInteger.ONE);
        builder.setSerialNumber(serial);
        builder.setSubject(SUBJECT_NAME);
        builder.setIssuer(SUBJECT_NAME);
        builder.setNotAfter(notAfter.getTime());
        builder.setPublicKey(keyPair.getPublic());
        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder when missing 'not after' date.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderMissingNotAfter() throws GeneralSecurityException {
        serial = serial.add(BigInteger.ONE);
        builder.setSerialNumber(serial);
        builder.setSubject(SUBJECT_NAME);
        builder.setIssuer(SUBJECT_NAME);
        builder.setNotBefore(notBefore.getTime());
        builder.setPublicKey(keyPair.getPublic());
        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder when missing public key.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderMissingPublicKey() throws GeneralSecurityException {
        serial = serial.add(BigInteger.ONE);
        builder.setSerialNumber(serial);
        builder.setSubject(SUBJECT_NAME);
        builder.setIssuer(SUBJECT_NAME);
        builder.setNotBefore(notBefore.getTime());
        builder.setNotAfter(notAfter.getTime());
        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder when 'notBefore' date is after 'notAfter' date.
     * 
     * @throws Exception
     */
    @Test(expectedExceptions = X509CertificateBuilderException.class)
    public void testBuilderBadDates() throws GeneralSecurityException {
        serial = serial.add(BigInteger.ONE);
        builder.setSerialNumber(serial);
        builder.setSubject(SUBJECT_NAME);
        builder.setIssuer(SUBJECT_NAME);
        builder.setNotBefore(notAfter.getTime());
        builder.setNotAfter(notBefore.getTime());
        builder.setPublicKey(keyPair.getPublic());
        builder.build(keyPair.getPrivate());
    }

    /**
     * Test builder with 'inhibitAnyPolicy'.
     * 
     * @throws Exception
     */
    @Test
    public void testInhibitAnyPolicy() throws GeneralSecurityException {
        // make sure there's no extension by default
        populate(builder);
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getCriticalExtensionOIDs().contains(INHIBIT_ANY_POLICY_OID),
                "certificate contains unexpected InhibitAnyPolicy extension");
        assertNull(certUtil.getInhibitAnyPolicy(cert));

        // test with final cert - should be removed
        builder.reset();
        populate(builder);
        builder.setInhibitAnyPolicy(6);
        cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getCriticalExtensionOIDs().contains(INHIBIT_ANY_POLICY_OID),
                "certificate contains unexpected InhibitAnyPolicy extension");
        assertNull(certUtil.getInhibitAnyPolicy(cert));

        // now try it with CA cert - should work.
        builder.reset();
        populate(builder);
        builder.setBasicConstraints(true);
        builder.setInhibitAnyPolicy(6);
        cert = builder.build(keyPair.getPrivate());
        assertTrue(cert.getCriticalExtensionOIDs().contains(INHIBIT_ANY_POLICY_OID),
                "certificate does not contain expected InhibitAnyPolicy extension");
        assertEquals(certUtil.getInhibitAnyPolicy(cert), Integer.valueOf(6));

        // TODO also check behavior when issuer has inhibit any policy set.
    }

    /**
     * Test builder with no 'Names'
     */
    @Test
    public void testBuilderNoNames() throws GeneralSecurityException {
        populate(builder);
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getNonCriticalExtensionOIDs().contains(NAME_CONSTRAINTS_OID),
                "certificate contains unexpected Name Constraints extension");

        assertTrue(certUtil.getPermittedNames(cert).isEmpty());
        assertTrue(certUtil.getExcludedNames(cert).isEmpty());

        builder.reset();
    }

    /**
     * Test builder with 'permittedNames'.
     * 
     * FIXME: add min/max. Add URI?
     * 
     * @throws Exception
     */
    @Test
    public void testBuilderPermittedNames() throws GeneralSecurityException {
        populate(builder);
        builder.setPermittedNames("CN=Alice", "CN=Bob");
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(NAME_CONSTRAINTS_OID),
                "certificate does not contain expected Name Constraints extension");

        List<GeneralSubtree> names = certUtil.getPermittedNames(cert);
        assertEquals(names.size(), 2);
        assertEquals(names.get(0).getName().getName(), "CN=Alice");
        assertEquals(names.get(1).getName().getName(), "CN=Bob");
    }

    /**
     * Test builder with 'excludedNames'
     * 
     * FIXME: add min/max. Add URI?
     * 
     * @throws Exception
     */
    @Test
    public void testBuilderExcludedNames() throws GeneralSecurityException {
        populate(builder);
        builder.setExcludedNames("CN=Alice", "CN=Bob");
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(NAME_CONSTRAINTS_OID),
                "certificate does not contain expected Name Constraints extension");

        List<GeneralSubtree> names = certUtil.getExcludedNames(cert);
        assertEquals(names.size(), 2);
        assertEquals(names.get(0).getName().getName(), "CN=Alice");
        assertEquals(names.get(1).getName().getName(), "CN=Bob");
    }

    /**
     * Test builder with 'OCSP Locations'
     * 
     * @throws Exception
     */
    @Test
    public void testOcspLocations() throws GeneralSecurityException, URISyntaxException, InvalidNameException {
        // make sure there's no extension by default
        populate(builder);
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getNonCriticalExtensionOIDs().contains(AUTHORITY_INFO_ACCESS_OID),
                "certificate does not contain expected AIA extension");
        assertTrue(certUtil.getOcspLocations(cert).isEmpty());

        // test it with some general names.
        builder.reset();
        populate(builder);
        builder.setOcspLocations(expectedGeneralNameUri1, expectedGeneralNameUri2, expectedGeneralNameDir);
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(AUTHORITY_INFO_ACCESS_OID),
                "certificate does not contain expected AIA extension");

        List<com.otterca.common.crypto.GeneralName<?>> actual = certUtil.getOcspLocations(cert);
        assertEquals(actual.get(0), expectedGeneralNameUri1);
        assertEquals(actual.get(1), expectedGeneralNameUri2);
        assertEquals(actual.get(2), expectedGeneralNameDir);

        // test it again with the URI convenience method.
        builder.reset();
        populate(builder);
        builder.setOcspLocations(expectedGeneralNameUri1.get(), expectedGeneralNameUri2.get());
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(AUTHORITY_INFO_ACCESS_OID),
                "certificate does not contain expected AIA extension");

        actual = certUtil.getOcspLocations(cert);
        assertEquals(actual.get(0), expectedGeneralNameUri1);
        assertEquals(actual.get(1), expectedGeneralNameUri2);
    }

    /**
     * Test builder with 'CA Issuer Locations'
     * 
     * @throws Exception
     */
    @Test
    public void testCaIssuerLocations() throws GeneralSecurityException, URISyntaxException, InvalidNameException {
        // make sure there are no extensions by default.
        populate(builder);
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getNonCriticalExtensionOIDs().contains(AUTHORITY_INFO_ACCESS_OID),
                "certificate contains unexpected AIA extension");
        assertTrue(certUtil.getCaIssuersLocations(cert).isEmpty());

        // test it with some general names.
        builder.reset();
        populate(builder);
        builder.setCaIssuersLocations(expectedGeneralNameUri1, expectedGeneralNameUri2, expectedGeneralNameDir);
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(AUTHORITY_INFO_ACCESS_OID),
                "certificate does not contain expected AIA extension");

        List<com.otterca.common.crypto.GeneralName<?>> actual = certUtil.getCaIssuersLocations(cert);
        assertEquals(actual.get(0), expectedGeneralNameUri1);
        assertEquals(actual.get(1), expectedGeneralNameUri2);
        assertEquals(actual.get(2), expectedGeneralNameDir);

        // test it again with the URI convenience method.
        builder.reset();
        populate(builder);
        builder.setCaIssuersLocations(expectedGeneralNameUri1.get(), expectedGeneralNameUri2.get());
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(AUTHORITY_INFO_ACCESS_OID),
                "certificate does not contain expected AIA extension");

        actual = certUtil.getCaIssuersLocations(cert);
        assertEquals(actual.get(0), expectedGeneralNameUri1);
        assertEquals(actual.get(1), expectedGeneralNameUri2);
    }

    /**
     * Test builder with 'CA Repositories'
     * 
     * @throws Exception
     */
    @Test
    public void testCaRepositories() throws GeneralSecurityException, URISyntaxException, InvalidNameException {
        // make sure there are no extensions by default.
        populate(builder);
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getNonCriticalExtensionOIDs().contains(SUBJECT_INFO_ACCESS_OID),
                "certificate contains unexpected SIA extension");
        assertTrue(certUtil.getCaRepositories(cert).isEmpty());

        // test it with some general names.
        builder.reset();
        populate(builder);
        builder.setCaRepositories(expectedGeneralNameUri1, expectedGeneralNameUri2, expectedGeneralNameDir);
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(SUBJECT_INFO_ACCESS_OID),
                "certificate does not contain expected SIA extension");

        List<com.otterca.common.crypto.GeneralName<?>> actual = certUtil.getCaRepositories(cert);
        assertEquals(actual.get(0), expectedGeneralNameUri1);
        assertEquals(actual.get(1), expectedGeneralNameUri2);
        assertEquals(actual.get(2), expectedGeneralNameDir);

        // test it again with the URI convenience method.
        builder.reset();
        populate(builder);
        builder.setCaRepositories(expectedGeneralNameUri1.get(), expectedGeneralNameUri2.get());
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(SUBJECT_INFO_ACCESS_OID),
                "certificate does not contain expected AIA extension");

        actual = certUtil.getCaRepositories(cert);
        assertEquals(actual.get(0), expectedGeneralNameUri1);
        assertEquals(actual.get(1), expectedGeneralNameUri2);
    }

    /**
     * Test builder with 'timestamping''
     * 
     * @throws Exception
     */
    @Test
    public void testTimestamping() throws GeneralSecurityException, URISyntaxException, InvalidNameException {
        // make sure there are no extensions by default.
        populate(builder);
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getNonCriticalExtensionOIDs().contains(SUBJECT_INFO_ACCESS_OID),
                "certificate contains unexpected SIA extension");
        assertTrue(certUtil.getTimestamping(cert).isEmpty());

        // test it with some general names.
        builder.reset();
        populate(builder);
        builder.setTimestampingLocations(expectedGeneralNameUri1, expectedGeneralNameUri2, expectedGeneralNameEmail,
                expectedGeneralNameDns, expectedGeneralNameIpAddress);
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(SUBJECT_INFO_ACCESS_OID),
                "certificate does not contain expected SIA extension");

        List<com.otterca.common.crypto.GeneralName<?>> actual = certUtil.getTimestamping(cert);
        assertEquals(actual.get(0), expectedGeneralNameUri1);
        assertEquals(actual.get(1), expectedGeneralNameUri2);
        assertEquals(actual.get(2), expectedGeneralNameEmail);
        assertEquals(actual.get(3), expectedGeneralNameDns);
        assertEquals(actual.get(4), expectedGeneralNameIpAddress);

        // test it again with the URI convenience method.
        builder.reset();
        populate(builder);
        builder.setTimestampingLocations(expectedGeneralNameUri1.get(), expectedGeneralNameUri2.get());
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(SUBJECT_INFO_ACCESS_OID),
                "certificate does not contain expected AIA extension");

        actual = certUtil.getTimestamping(cert);
        assertEquals(actual.get(0), expectedGeneralNameUri1);
        assertEquals(actual.get(1), expectedGeneralNameUri2);
    }

    /**
     * Test builder with 'private key usage period'
     * 
     * @throws Exception
     */
    @Test
    public void testPrivateKeyUsagePeriod() throws GeneralSecurityException {
        // make sure there are no extensions by default.
        populate(builder);
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getNonCriticalExtensionOIDs().contains(PRIVATE_KEY_USAGE_PERIOD_OID),
                "certificate contains unexpected Private Key Usage Period extension");
        assertEquals(certUtil.getPrivateKeyUsagePeriod(cert).length, 0);

        // test it with two dates.
        builder.reset();
        populate(builder);
        builder.setPrivateKeyUsagePeriod(notBefore.getTime(), notAfter.getTime());
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(PRIVATE_KEY_USAGE_PERIOD_OID),
                "certificate does not contain expected Private Key Usage Period extension");

        Date[] dates = certUtil.getPrivateKeyUsagePeriod(cert);

        assertEquals(dates[0], notBefore.getTime());
        assertEquals(dates[1], notAfter.getTime());

        // test it with just 'not before' date.
        builder.reset();
        populate(builder);
        builder.setPrivateKeyUsagePeriod(notBefore.getTime(), null);
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(PRIVATE_KEY_USAGE_PERIOD_OID),
                "certificate does not contain expected Private Key Usage Period extension");

        dates = certUtil.getPrivateKeyUsagePeriod(cert);

        assertEquals(dates[0], notBefore.getTime());
        assertEquals(dates[1], null);

        // test it with just 'not after' date.
        builder.reset();
        populate(builder);
        builder.setPrivateKeyUsagePeriod(null, notAfter.getTime());
        cert = builder.build(keyPair.getPrivate());

        assertTrue(cert.getNonCriticalExtensionOIDs().contains(PRIVATE_KEY_USAGE_PERIOD_OID),
                "certificate does not contain expected Private Key Usage Period extension");

        dates = certUtil.getPrivateKeyUsagePeriod(cert);

        assertEquals(dates[0], null);
        assertEquals(dates[1], notAfter.getTime());

        // test it with no dates. The extension should not be added.
        builder.reset();
        populate(builder);
        builder.setPrivateKeyUsagePeriod(null, null);
        cert = builder.build(keyPair.getPrivate());

        assertFalse(cert.getNonCriticalExtensionOIDs().contains(PRIVATE_KEY_USAGE_PERIOD_OID),
                "certificate contains unexpected Private Key Usage Period extension");
    }

    /**
     * Test conversion to byte array and back.
     */
    @Test
    public void testTestRoundtrip() throws GeneralSecurityException {
        populate(builder);
        X509Certificate expected = builder.build(keyPair.getPrivate());

        X509Certificate actual = certUtil.getCertificate(expected.getEncoded());
        assertEquals(actual.getSerialNumber(), expected.getSerialNumber());
        assertEquals(actual.getIssuerDN().toString(), expected.getIssuerDN().toString());
        assertEquals(actual.getSubjectDN().toString(), expected.getSubjectDN().toString());
        assertEquals(actual.getNotBefore(), expected.getNotBefore());
        assertEquals(actual.getNotAfter(), expected.getNotAfter());
    }

    /**
     * Test search criteria. There's no easy way to check the subject key id and
     * authority key id - we can get the extension's bytes but still need to
     * parse them.
     */
    @Test
    public void testTestSearchCriteria() throws GeneralSecurityException {

        // create issuer certificate
        populate(builder);
        builder.setSubject(ISSUER_NAME);
        builder.setIssuer(ISSUER_NAME);
        builder.setBasicConstraints(true);
        X509Certificate issuer = builder.build(issuerKeyPair.getPrivate());

        builder.reset();

        // create subject certificate
        populate(builder);
        builder.setIssuer(issuer);
        X509Certificate cert = builder.build(keyPair.getPrivate());

        assertEquals(certUtil.getFingerprint(cert), toHex(DigestUtils.sha(cert.getEncoded())));
        assertEquals(certUtil.getCertificateHash(cert), rfc4387(cert.getEncoded()));
        assertEquals(certUtil.getIHash(cert), rfc4387(cert.getIssuerX500Principal().getEncoded()));
        assertEquals(certUtil.getSHash(cert), rfc4387(cert.getSubjectX500Principal().getEncoded()));
    }

    /**
     * Compute SHA1 hash of DER-encoded value, encode it using Base64, and drop
     * the trailing '='. (from X509CertificateUtil)
     * 
     * @param asn1
     * @return
     */
    public final String rfc4387(byte[] asn1) {
        byte[] digest = DigestUtils.sha(asn1);
        return Base64.encodeBase64String(digest).substring(0, 28);
    }

    /**
     * Return colon-separated hex string, e.g., 01:23:45:67.
     * 
     * Implementation note: this algorithm could be made a little more
     * efficient. :-) (from X509CertificateUtil)
     * 
     * @param data
     * @return
     */
    public final String toHex(byte[] data) {
        String hex = Hex.encodeHexString(data);
        StringBuilder sb = new StringBuilder();
        sb.append(hex.substring(0, 2));
        for (int i = 2; i < hex.length(); i += 2) {
            sb.append(':');
            sb.append(hex.charAt(i));
            sb.append(hex.charAt(i + 1));
        }
        return sb.toString();
    }
}