/*
 * The MIT License
 *
 * Copyright 2014 Karol Bucek.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.jruby.ext.openssl;

import static org.jruby.ext.openssl.OpenSSL.debugStackTrace;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyFactorySpi;
import java.security.KeyPairGenerator;
import java.security.KeyPairGeneratorSpi;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.MessageDigest;
import java.security.MessageDigestSpi;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.SecureRandomSpi;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.SignatureSpi;
import java.security.cert.CRLException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateFactorySpi;
import java.security.cert.X509CRL;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.concurrent.ConcurrentHashMap;

import javax.crypto.Cipher;
import javax.crypto.CipherSpi;
import javax.crypto.KeyAgreement;
import javax.crypto.KeyAgreementSpi;
import javax.crypto.KeyGenerator;
import javax.crypto.KeyGeneratorSpi;
import javax.crypto.Mac;
import javax.crypto.MacSpi;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.SecretKeyFactorySpi;
import javax.net.ssl.SSLContext;

import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.CertificateList;
import org.bouncycastle.cert.CertException;
import org.bouncycastle.cert.X509CRLHolder;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.params.DSAParameters;
import org.bouncycastle.crypto.params.DSAPublicKeyParameters;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.jce.provider.X509CRLObject;
import org.bouncycastle.operator.ContentVerifierProvider;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorException;
import org.bouncycastle.operator.bc.BcDSAContentVerifierProviderBuilder;
import org.bouncycastle.operator.bc.BcRSAContentVerifierProviderBuilder;

/**
 * Java Security (and JCE) helpers.
 *
 * @author kares
 */
public abstract class SecurityHelper {

    private static String BC_PROVIDER_CLASS = "org.bouncycastle.jce.provider.BouncyCastleProvider";
    private static String BC_PROVIDER_NAME = "BC";
    static boolean setBouncyCastleProvider = true; // (package access for tests)
    static Provider securityProvider; // 'BC' provider (package access for tests)
    private static volatile Boolean registerProvider = null;
    static final Map<String, Class> implEngines = new ConcurrentHashMap<String, Class>(16, 0.75f, 1);

    /**
     * inject under a given name a cipher. also ensures that the registered
     * classes are getting used.
     *
     * @param name the name under which the class gets registered
     * @param clazz the CipherSpi class
     */
    public static void addCipher(String name, Class<? extends CipherSpi> clazz) {
        implEngines.put("Cipher:" + name, clazz);
        tryCipherInternal = true;
    }

    /**
     * inject under a given name a signature
     *
     * @param name the name under which the class gets registered
     * @param clazz the SignaturSpi class
     */
    public static void addSignature(String name, Class<? extends SignatureSpi> clazz) {
        implEngines.put("Signature:" + name, clazz);
    }

    public static Provider getSecurityProvider() {
        if ( setBouncyCastleProvider && securityProvider == null ) {
            synchronized(SecurityHelper.class) {
                if ( setBouncyCastleProvider && securityProvider == null ) {
                    setBouncyCastleProvider(); setBouncyCastleProvider = false;
                }
            }
        }
        doRegisterProvider();
        return securityProvider;
    }

    static final boolean SPI_ACCESSIBLE;

    static {
        boolean canSetAccessible = true;
        if ( OpenSSL.javaVersion9(true) ) {
            final Provider provider = getSecurityProvider();
            if ( provider != null ) {
                try {
                    // NOTE: some getXxx pieces might still work
                    // where SPI are returned directly + there's a public <init> e.g. MessageDigest(...)
                    getCertificateFactory("X.509", provider); // !!! disables EVERYTHING :(
                }
                catch (CertificateException ex) {
                    debugStackTrace(ex);
                    canSetAccessible = false;
                }
                catch (RuntimeException ex) {
                    debugStackTrace(ex);
                    // java.lang.reflect.InaccessibleObjectException (extends RuntimeException)
                    canSetAccessible = false;
                }
            }
        }
        SPI_ACCESSIBLE = canSetAccessible;
    }

    static Provider getSecurityProviderIfAccessible() {
        return SPI_ACCESSIBLE ? getSecurityProvider() : null;
    }

    public static synchronized void setSecurityProvider(final Provider provider) {
        if ( provider != null ) OpenSSL.debug("using provider: " + provider);
        securityProvider = provider;
    }

    static synchronized void setBouncyCastleProvider() {
        setSecurityProvider( newBouncyCastleProvider() );
    }

    private static Provider newBouncyCastleProvider() {
        try {
            return (Provider) Class.forName(BC_PROVIDER_CLASS).newInstance();
        }
        catch (Throwable ignored) { /* no bouncy castle available */ }
        return null;
    }

    public static synchronized void setRegisterProvider(final boolean register) {
        registerProvider = Boolean.valueOf(register);
        if ( register ) getSecurityProvider(); // so that securityProvider != null
        // getSecurityProvider does doRegisterProvider();
    }

    static boolean isProviderAvailable(final String name) {
        return Security.getProvider(name) != null;
    }

    public static boolean isProviderRegistered() {
        if ( securityProvider == null ) return false;
        return Security.getProvider(securityProvider.getName()) != null;
    }

    private static void doRegisterProvider() {
        if ( registerProvider != null ) {
            synchronized(SecurityHelper.class) {
                final Boolean register = registerProvider;
                if ( register != null && register.booleanValue() ) {
                    if ( securityProvider != null ) {
                        Security.addProvider(securityProvider);
                        registerProvider = null;
                    }
                }
            }
        }
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static CertificateFactory getCertificateFactory(final String type)
        throws CertificateException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getCertificateFactory(type, provider);
        }
        catch (CertificateException e) { debugStackTrace(e); }
        return CertificateFactory.getInstance(type);
    }

    static CertificateFactory getCertificateFactory(final String type, final Provider provider)
        throws CertificateException {
        final CertificateFactorySpi spi;
        boolean addedBC = false;
        synchronized(SecurityHelper.class) {
            try { // TODO fixed since BC 1.55 (only needed on 1.54) and should be removed eventually ...
                if (provider.getName().equals(BC_PROVIDER_NAME) && Security.getProvider(BC_PROVIDER_NAME) == null) {
                    Security.addProvider(provider);
                    addedBC = true;
                }
                spi = (CertificateFactorySpi) getImplEngine("CertificateFactory", type);
            } finally {
                if (addedBC) {
                    Security.removeProvider(BC_PROVIDER_NAME);
                }
            }
        }
        if ( spi == null ) throw new CertificateException(type + " not found");
        return newInstance(CertificateFactory.class,
                new Class[]{ CertificateFactorySpi.class, Provider.class, String.class },
                new Object[]{ spi, provider, type }
        );
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static KeyFactory getKeyFactory(final String algorithm)
        throws NoSuchAlgorithmException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getKeyFactory(algorithm, provider);
        }
        catch (NoSuchAlgorithmException e) { }
        return KeyFactory.getInstance(algorithm);
    }

    static KeyFactory getKeyFactory(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        KeyFactorySpi spi = (KeyFactorySpi) getImplEngine("KeyFactory", algorithm);
        if ( spi == null ) throw new NoSuchAlgorithmException(algorithm + " not found");
        return newInstance(KeyFactory.class,
            new Class[] { KeyFactorySpi.class, Provider.class, String.class },
            new Object[] { spi, provider, algorithm }
        );
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static KeyPairGenerator getKeyPairGenerator(final String algorithm)
        throws NoSuchAlgorithmException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getKeyPairGenerator(algorithm, provider);
        }
        catch (NoSuchAlgorithmException e) { }
        return KeyPairGenerator.getInstance(algorithm);
    }

    @SuppressWarnings("unchecked")
    static KeyPairGenerator getKeyPairGenerator(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        final Object spi = getImplEngine("KeyPairGenerator", algorithm);
        if ( spi == null ) {
            throw new NoSuchAlgorithmException(algorithm + " KeyPairGenerator not available");
        }

        final KeyPairGenerator keyPairGenerator;
        if ( spi instanceof KeyPairGenerator ) {
            keyPairGenerator = (KeyPairGenerator) spi;
        }
        else {
            final Class<? extends KeyPairGenerator> delegate;
            try {
                delegate = (Class<? extends KeyPairGenerator>)
                    Class.forName(KeyPairGenerator.class.getName() + "$Delegate");
            } catch (ClassNotFoundException e) { throw new RuntimeException(e); }

            keyPairGenerator = newInstance(delegate,
                new Class[] { KeyPairGeneratorSpi.class, String.class }, spi, algorithm
            );
        }
        setField(keyPairGenerator, KeyPairGenerator.class, "provider", provider);
        return keyPairGenerator;
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static KeyStore getKeyStore(final String type)
        throws KeyStoreException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getKeyStore(type, provider);
        }
        catch (KeyStoreException e) { }
        return KeyStore.getInstance(type);
    }

    static KeyStore getKeyStore(final String type, final Provider provider)
        throws KeyStoreException {
        return KeyStore.getInstance(type, provider);
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static MessageDigest getMessageDigest(final String algorithm) throws NoSuchAlgorithmException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getMessageDigest(algorithm, provider);
        }
        catch (NoSuchAlgorithmException e) { }
        return MessageDigest.getInstance(algorithm);
    }

    @SuppressWarnings("unchecked")
    static MessageDigest getMessageDigest(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        final Object spi = getImplEngine("MessageDigest", algorithm);
        if ( spi == null ) throw new NoSuchAlgorithmException(algorithm + " not found");

        final MessageDigest messageDigest;
        if ( spi instanceof MessageDigest ) {
            messageDigest = (MessageDigest) spi;
        }
        else {
            final Class<? extends MessageDigest> delegate;
            try {
                delegate = (Class<? extends MessageDigest>)
                    Class.forName(MessageDigest.class.getName() + "$Delegate");
            } catch (ClassNotFoundException e) { throw new RuntimeException(e); }

            messageDigest = newInstance(delegate,
                new Class[] { MessageDigestSpi.class, String.class }, spi, algorithm
            );
        }
        setField(messageDigest, MessageDigest.class, "provider", provider);
        return messageDigest;
    }

    public static SecureRandom getSecureRandom() {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) {
                final String algorithm = getSecureRandomAlgorithm(provider);
                if ( algorithm != null ) {
                    return getSecureRandom(algorithm, provider);
                }
            }
        }
        catch (NoSuchAlgorithmException e) { }
        return new SecureRandom(); // likely "SHA1PRNG" from SPI sun.security.provider.SecureRandom
    }

    private static SecureRandom getSecureRandom(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        final SecureRandomSpi spi = (SecureRandomSpi) getImplEngine("SecureRandom", algorithm);
        if ( spi == null ) throw new NoSuchAlgorithmException(algorithm + " not found");

        return newInstance(SecureRandom.class,
            new Class[] { SecureRandomSpi.class, Provider.class, String.class },
            new Object[] { spi, provider, algorithm }
        );
    }

    // NOTE: none (at least for BC 1.47)
    private static String getSecureRandomAlgorithm(final Provider provider) {
        for ( Provider.Service service : provider.getServices() ) {
            if ( "SecureRandom".equals( service.getType() ) ) {
                return service.getAlgorithm();
            }
        }
        return null;
    }

    private static Boolean tryCipherInternal = Boolean.FALSE;

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static Cipher getCipher(final String transformation)
        throws NoSuchAlgorithmException, NoSuchPaddingException {
        try {
            if ( tryCipherInternal == Boolean.FALSE ) {
                final Provider provider = getSecurityProvider();
                if ( provider != null ) {
                    return getCipher(transformation, provider);
                }
            }
        }
        catch (NoSuchAlgorithmException e) { }
        catch (NoSuchPaddingException e) { }
        catch (SecurityException e) {
            // java.lang.SecurityException: JCE cannot authenticate the provider BC
            if ( tryCipherInternal != null ) tryCipherInternal = Boolean.TRUE;
            debugStackTrace(e);
        }
        if ( tryCipherInternal == Boolean.TRUE ) {
            try {
                final Provider provider = getSecurityProvider();
                if ( provider != null ) {
                    return getCipherInternal(transformation, provider);
                }
            }
            catch (NoSuchAlgorithmException e) { }
            catch (RuntimeException e) {
                // likely javax.crypto.JceSecurityManager.isCallerTrusted gets
                // us a NPE from javax.crypto.Cipher.<init>(Cipher.java:264)
                tryCipherInternal = null; // do not try BC at all
                debugStackTrace(e);
            }
        }
        return Cipher.getInstance(transformation);
    }

    static Cipher getCipher(final String transformation, final Provider provider)
        throws NoSuchAlgorithmException, NoSuchPaddingException {
        return Cipher.getInstance(transformation, provider);
    }

    private static final Class<?>[] STRING_PARAM = { String.class };

    private static Cipher getCipherInternal(String transformation, final Provider provider)
        throws NoSuchAlgorithmException {
        CipherSpi spi = (CipherSpi) getImplEngine("Cipher", transformation);
        if ( spi == null ) {
            //
            // try the long way
            //
            StringTokenizer tok = new StringTokenizer(transformation, "/");
            final String algorithm = tok.nextToken();

            spi = (CipherSpi) getImplEngine("Cipher", algorithm);
            if ( spi == null ) {
                // if ( silent ) return null;
                throw new NoSuchAlgorithmException(transformation + " not found");
            }

            //
            // make sure we don't get fooled by a "//" in the string
            //
            if ( tok.hasMoreTokens() && ! transformation.regionMatches(algorithm.length(), "//", 0, 2) ) {
                // spi.engineSetMode(tok.nextToken()) :
                invoke(spi, CipherSpi.class, "engineSetMode", STRING_PARAM, tok.nextToken());
            }
            if ( tok.hasMoreTokens() ) {
                // spi.engineSetPadding(tok.nextToken()) :
                invoke(spi, CipherSpi.class, "engineSetPadding", STRING_PARAM, tok.nextToken());
            }

        }
        try {
            // this constructor does not verify the provider
            Cipher cipher = newInstance(Cipher.class,
                    new Class[] { CipherSpi.class, String.class },
                    new Object[] { spi, transformation }
            );
            setField(cipher, Cipher.class, "provider", provider);
            return cipher;
        }
        catch( Exception e ) {
            // this constructor does verify the provider which might fail
            return newInstance(Cipher.class,
                    new Class[] { CipherSpi.class, Provider.class, String.class },
                    new Object[] { spi, provider, transformation }
            );
        }
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static Signature getSignature(final String algorithm) throws NoSuchAlgorithmException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getSignature(algorithm, provider);
        }
        catch (NoSuchAlgorithmException e) { }
        return Signature.getInstance(algorithm);
    }

    @SuppressWarnings("unchecked")
    static Signature getSignature(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        final Object spi = getImplEngine("Signature", algorithm);
        if ( spi == null ) throw new NoSuchAlgorithmException(algorithm + " Signature not available");

        final Signature signature;
        if ( spi instanceof Signature ) {
            signature = (Signature) spi;
        } else {
            final Class<? extends Signature> delegate;
            try {
                delegate = (Class<? extends Signature>)
                    Class.forName(Signature.class.getName() + "$Delegate");
            } catch (ClassNotFoundException e) { throw new RuntimeException(e); }

            signature = newInstance(delegate,
                new Class[] { SignatureSpi.class, String.class }, spi, algorithm
            );
        }
        setField(signature, Signature.class, "provider", provider);
        return signature;
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static Mac getMac(final String algorithm) throws NoSuchAlgorithmException {
        Mac mac = null;
        final Provider provider = getSecurityProviderIfAccessible();
        if ( provider != null ) {
            mac = getMac(algorithm, provider, true);
        }
        if ( mac == null ) mac = Mac.getInstance(algorithm);
        return mac;
    }

    static Mac getMac(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        return getMac(algorithm, provider, false);
    }

    private static Mac getMac(final String algorithm, final Provider provider, boolean silent)
        throws NoSuchAlgorithmException {
        MacSpi spi = (MacSpi) getImplEngine("Mac", algorithm);
        if ( spi == null ) {
            if ( silent ) return null;
            throw new NoSuchAlgorithmException(algorithm + " not found");
        }
        return newInstance(Mac.class,
            new Class[] { MacSpi.class, Provider.class, String.class },
            new Object[] { spi, provider, algorithm }
        );
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static KeyGenerator getKeyGenerator(final String algorithm) throws NoSuchAlgorithmException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getKeyGenerator(algorithm, provider);
        }
        catch (NoSuchAlgorithmException e) { }
        catch (SecurityException e) { debugStackTrace(e); }
        return KeyGenerator.getInstance(algorithm);
    }

    static KeyGenerator getKeyGenerator(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        final KeyGeneratorSpi spi = (KeyGeneratorSpi) getImplEngine("KeyGenerator", algorithm);
        if ( spi == null ) throw new NoSuchAlgorithmException(algorithm + " not found");

        return newInstance(KeyGenerator.class,
            new Class[] { KeyGeneratorSpi.class, Provider.class, String.class },
            new Object[] { spi, provider, algorithm }
        );
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static KeyAgreement getKeyAgreement(final String algorithm) throws NoSuchAlgorithmException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getKeyAgreement(algorithm, provider);
        }
        catch (NoSuchAlgorithmException e) { }
        catch (SecurityException e) { debugStackTrace(e); }
        return KeyAgreement.getInstance(algorithm);
    }

    static KeyAgreement getKeyAgreement(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        final KeyAgreementSpi spi = (KeyAgreementSpi) getImplEngine("KeyAgreement", algorithm);
        if ( spi == null ) throw new NoSuchAlgorithmException(algorithm + " not found");

        return newInstance(KeyAgreement.class,
            new Class[] { KeyAgreementSpi.class, Provider.class, String.class },
            new Object[] { spi, provider, algorithm }
        );
    }

    /**
     * @note code calling this should not assume BC provider internals !
     */
    public static SecretKeyFactory getSecretKeyFactory(final String algorithm) throws NoSuchAlgorithmException {
        try {
            final Provider provider = getSecurityProviderIfAccessible();
            if ( provider != null ) return getSecretKeyFactory(algorithm, provider);
        }
        catch (NoSuchAlgorithmException e) { }
        catch (SecurityException e) { debugStackTrace(e); }
        return SecretKeyFactory.getInstance(algorithm);
    }

    static SecretKeyFactory getSecretKeyFactory(final String algorithm, final Provider provider)
        throws NoSuchAlgorithmException {
        final SecretKeyFactorySpi spi = (SecretKeyFactorySpi) getImplEngine("SecretKeyFactory", algorithm);
        if ( spi == null ) throw new NoSuchAlgorithmException(algorithm + " not found");

        return newInstance(SecretKeyFactory.class,
            new Class[] { SecretKeyFactorySpi.class, Provider.class, String.class },
            new Object[] { spi, provider, algorithm }
        );
    }

    private static boolean providerSSLContext = false; // BC does not implement + JDK default is fine

    public static SSLContext getSSLContext(final String protocol)
        throws NoSuchAlgorithmException {
        try {
            if ( providerSSLContext ) {
                final Provider provider = getSecurityProviderIfAccessible();
                if ( provider != null ) {
                    return getSSLContext(protocol, provider);
                }
            }
        }
        catch (NoSuchAlgorithmException e) { }
        return SSLContext.getInstance(protocol);
    }

    private static SSLContext getSSLContext(final String protocol, final Provider provider)
        throws NoSuchAlgorithmException {
        return SSLContext.getInstance(protocol, provider);
    }

    public static boolean verify(final X509CRL crl, final PublicKey publicKey)
        throws NoSuchAlgorithmException, CRLException, InvalidKeyException, SignatureException {
        return verify(crl, publicKey, false);
    }

    static boolean verify(final X509CRL crl, final PublicKey publicKey, final boolean silent)
        throws NoSuchAlgorithmException, CRLException, InvalidKeyException, SignatureException {

        if ( crl instanceof X509CRLObject ) {
            final CertificateList crlList = (CertificateList) getCertificateList(crl);
            final AlgorithmIdentifier tbsSignatureId = crlList.getTBSCertList().getSignature();
            if ( ! crlList.getSignatureAlgorithm().equals(tbsSignatureId) ) {
                if ( silent ) return false;
                throw new CRLException("Signature algorithm on CertificateList does not match TBSCertList.");
            }

            final Signature signature = getSignature(crl.getSigAlgName(), securityProvider);

            signature.initVerify(publicKey);
            signature.update(crl.getTBSCertList());

            if ( ! signature.verify( crl.getSignature() ) ) {
                if ( silent ) return false;
                throw new SignatureException("CRL does not verify with supplied public key.");
            }
            return true;
        }
        else {
            try {
                final DigestAlgorithmIdentifierFinder digestAlgFinder = new DefaultDigestAlgorithmIdentifierFinder();
                final ContentVerifierProvider verifierProvider;
                if ( "DSA".equalsIgnoreCase( publicKey.getAlgorithm() )) {
                    BigInteger y = ((DSAPublicKey) publicKey).getY();
                    DSAParams params = ((DSAPublicKey) publicKey).getParams();
                    DSAParameters parameters = new DSAParameters(params.getP(), params.getQ(), params.getG());
                    AsymmetricKeyParameter dsaKey = new DSAPublicKeyParameters(y, parameters);
                    verifierProvider = new BcDSAContentVerifierProviderBuilder(digestAlgFinder).build(dsaKey);
                }
                else {
                    BigInteger mod = ((RSAPublicKey) publicKey).getModulus();
                    BigInteger exp = ((RSAPublicKey) publicKey).getPublicExponent();
                    AsymmetricKeyParameter rsaKey = new RSAKeyParameters(false, mod, exp);
                    verifierProvider = new BcRSAContentVerifierProviderBuilder(digestAlgFinder).build(rsaKey);
                }
                return new X509CRLHolder(crl.getEncoded()).isSignatureValid( verifierProvider );
            }
            catch (OperatorException e) {
                throw new SignatureException(e);
            }
            catch (CertException e) {
                throw new SignatureException(e);
            }
	    // can happen if the input is DER but does not match expected strucure
            catch (ClassCastException e) {
                throw new SignatureException(e);
            }
            catch (IOException e) {
                throw new SignatureException(e);
            }
        }
    }

    private static Object getCertificateList(final Object crl) { // X509CRLObject
        try { // private CertificateList c;
            final Field cField = X509CRLObject.class.getDeclaredField("c");
            cField.setAccessible(true);
            return cField.get(crl);
        }
        catch (NoSuchFieldException e) {
            debugStackTrace(e); return null;
        }
        catch (IllegalAccessException e) { return null; }
        catch (SecurityException e) { return null; }
    }

    // these are BC JCE (@see javax.crypto.JCEUtil) inspired internals :
    // https://github.com/bcgit/bc-java/blob/master/jce/src/main/java/javax/crypto/JCEUtil.java

    private static Object getImplEngine(String baseName, String algorithm) {
        Object engine = findImplEngine(baseName, algorithm.toUpperCase(Locale.ENGLISH));
        if (engine == null) {
            engine = findImplEngine(baseName, algorithm);
        }
        return engine;
    }

    private static Object findImplEngine(final String baseName, String algorithm) {
        Class implEngineClass = implEngines.get(baseName + ":" + algorithm);

        if (implEngineClass == null) {
            final Provider bcProvider = securityProvider;
            String alias;
            while ((alias = bcProvider.getProperty("Alg.Alias." + baseName + "." + algorithm)) != null) {
                algorithm = alias;
            }
            final String className = bcProvider.getProperty(baseName + "." + algorithm);
            if (className != null) {
                try {
                    ClassLoader loader = bcProvider.getClass().getClassLoader();
                    if (loader != null) {
                        implEngineClass = loader.loadClass(className);
                    } else {
                        implEngineClass = Class.forName(className);
                    }
                    implEngineClass.newInstance(); // this instance is thrown away to test newInstance, but only once
                } catch (ClassNotFoundException e) {
                    throw new IllegalStateException("algorithm " + algorithm + " in provider " + bcProvider.getName() + " but no class \"" + className + "\" found!");
                } catch (Exception e) {
                    throw new IllegalStateException("algorithm " + algorithm + " in provider " + bcProvider.getName() + " but class \"" + className + "\" inaccessible!");
                }
            } else {
                return null;
            }

            implEngines.put(baseName + ":" + algorithm, implEngineClass);
        }

        try {
            return implEngineClass.newInstance();
        } catch (Exception e) {
            final Provider bcProvider = securityProvider;
            String className = implEngineClass.getName();
            throw new IllegalStateException("algorithm " + algorithm + " in provider " + bcProvider.getName() + " but class \"" + className + "\" inaccessible!");
        }
    }

    // the obligratory "reflection crap" :

    private static <T> T newInstance(Class<T> klass, Class<?>[] paramTypes, Object... params) {
        final Constructor<T> constructor;
        try {
            constructor = klass.getDeclaredConstructor(paramTypes);
            constructor.setAccessible(true);
            return constructor.newInstance(params);
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException(e.getMessage(), e);
        } catch (InvocationTargetException e) {
            throw new IllegalStateException(e.getTargetException());
        } catch (InstantiationException e) {
            throw new IllegalStateException(e);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    @SuppressWarnings("unchecked")
    private static <T> T invoke(Object object, Class<?> klass, String methodName, Class<?>[] paramTypes, Object... params) {
        final Method method;
        try {
            method = klass.getDeclaredMethod(methodName, paramTypes);
            method.setAccessible(true);
            return (T) method.invoke(object, params);
        } catch (NoSuchMethodException e) {
            throw new IllegalStateException(e.getMessage(), e);
        } catch (InvocationTargetException e) {
            throw new IllegalStateException(e.getTargetException());
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    private static void setField(Object obj, Class<?> fieldOwner, String fieldName, Object value) {
        final Field field;
        try {
            field = fieldOwner.getDeclaredField(fieldName);
            field.setAccessible(true);
            field.set(obj, value);
        } catch (NoSuchFieldException e) {
            throw new IllegalStateException("no field '" + fieldName + "' declared in " + fieldOwner + "", e);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }
}
