com.vmware.admiral.common.util.CertificateUtil.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.admiral.common.util.CertificateUtil.java

Source

/*
 * Copyright (c) 2016 VMware, Inc. All Rights Reserved.
 *
 * This product is licensed to you under the Apache License, Version 2.0 (the "License").
 * You may not use this product except in compliance with the License.
 *
 * This product may include a number of subcomponents with separate copyright notices
 * and license terms. Your use of these subcomponents is subject to the terms and
 * conditions of the subcomponent's license, as noted in the LICENSE file.
 */

package com.vmware.admiral.common.util;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.cert.CertIOException;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.PEMWriter;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import com.vmware.admiral.common.security.EncryptionUtils;
import com.vmware.xenon.common.LocalizableValidationException;
import com.vmware.xenon.common.Utils;

/**
 * Utility class that provides useful functions for certificate's data retrieval and manipulation.
 */
@SuppressWarnings("deprecation")
public class CertificateUtil {
    private static final char[] EMPTY = new char[0];
    private static final String SIGNING_ALGORITHM = "SHA1withRSA";
    protected static final long CERTIFICATE_TOLERANCE_OFFSET = TimeUnit.DAYS.toMillis(1L);
    private static final Provider PROVIDER = new BouncyCastleProvider();
    private static final long DEFAULT_VALIDITY = TimeUnit.DAYS
            .toMillis(Long.getLong("default.selfSignedCertificate.validity.day", 365 * 2));

    /**
     * Utility method to decode a certificate chain PEM encoded string value to an array of X509Certificate
     * certificate instances.
     *
     * @param certChainPEM
     *            - a certificate chain (one or more certificates) PEM encoded string value.
     * @return - decoded array of X509Certificate  certificate instances.
     * @throws RuntimeException
     *             if a certificate can't be decoded to X509Certificate type certificate.
     */
    public static X509Certificate[] createCertificateChain(String certChainPEM) {
        AssertUtil.assertNotNull(certChainPEM, "certChainPEM should not be null.");

        List<X509Certificate> chain = new ArrayList<>();
        try (PEMParser parser = new PEMParser(new StringReader(certChainPEM))) {

            JcaX509CertificateConverter converter = new JcaX509CertificateConverter();
            X509CertificateHolder certificateHolder;
            while ((certificateHolder = (X509CertificateHolder) parser.readObject()) != null) {
                chain.add(converter.getCertificate(certificateHolder));
            }
        } catch (IOException | CertificateException e) {
            throw new RuntimeException("Failed to create certificate: " + certChainPEM, e);
        }

        if (chain.isEmpty()) {
            throw new RuntimeException("A valid certificate was not found: " + certChainPEM);
        }

        return chain.toArray(new X509Certificate[chain.size()]);
    }

    /**
     * Utility method to decode a certificate PEM encoded string value to X509Certificate type
     * certificate instance.
     *
     * The difference between this method and {@link #createCertificateChain(String)} is that this
     * expects exactly one PEM encoded certificate. Use when the PEM represents a distinguished
     * private or public key. For general purpose, where the expectation is to have one or more
     * PEM encoded certificates, certificate chain, use {@link #createCertificateChain(String)}.
     *
     * @param certPEM
     *            - a certificate PEM encoded string value.
     * @return - decoded X509Certificate type certificate instance.
     * @throws RuntimeException
     *             if the certificate can't be decoded to X509Certificate type certificate.
     */
    public static X509Certificate createCertificate(String certPEM) {
        X509Certificate[] createCertificateChain = createCertificateChain(certPEM);
        AssertUtil.assertTrue(createCertificateChain.length == 1,
                "Expected exactly one certificate in PEM: " + certPEM);

        return createCertificateChain[0];
    }

    /**
     * Utility method to decode a PEM encoded private key string to a PrivateKey instance
     *
     * @param key
     *            - a PEM encoded private key string
     * @return - decoded PrivateKey instance
     */
    public static KeyPair createKeyPair(String key) {
        AssertUtil.assertNotNull(key, "key");
        String decryptedKey = EncryptionUtils.decrypt(key);
        try (PEMParser parser = new PEMParser(new StringReader(decryptedKey))) {

            JcaPEMKeyConverter converter = new JcaPEMKeyConverter();
            PEMKeyPair keyPair = (PEMKeyPair) parser.readObject();
            if (keyPair == null) {
                throw new RuntimeException("A valid key pair was not found");
            }
            return converter.getKeyPair(keyPair);

        } catch (IOException e) {
            throw new RuntimeException("Failed to create key pair", e);
        }
    }

    /**
     * Create an empty key store
     */
    public static KeyStore createEmptyKeyStore() {
        try {
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null); // initialize empty keystore
            return keyStore;

        } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
            throw new RuntimeException("Failed to create empty keystore", e);
        }
    }

    /**
     * Set a certificate entry in a trust store
     */
    public static void setCertificateEntry(KeyStore trustStore, String alias, String trustedCert) {

        X509Certificate[] certificates = CertificateUtil.createCertificateChain(trustedCert);

        for (X509Certificate certificate : certificates) {
            String certAlias = alias + "_" + CertificateUtil.getCommonName(certificate.getSubjectDN());
            try {
                trustStore.setCertificateEntry(certAlias, certificate);

            } catch (KeyStoreException e) {
                throw new RuntimeException("Failed to set certificate entry", e);
            }
        }
    }

    /**
     * Set a key entry in a key store
     */
    private static void setKeyEntry(KeyStore keyStore, String alias, String clientKey, String clientCert) {

        X509Certificate[] clientCertificates = CertificateUtil.createCertificateChain(clientCert);
        PrivateKey key = CertificateUtil.createKeyPair(clientKey).getPrivate();

        try {
            keyStore.setKeyEntry(alias, key, EMPTY, clientCertificates);

        } catch (KeyStoreException e) {
            throw new RuntimeException("Failed to set key entry", e);
        }
    }

    /**
     * Create a TrustManager for the given PEM encoded certificate
     */
    public static TrustManager[] getTrustManagers(String alias, String trustedCert) {
        if (trustedCert == null) {
            return null;
        }

        KeyStore trustStore = createEmptyKeyStore();

        setCertificateEntry(trustStore, alias, trustedCert);

        return getTrustManagers(trustStore);
    }

    /**
     * Create a TrustManager using the given trust store
     */
    public static TrustManager[] getTrustManagers(KeyStore trustStore) {
        try {
            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());

            tmf.init(trustStore);
            return tmf.getTrustManagers();

        } catch (NoSuchAlgorithmException | KeyStoreException e) {
            throw new RuntimeException("Failed to create a TrustManager from a trust store", e);
        }
    }

    /**
     * Get a KeyManager configured with the given private key and certificate
     */
    public static KeyManager[] getKeyManagers(String alias, String clientKey, String clientCert) {

        if (clientCert == null) {
            return null;
        }

        KeyStore keyStore = createEmptyKeyStore();
        setKeyEntry(keyStore, alias, clientKey, clientCert);

        return getKeyManagers(keyStore);
    }

    /**
     * Get a KeyManager based on the given key store
     */
    public static KeyManager[] getKeyManagers(KeyStore keyStore) {
        try {
            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());

            kmf.init(keyStore, EMPTY);
            return kmf.getKeyManagers();

        } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
            throw new RuntimeException("Failed to create a KeyManager from a key store", e);
        }
    }

    /**
     * Create a SSLContext for a given trusted certificate and client key and certificate
     */
    public static SSLContext createSSLContext(String trustedCert, String clientKey, String clientCert) {

        try {
            SSLContext ctx = SSLContext.getInstance("TLS");
            ctx.init(CertificateUtil.getKeyManagers("client", clientKey, clientCert),
                    CertificateUtil.getTrustManagers("server", trustedCert), null);

            return ctx;

        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("Failed to create SSLContext", e);
        }
    }

    /**
     * Create an SSLContext for the given TrustManager and KeyManager
     */
    public static SSLContext createSSLContext(TrustManager trustManager, KeyManager keyManager) {

        try {
            SSLContext ctx = SSLContext.getInstance("TLS");
            ctx.init(new KeyManager[] { keyManager }, new TrustManager[] { trustManager }, null);

            return ctx;

        } catch (NoSuchAlgorithmException | KeyManagementException e) {
            throw new RuntimeException("Failed to create SSLContext", e);
        }
    }

    /**
     * Extracts the Certificate Principal Common Name (CN).
     */
    public static String getCommonName(Principal certPrincipal) {
        if (certPrincipal == null || certPrincipal.getName() == null || certPrincipal.getName().isEmpty()) {
            return null;
        }
        String attr = getAttributeFromDN(certPrincipal.getName(), "CN");
        if (attr == null) {
            Utils.logWarning("DN %s doesn't contain attribute 'CN'", certPrincipal.getName());
            attr = certPrincipal.getName();
        }
        return attr;

    }

    /**
     * Extracts from DN a given attribute.
     *
     * @param dn
     *            The entire DN
     * @param attribute
     *            The attribute to extract
     * @return the attribute value or null if not found an attribute with the given dn.
     */
    public static String getAttributeFromDN(String dn, String attribute) {
        try {
            LdapName subjectDn = new LdapName(dn);

            for (Rdn rdn : subjectDn.getRdns()) {
                if (rdn.getType().equals(attribute)) {
                    return rdn.getValue().toString();
                }
            }
        } catch (InvalidNameException e) {
            throw new IllegalArgumentException(e);
        }

        return null;
    }

    /**
     * Serialize Certificate in PEM format
     */
    public static String toPEMformat(X509Certificate certificate) {
        StringWriter sw = new StringWriter();
        PEMWriter pemWriter = new PEMWriter(sw);
        try {
            pemWriter.writeObject(certificate);
            pemWriter.close();

            return sw.toString();

        } catch (IOException x) {
            throw new RuntimeException("Failed to serialize certificate", x);
        }
    }

    /**
     * Serialize Certificate chain in PEM format
     */
    public static String toPEMformat(X509Certificate[] certificateChain) {
        StringWriter sw = new StringWriter();
        for (X509Certificate certificate : certificateChain) {
            sw.append(toPEMformat(certificate));
        }
        return sw.toString();
    }

    public static void validateCertificateChain(X509Certificate[] certificateChain) throws Exception {
        List<X509Certificate> certificates = Arrays.asList(certificateChain);

        for (X509Certificate certificate : certificates) {
            checkIfCertificateExistsMoreThanOneTimeInChain(certificates, certificate);
        }

        Iterator<X509Certificate> it = certificates.iterator();

        X509Certificate current = it.next();
        current.checkValidity();

        while (it.hasNext()) {
            X509Certificate next = it.next();
            next.checkValidity();

            try {
                current.verify(next.getPublicKey());
            } catch (InvalidKeyException | CertificateException | NoSuchAlgorithmException
                    | NoSuchProviderException e) {
                throw new IllegalArgumentException(e);
            } catch (SignatureException e) {
                throw new LocalizableValidationException("Certificate chain is not valid.",
                        "common.certificate.invalid");
            }
            current = next;
        }
    }

    private static void checkIfCertificateExistsMoreThanOneTimeInChain(List<X509Certificate> certificates,
            X509Certificate certificate) {

        int exists = 0;

        for (X509Certificate currentcert : certificates) {
            if (certificate.equals(currentcert) && exists >= 1) {
                String errorMsg = String.format(
                        "Certificate with subject: %s exists in more than one place in chain.",
                        certificate.getSubjectX500Principal().getName());
                throw new LocalizableValidationException(errorMsg, "common.certificate.exists.in.chain",
                        certificate.getSubjectX500Principal().getName());
            } else if (certificate.equals(currentcert)) {
                exists++;
            }
        }
    }

    /**
     * Retrieve the {@link ThumbprintAlgorithm#DEFAULT} thumbprint of a X.509 certificate.
     *
     * @param cert
     *            certificate
     * @return the thumbprint corresponding to the certificate; {@code not-null} value
     *
     * @throws CertificateEncodingException
     *             if an error occur while getting the encoded form of the certificates
     */
    public static String computeCertificateThumbprint(X509Certificate cert) {
        return computeCertificateThumbprint(cert, ThumbprintAlgorithm.DEFAULT);
    }

    /**
     * Retrieve thumbprint of a X.509 certificate as specified in {@link ThumbprintAlgorithm}
     * parameter.
     *
     * @param cert
     *            certificate
     * @param thumbprintAlgorithm
     *            the type of {@link ThumbprintAlgorithm}
     * @return the thumbprint corresponding to the certificate; {@code not-null} value
     *
     * @throws CertificateEncodingException
     *             if an error occur while getting the encoded form of the certificates
     */
    public static String computeCertificateThumbprint(X509Certificate cert,
            ThumbprintAlgorithm thumbprintAlgorithm) {
        if (cert == null) {
            throw new LocalizableValidationException("certificate must not be null.", "common.certificate.null");
        }
        byte[] digest;
        try {
            MessageDigest md = MessageDigest.getInstance(thumbprintAlgorithm.getAlgorithmName());
            digest = md.digest(cert.getEncoded());
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException(e);
        } catch (CertificateEncodingException e) {
            throw new IllegalArgumentException(e);
        }

        StringBuilder thumbprint = new StringBuilder();
        for (int i = 0, len = digest.length; i < len; ++i) {
            if (i > 0) {
                thumbprint.append(':');
            }
            byte b = digest[i];
            thumbprint.append(HEX.charAt((b & 0xF0) >> 4));
            thumbprint.append(HEX.charAt(b & 0x0F));
        }
        return thumbprint.toString();
    }

    private static final String HEX = "0123456789ABCDEF";

    /**
     * Generates a signed certificate and a private key for client auth.
     *
     * @param fqdn
     *            the fqdn name of the subject
     * @param issuerCertificate
     *            the certificate that will be used as a signer. Cannot be null.
     * @param issuerPrivateKey
     *            the private key to sign the certificate. Cannot be null.
     * @return holder object for the issued certificate and the private key.
     */
    public static CertChainKeyPair generateSignedForClient(String fqdn, X509Certificate issuerCertificate,
            PrivateKey issuerPrivateKey) {
        try {
            return generateCertificateAndSign(fqdn, issuerCertificate, issuerPrivateKey, getClientExtensions());
        } catch (CertIOException | CertificateException | OperatorCreationException e) {
            throw new RuntimeException(
                    String.format("Failed to generate client certificate, reason: %s", e.getMessage()), e);
        }
    }

    /**
     * Generates a signed certificate and a private key.
     *
     * @param fqdn
     *            the fqdn name of the subject
     * @param issuerCertificate
     *            the certificate that will be used as a signer. Cannot be null.
     * @param issuerPrivateKey
     *            the private key to sign the certificate. Cannot be null.
     * @return holder object for the issued certificate and the private key.
     */
    public static CertChainKeyPair generateSigned(String fqdn, X509Certificate issuerCertificate,
            PrivateKey issuerPrivateKey) {
        try {
            return generateCertificateAndSign(fqdn, issuerCertificate, issuerPrivateKey,
                    getServerExtensions(issuerCertificate));
        } catch (CertificateException | OperatorCreationException | NoSuchAlgorithmException | IOException e) {
            throw new RuntimeException(
                    String.format("Failed to generate server certificate, reason: %s", e.getMessage()), e);
        }
    }

    /**
     * Returns certificate thumbprint with no colon characters and lower-cased for the first
     * certificate in the chain.
     */
    public static String generatePureFingerPrint(X509Certificate[] certificateChain) {
        return generatePureFingerPrint(certificateChain[0]);
    }

    /**
     * Returns certificate thumbprint with no colon characters and lower-cased.
     */
    public static String generatePureFingerPrint(X509Certificate certificate) {
        String thumbprint = computeCertificateThumbprint(certificate);
        return thumbprint.replaceAll(":", "").toLowerCase();
    }

    private static CertChainKeyPair generateCertificateAndSign(String fqdn, X509Certificate issuerCertificate,
            PrivateKey issuerPrivateKey, List<ExtensionHolder> extensions)
            throws CertificateException, CertIOException, OperatorCreationException {
        AssertUtil.assertNotNull(issuerCertificate, "issuerCertificate");
        AssertUtil.assertNotNull(issuerPrivateKey, "issuerPrivateKey");

        // private key that we are creating certificate for
        KeyPair pair = KeyUtil.generateRSAKeyPair();

        PublicKey publicKey = pair.getPublic();
        PrivateKey privateKey = convertToSunImpl(pair.getPrivate());

        ContentSigner signer = new JcaContentSignerBuilder(SIGNING_ALGORITHM).setProvider(PROVIDER)
                .build(issuerPrivateKey);

        X500Name subjectName = new X500Name("CN=" + fqdn);

        // serial number of certificate
        BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());

        // valid from
        Date notBefore = generateNotBeforeDate();

        // valid to
        Date notAfter = generateNotAfterDate(notBefore, DEFAULT_VALIDITY);

        X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(issuerCertificate, serial, notBefore,
                notAfter, subjectName, publicKey);

        for (ExtensionHolder extension : extensions) {
            certBuilder.addExtension(extension.getOID(), extension.isCritical(), extension.getValue());
        }

        X509CertificateHolder certificateHolder = certBuilder.build(signer);

        X509Certificate certificate = new JcaX509CertificateConverter().setProvider(PROVIDER)
                .getCertificate(certificateHolder);

        List<X509Certificate> certificateChain = new ArrayList<>(2);
        certificateChain.add(certificate);
        certificateChain.add(issuerCertificate);

        return new CertChainKeyPair(certificateChain, certificate, privateKey);
    }

    /**
     * Helper method for converting the bouncycastle implementation of a private key to a sun
     * implementation.
     *
     * @param key
     *            the private key to be converted.
     * @return a sun implementation of the private key.
     */
    private static PrivateKey convertToSunImpl(PrivateKey key) {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(key.getEncoded());
        KeyFactory kf;
        try {
            kf = KeyFactory.getInstance(KeyUtil.RSA_ALGORITHM);
            return kf.generatePrivate(spec);
        } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static Date generateNotBeforeDate() {
        return new Date(System.currentTimeMillis() - CERTIFICATE_TOLERANCE_OFFSET);
    }

    private static Date generateNotAfterDate(Date notBefore, long validityPeriod) {
        return new Date(notBefore.getTime() + validityPeriod);
    }

    private static List<ExtensionHolder> getClientExtensions() {
        List<ExtensionHolder> extensions = new ArrayList<>();

        extensions.add(new ExtensionHolder(Extension.basicConstraints, true, new BasicConstraints(false)));
        extensions.add(new ExtensionHolder(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature)));
        extensions.add(new ExtensionHolder(Extension.extendedKeyUsage, true,
                new ExtendedKeyUsage(KeyPurposeId.id_kp_clientAuth)));

        return extensions;
    }

    private static List<ExtensionHolder> getServerExtensions(X509Certificate issuerCertificate)
            throws CertificateEncodingException, NoSuchAlgorithmException, IOException {
        List<ExtensionHolder> extensions = new ArrayList<>();

        // SSO forces us to allow data encipherment
        extensions.add(new ExtensionHolder(Extension.keyUsage, true,
                new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment)));

        extensions.add(new ExtensionHolder(Extension.extendedKeyUsage, true,
                new ExtendedKeyUsage(KeyPurposeId.id_kp_serverAuth)));

        Extension authorityKeyExtension = new Extension(Extension.authorityKeyIdentifier, false,
                new DEROctetString(new JcaX509ExtensionUtils().createAuthorityKeyIdentifier(issuerCertificate)));
        extensions.add(new ExtensionHolder(authorityKeyExtension.getExtnId(), authorityKeyExtension.isCritical(),
                authorityKeyExtension.getParsedValue()));

        return extensions;
    }

    public static enum ThumbprintAlgorithm {
        SHA_1("SHA-1", "[a-fA-F0-9:]{59}"), SHA_256("SHA-256", "[a-fA-F0-9:]{95}");

        public static final ThumbprintAlgorithm DEFAULT = SHA_1;

        ThumbprintAlgorithm(String algorithmName, String thumbprintRegex) {
            this.algorithmName = algorithmName;
            this.thumbprintRegex = thumbprintRegex;
        }

        private final String algorithmName;
        private final String thumbprintRegex;

        public String getAlgorithmName() {
            return algorithmName;
        }

        public boolean isValid(String thumbPrint) {
            if (thumbPrint == null) {
                return false;
            }
            return thumbPrint.matches(thumbprintRegex);
        }

        public static boolean isValidThumbprint(String thumbPrint) {
            if (thumbPrint == null) {
                return false;
            }
            return SHA_256.isValid(thumbPrint) || SHA_1.isValid(thumbPrint);
        }
    }

    /**
     * Wrapper class for certificate chain and a private key.
     */
    public static class CertChainKeyPair {
        private final List<X509Certificate> certificateChain;
        private final PrivateKey privateKey;
        private final X509Certificate certificate;

        public CertChainKeyPair(List<X509Certificate> certificateChain, X509Certificate certificate,
                PrivateKey privateKey) {
            this.certificateChain = certificateChain;
            this.certificate = certificate;
            this.privateKey = privateKey;
        }

        public List<X509Certificate> getCertificateChain() {
            return certificateChain;
        }

        public PrivateKey getPrivateKey() {
            return privateKey;
        }

        public X509Certificate getCertificate() {
            return certificate;
        }
    }

    private static class ExtensionHolder {

        private final ASN1ObjectIdentifier oid;
        private final boolean isCritical;
        private final ASN1Encodable value;

        public ExtensionHolder(ASN1ObjectIdentifier oid, boolean isCritical, ASN1Encodable value) {
            this.oid = oid;
            this.isCritical = isCritical;
            this.value = value;
        }

        public ASN1ObjectIdentifier getOID() {
            return oid;
        }

        public boolean isCritical() {
            return isCritical;
        }

        public ASN1Encodable getValue() {
            return value;
        }
    }
}