/***** BEGIN LICENSE BLOCK *****
 * Version: EPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 1.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.eclipse.org/legal/epl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2006, 2007 Ola Bini <ola@ologix.com>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package org.jruby.ext.openssl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.math.BigInteger;

import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;

import org.joda.time.DateTime;
import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyFixnum;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.RubyTime;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.openssl.impl.ASN1Registry;
import org.jruby.ext.openssl.x509store.PEMInputOutput;
import org.jruby.ext.openssl.x509store.X509AuxCertificate;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.builtin.Variable;
import org.jruby.runtime.component.VariableEntry;
import org.jruby.util.ByteList;

import static org.jruby.ext.openssl.X509._X509;
import static org.jruby.ext.openssl.X509Extension.newExtension;
import static org.jruby.ext.openssl.X509CRL.extensions_to_text;
import static org.jruby.ext.openssl.StringHelper.appendGMTDateTime;
import static org.jruby.ext.openssl.StringHelper.appendLowerHexValue;
import static org.jruby.ext.openssl.StringHelper.lowerHexBytes;
import static org.jruby.ext.openssl.OpenSSL.debug;
import static org.jruby.ext.openssl.OpenSSL.debugStackTrace;

/**
 * @author <a href="mailto:ola.bini@ki.se">Ola Bini</a>
 */
public class X509Cert extends RubyObject {
    private static final long serialVersionUID = -6524431607032364369L;

    private static ObjectAllocator X509CERT_ALLOCATOR = new ObjectAllocator() {
        public IRubyObject allocate(Ruby runtime, RubyClass klass) {
            return new X509Cert(runtime, klass);
        }
    };

    public static void createX509Cert(final Ruby runtime, final RubyModule X509) {
        RubyClass Certificate = X509.defineClassUnder("Certificate", runtime.getObject(), X509CERT_ALLOCATOR);
        RubyClass OpenSSLError = runtime.getModule("OpenSSL").getClass("OpenSSLError");
        X509.defineClassUnder("CertificateError", OpenSSLError, OpenSSLError.getAllocator());
        Certificate.defineAnnotatedMethods(X509Cert.class);
    }

    public X509Cert(Ruby runtime, RubyClass type) {
        super(runtime, type);
    }

    private X509Cert(Ruby runtime) {
        super(runtime, _Certificate(runtime));
    }

    private IRubyObject subject;
    private IRubyObject issuer;
    private BigInteger serial = BigInteger.ZERO;
    private RubyTime not_before;
    private RubyTime not_after;

    private IRubyObject sig_alg;
    private IRubyObject version;

    private X509Certificate cert;

    private transient PKey public_key; // lazy initialized

    private final List<X509Extension> extensions = new ArrayList<X509Extension>();

    private boolean changed = true;

    final X509AuxCertificate getAuxCert() {
        if ( cert == null ) return null;
        if ( cert instanceof X509AuxCertificate ) {
            return (X509AuxCertificate) cert;
        }
        return new X509AuxCertificate(cert);
    }

    public static IRubyObject wrap(Ruby runtime, Certificate cert)
        throws CertificateEncodingException {
        return wrap(runtime.getCurrentContext(), cert.getEncoded());
    }

    static X509Cert wrap(ThreadContext context, Certificate cert)
        throws CertificateEncodingException {
        return wrap(context, cert.getEncoded());
    }

    // this is the javax.security counterpart of the previous wrap method
    public static IRubyObject wrap(Ruby runtime, javax.security.cert.Certificate cert)
        throws javax.security.cert.CertificateEncodingException {
        return wrap(runtime.getCurrentContext(), cert.getEncoded());
    }

    static X509Cert wrap(ThreadContext context, javax.security.cert.Certificate cert)
        throws javax.security.cert.CertificateEncodingException {
        return wrap(context, cert.getEncoded());
    }

    static X509Cert wrap(final ThreadContext context, final byte[] encoded) {
        //final Ruby runtime = context.runtime;
        //final RubyString enc = StringHelper.newString(runtime, encoded);
        //return _Certificate(runtime).callMethod(context, "new", enc);
        final X509Cert cert = new X509Cert(context.runtime);
        cert.initialize(context, encoded);
        return cert;
    }

    @JRubyMethod(name="initialize", optional = 1, visibility = Visibility.PRIVATE)
    public IRubyObject initialize(final ThreadContext context,
        final IRubyObject[] args, final Block unusedBlock) {

        if ( args.length == 0 ) {
            this.subject = X509Name.newName(context.runtime);
            this.issuer = X509Name.newName(context.runtime);
            return this;
        }

        final RubyString str = StringHelper.readPossibleDERInput(context, args[0]);
        final ByteList bytes = str.getByteList();
        initialize(context, bytes.unsafeBytes(), bytes.getBegin(), bytes.getRealSize());

        return this;
    }

    private void initialize(final ThreadContext context, final byte[] encoded) {
        initialize(context, encoded, 0, encoded.length);
    }

    private void initialize(final ThreadContext context, final byte[] encoded, final int offset, final int length) {
        final Ruby runtime = context.runtime;

        byte[] bytes = StringHelper.readX509PEM(encoded, offset, length);

        try {
            final ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
            cert = (X509Certificate) SecurityHelper.getCertificateFactory("X.509").generateCertificate(bis);
        }
        catch (CertificateException e) {
            throw newCertificateError(runtime, e);
        }

        if ( cert == null ) {
            throw newCertificateError(runtime, (String) null);
        }

        set_serial( RubyNumeric.str2inum(runtime, runtime.newString(cert.getSerialNumber().toString()), 10) );
        set_not_before( context, RubyTime.newTime( runtime, cert.getNotBefore().getTime() ) );
        set_not_after( context, RubyTime.newTime( runtime, cert.getNotAfter().getTime() ) );
        this.subject = X509Name.newName(runtime, cert.getSubjectX500Principal());
        this.issuer = X509Name.newName(runtime, cert.getIssuerX500Principal());
        this.version = RubyFixnum.newFixnum(runtime, cert.getVersion() - 1);
        String sigAlgorithm = cert.getSigAlgOID();

        if ( sigAlgorithm == null ) sigAlgorithm = cert.getSigAlgName(); // e.g. SHA256withRSA
        else {
            sigAlgorithm = ASN1.oid2name(runtime, new ASN1ObjectIdentifier(sigAlgorithm), true);
            if ( sigAlgorithm == null ) {
                sigAlgorithm = "itu-t"; // MRI compability ... the "crazy" parts
                // for some certificates that MRI parses,
                // we get getSigAlgOID() == getSigAlgName() == "0.0"

                if ( cert.getSigAlgName() != null && ! cert.getSigAlgOID().equals(cert.getSigAlgName()) ) {
                    sigAlgorithm = cert.getSigAlgName(); // not sure if it makes any sense
                }
            }

        } // "hot" path e.g. sha256WithRSAEncryption
        this.sig_alg = RubyString.newString(runtime, sigAlgorithm);

        final Set<String> criticalExtOIDs = cert.getCriticalExtensionOIDs();
        if ( criticalExtOIDs != null ) {
            for ( final String extOID : criticalExtOIDs ) {
                addExtension(context, extOID, true);
            }
        }

        final Set<String> nonCriticalExtOIDs = cert.getNonCriticalExtensionOIDs();
        if ( nonCriticalExtOIDs != null ) {
            for ( final String extOID : nonCriticalExtOIDs ) {
                addExtension(context, extOID, false);
            }
        }
        changed = false;
    }

    private void addExtension(final ThreadContext context,
        final String extOID, final boolean critical) {
        try {
            final byte[] extValue = cert.getExtensionValue(extOID);
            if ( extValue == null ) return;
            final X509Extension[] extension = newExtension(context, extOID, extValue, critical);
            for ( int i = 0; i < extension.length; i++ ) this.extensions.add( extension[i] );
        }
        catch (IOException e) { throw newCertificateError(context.runtime, e); }
    }

    private static RubyClass _CertificateError(final Ruby runtime) {
        return _X509(runtime).getClass("CertificateError");
    }

    static RubyClass _Certificate(final Ruby runtime) {
        return _X509(runtime).getClass("Certificate");
    }

    public static RaiseException newCertificateError(final Ruby runtime, Exception e) {
        return Utils.newError(runtime, _CertificateError(runtime), e);
    }

    static RaiseException newCertificateError(final Ruby runtime, String msg) {
        return Utils.newError(runtime, _CertificateError(runtime), msg);
    }

    @Override
    @JRubyMethod(visibility = Visibility.PRIVATE)
    public IRubyObject initialize_copy(IRubyObject obj) {
        if ( this == obj ) return this;

        checkFrozen();
        return this;
    }

    @JRubyMethod
    public IRubyObject to_der() {
        try {
            return StringHelper.newString(getRuntime(), cert.getEncoded());
        }
        catch (CertificateEncodingException ex) {
            throw newCertificateError(getRuntime(), ex);
        }
    }

    @JRubyMethod(name = {"to_pem", "to_s"})
    public IRubyObject to_pem() {
        final StringWriter str = new StringWriter();
        try {
            PEMInputOutput.writeX509Certificate(str, getAuxCert());
            return getRuntime().newString( str.toString() );
        }
        catch (IOException ex) {
            throw getRuntime().newIOErrorFromException(ex);
        }
    }

    @JRubyMethod
    public IRubyObject to_text(final ThreadContext context) {
        final Ruby runtime = context.runtime;
        final char[] S20 = StringHelper.S20;
        final StringBuilder text = new StringBuilder(240);

        text.append("Certificate:\n");
        text.append(S20,0,4).append("Data:\n");
        final int version = this.version == null ? 0 : RubyNumeric.fix2int(this.version);
        text.append(S20,0,8).append("Version: ").append( version + 1 ).
             append(" (0x").append( Integer.toString( version, 16 ) ).append(")\n");
        // <= 0x1122334455667788 printed on same line as :
        // Serial Number: 1234605616436508552 (0x1122334455667788)
        // but 0x112233445566778899 ends up :
        // Serial Number:
        //      11:22:33:44:55:66:77:88:99
        text.append(S20,0,8).append("Serial Number:");
        if ( serial.compareTo( new BigInteger("FFFFFFFFFFFFFFFF", 16) ) > 0 ) {
            text.append('\n');
            text.append(S20,0,12).append( lowerHexBytes(serial.toByteArray(), 1) ).append('\n');
        }
        else {
            text.append(' ').append(serial.toString(10)).append(' ');
            text.append('(').append("0x").append(serial.toString(16)).append(')').append('\n');
        }

        text.append(S20,0,4).append("Signature Algorithm: ").append( signature_algorithm() ).append('\n');
        //final RubyString issuer = issuer().asString(); ByteList bytes = issuer.getByteList();
        //StringHelper.gsub(runtime, bytes, (byte) '/', (byte) ' ');
        //if ( bytes.charAt(0) == ' ' ) bytes.setBegin(bytes.getBegin() + 1);
        text.append(S20,0,8).append("Issuer: ").append( issuer ).append('\n');
        text.append(S20,0,8).append("Validity\n");
        text.append(S20,0,12).append("Not Before: ");
        appendGMTDateTime( text, getNotBefore() ).append('\n');
        text.append(S20,0,12).append("Not After : ");
        appendGMTDateTime( text, getNotAfter() ).append('\n');

        text.append(S20,0,8).append("Subject: ").append( subject() ).append('\n');
        text.append(S20,0,8).append("Subject Public Key Info:\n");

        final PublicKey publicKey = getPublicKey();
        text.append(S20,0,12).append("Public Key Algorithm: ").append(publicKey.getAlgorithm()).append('\n');

        if ( "RSA".equals( publicKey.getAlgorithm() ) ) {
            final RSAPublicKey rsaKey = ((RSAPublicKey) publicKey);
            text.append(S20,0,16).append("Public-Key: (").append( rsaKey.getModulus().bitLength() ).append(" bit)\n");

            text.append(S20,0,16).append("Modulus:\n");
            appendLowerHexValue(text, rsaKey.getModulus().toByteArray(), 20, 45);

            final BigInteger exponent = rsaKey.getPublicExponent();
            text.append(S20,0,16).append("Exponent: ").append(exponent).
                 append(" (0x").append( exponent.toString(16) ).append(")\n");
        }
        else if ( "DSA".equals( publicKey.getAlgorithm() ) ) {
            final DSAPublicKey dsaKey = ((DSAPublicKey) publicKey);
            text.append(S20,0,16).append("Public-Key: (").append( dsaKey.getY().bitLength() ).append(" bit)\n");

            text.append(S20,0,16).append("TODO: not-implemented (PR HOME-WORK)").append('\n'); // left-TODO
        }
        else {
            text.append(S20,0,16).append("TODO: not-implemented (PRs WELCOME!)").append('\n'); // left-TODO
        }

        if ( extensions != null && extensions.size() > 0 ) {
            text.append(S20,0,8).append("X509v3 extensions:\n");
            extensions_to_text(context, extensions, text, 12);
        }

        text.append(S20,0,4).append("Signature Algorithm: ").append( signature_algorithm() ).append('\n');

        appendLowerHexValue(text, getSignature(), 9, 54);

        return RubyString.newString( runtime, text );
    }

    @Override
    @JRubyMethod
    @SuppressWarnings("unchecked")
    public IRubyObject inspect() {
        final ArrayList<Variable<String>> varList = new ArrayList<Variable<String>>(5);
        varList.add(new VariableEntry<String>( "subject", subject().isNil() ? "nil" : subject().asString().toString() ));
        varList.add(new VariableEntry<String>( "issuer", issuer().isNil() ? "nil" : issuer().asString().toString() ));
        varList.add(new VariableEntry<String>( "serial", serial().isNil() ? "nil" : serial().asString().toString() ));
        varList.add(new VariableEntry<String>( "not_before", not_before().isNil() ? "nil" : not_before().toString() ));
        varList.add(new VariableEntry<String>( "not_after", not_after().isNil() ? "nil" : not_after().toString() ));

        return ObjectSupport.inspect(this, (List) varList);
    }

    @JRubyMethod
    public IRubyObject version() {
        return version != null ? version : ( version = getRuntime().newFixnum(0) );
    }

    @JRubyMethod(name = "version=")
    public IRubyObject set_version(final IRubyObject version) {
        if ( ! version().equals(version) ) {
            this.changed = true;
        }
        return this.version = version;
    }

    @JRubyMethod
    public IRubyObject signature_algorithm() {
        return sig_alg;
    }

    private byte[] getSignature() {
        return cert.getSignature();
    }

    BigInteger getSerial() { return serial; }

    @JRubyMethod
    public IRubyObject serial() {
        return BN.newBN(getRuntime(), serial);
    }

    @JRubyMethod(name = "serial=")
    public IRubyObject set_serial(final IRubyObject serial) {
        final String serialStr = serial.asString().toString();
        final BigInteger serialInt;
        if ( serialStr.equals("0") ) { // MRI compatibility: allow 0 serial number
            serialInt = BigInteger.ONE;
        } else {
            serialInt = new BigInteger(serialStr);
        }
        this.changed = ! serialInt.equals(this.serial);
        //generator.setSerialNumber( serialInt.abs() );
        this.serial = serialInt; return serial;
    }

    X509Name getSubject() { return ((X509Name) subject); }

    @JRubyMethod
    public IRubyObject subject() {
        return subject;
    }

    @JRubyMethod(name = "subject=")
    public IRubyObject set_subject(final IRubyObject subject) {
        if ( ! subject.equals(this.subject) ) this.changed = true;
        return this.subject = subject;
    }

    X509Name getIssuer() { return ((X509Name) issuer); }

    @JRubyMethod
    public IRubyObject issuer() {
        return issuer;
    }

    @JRubyMethod(name = "issuer=")
    public IRubyObject set_issuer(final IRubyObject issuer) {
        if ( ! issuer.equals(this.issuer) ) this.changed = true;
        return this.issuer = issuer;
    }

    @JRubyMethod
    public IRubyObject not_before() {
        return not_before == null ? getRuntime().getNil() : not_before;
    }

    @JRubyMethod(name = "not_before=")
    public IRubyObject set_not_before(final ThreadContext context, final IRubyObject time) {
        changed = true;
        not_before = (RubyTime) time.callMethod(context, "getutc");
        not_before.setMicroseconds(0);
        return time;
    }

    DateTime getNotBefore() {
        return not_before == null ? null : not_before.getDateTime();
    }

    @JRubyMethod
    public IRubyObject not_after() {
        return not_after == null ? getRuntime().getNil() : not_after;
    }

    @JRubyMethod(name = "not_after=")
    public IRubyObject set_not_after(final ThreadContext context, final IRubyObject time) {
        changed = true;
        not_after = (RubyTime) time.callMethod(context, "getutc");
        not_after.setMicroseconds(0);
        return time;
    }

    DateTime getNotAfter() {
        return not_after == null ? null : not_after.getDateTime();
    }

    @JRubyMethod
    public IRubyObject public_key(final ThreadContext context) {
        if ( public_key == null ) initializePublicKey();
        return public_key.callMethod(context, "public_key");
    }

    @JRubyMethod(name = "public_key=")
    public IRubyObject set_public_key(IRubyObject public_key) {
        if ( ! ( public_key instanceof PKey ) ) {
            throw getRuntime().newTypeError("OpenSSL::PKey::PKey expected but got " + public_key.getMetaClass().getName());
        }
        if ( ! public_key.equals(this.public_key) ) {
            this.changed = true;
        }
        return this.public_key = (PKey) public_key;
    }

    private PublicKey getPublicKey() {
        if ( public_key == null ) initializePublicKey();
        return public_key.getPublicKey();
    }

    private void initializePublicKey() throws RaiseException {
        final Ruby runtime = getRuntime();

        final boolean changed = this.changed;

        if ( cert == null ) {
            throw newCertificateError(runtime, "no certificate");
        }

        final PublicKey publicKey = cert.getPublicKey();

        final String algorithm = publicKey.getAlgorithm();

        if ( "RSA".equalsIgnoreCase(algorithm) ) {
            //if ( public_key == null ) {
            //    throw new IllegalStateException("no public key encoded data");
            //}
            set_public_key( PKeyRSA.newInstance(runtime, publicKey) );
        }
        else if ( "DSA".equalsIgnoreCase(algorithm) ) {
            //if ( public_key == null ) {
            //    throw new IllegalStateException("no public key encoded data");
            //}
            set_public_key( PKeyDSA.newInstance(runtime, publicKey) );
        }
        else {
            String message = "unsupported algorithm";
            if ( algorithm != null ) message += " '" + algorithm + "'";
            throw newCertificateError(runtime, message);
        }

        this.changed = changed;
    }

    @JRubyMethod
    public IRubyObject sign(final ThreadContext context, final IRubyObject key, final IRubyObject digest) {
        final Ruby runtime = context.runtime;

        // Have to obey some artificial constraints of the OpenSSL implementation. Stupid.
        final String keyAlg = ((PKey) key).getAlgorithm();
        final String digAlg; final String digName;
        if (digest instanceof Digest) {
            digAlg = ((Digest) digest).getShortAlgorithm();
            digName = ((Digest) digest).name().toString();
        }
        else {
            digAlg = digest.asJavaString(); digName = null;
        }

        if( ( "DSA".equalsIgnoreCase(keyAlg) && "MD5".equalsIgnoreCase(digAlg) ) ||
            ( "RSA".equalsIgnoreCase(keyAlg) && "DSS1".equals(digName) ) ) {
            throw newCertificateError(runtime, "signature_algorithm not supported");
        }

        org.bouncycastle.x509.X509V3CertificateGenerator builder = getCertificateBuilder();

        for ( X509Extension ext : uniqueExtensions() ) {
            try {
                final byte[] bytes = ext.getRealValueEncoded();
                builder.addExtension(ext.getRealObjectID().getId(), ext.isRealCritical(), bytes);
            }
            catch (IOException ioe) {
                throw runtime.newIOErrorFromException(ioe);
            }
        }

        builder.setSignatureAlgorithm(digAlg + "WITH" + keyAlg); // "SHA1WITHRSA"

        try {
            cert = builder.generate( ((PKey) key).getPrivateKey() );
        }
        catch (GeneralSecurityException e) {
            throw newCertificateError(runtime, e);
        }

        if (cert == null) throw newCertificateError(runtime, (String) null);

        String name = ASN1Registry.o2a(cert.getSigAlgOID());
        if ( name == null ) name = cert.getSigAlgOID();
        this.sig_alg = runtime.newString(name);
        this.changed = false;
        return this;
    }

    private org.bouncycastle.x509.X509V3CertificateGenerator getCertificateBuilder() {
        org.bouncycastle.x509.X509V3CertificateGenerator generator =
            new org.bouncycastle.x509.X509V3CertificateGenerator();
        generator.setSerialNumber( serial.abs() );

        if ( subject != null ) generator.setSubjectDN( ((X509Name) subject).getRealName() );
        if ( issuer != null ) generator.setIssuerDN( ((X509Name) issuer).getRealName() );

        generator.setNotBefore( not_before.getJavaDate() );
        generator.setNotAfter( not_after.getJavaDate() );
        generator.setPublicKey( getPublicKey() );

        return generator;
    }

    //private transient org.bouncycastle.x509.X509V3CertificateGenerator generator;

    @JRubyMethod
    public RubyBoolean verify(final IRubyObject key) {
        final Ruby runtime = getRuntime();

        if ( changed ) return runtime.getFalse();

        try {
            cert.verify(((PKey) key).getPublicKey());
            return runtime.getTrue();
        }
        catch (CertificateException e) {
            debug(runtime, "Certificate#verify failed: ", e);
            throw newCertificateError(runtime, e);
        }
        catch (NoSuchAlgorithmException e) {
            debugStackTrace(runtime, e);
            throw newCertificateError(runtime, e);
        }
        catch (NoSuchProviderException e) {
            debugStackTrace(runtime, e);
            throw newCertificateError(runtime, e);
        }
        catch (SignatureException e) {
            debug(runtime, "Certificate#verify failed: ", e);
            return runtime.getFalse();
        }
        catch (InvalidKeyException e) {
            debug(runtime, "Certificate#verify failed: ", e);
            return runtime.getFalse();
        }
    }

    @JRubyMethod
    public RubyBoolean check_private_key(final IRubyObject key) {
        final PublicKey certPublicKey = cert.getPublicKey();
        if ( certPublicKey.equals( ((PKey) key).getPublicKey() ) ) {
            return getRuntime().getTrue();
        }
        return getRuntime().getFalse();
    }

    @JRubyMethod
    public RubyArray extensions() {
        @SuppressWarnings("unchecked")
        final List<IRubyObject> extensions = (List) this.extensions;
        return getRuntime().newArray( extensions );
    }

    @SuppressWarnings("unchecked")
    @JRubyMethod(name = "extensions=")
    public IRubyObject set_extensions(final IRubyObject array) {
        extensions.clear(); // RubyArray is a List :
        extensions.addAll( (List<X509Extension>) array );
        return array;
    }

    @JRubyMethod
    public IRubyObject add_extension(final IRubyObject ext) {
        changed = true;
        extensions.add((X509Extension) ext);
        return ext;
    }

    private Collection<X509Extension> uniqueExtensions() {
        final Map<ASN1ObjectIdentifier, X509Extension> unique =
            new LinkedHashMap<ASN1ObjectIdentifier, X509Extension>();

        for ( X509Extension current : this.extensions ) {

            final ASN1ObjectIdentifier oid = current.getRealObjectID();
            final X509Extension existing = unique.get( oid );
            if ( existing == null ) {
                unique.put( oid, current ); continue;
            }

            // NOTE: dealing with Java API limits here since it does not
            // handle multiple OID mappings to a sequence out of the box

            // commonly used e.g. with subjectAltName || issuserAltName :
            if ( "2.5.29.17".equals( oid.getId() ) || "2.5.29.18".equals( oid.getId() ) ) {
                final ASN1EncodableVector vec = new ASN1EncodableVector();
                try {
                    GeneralName[] n1 = extRealNames(existing);
                    for ( int i = 0; i < n1.length; i++ ) vec.add( n1[i] );
                    GeneralName[] n2 = extRealNames(current);
                    for ( int i = 0; i < n2.length; i++ ) vec.add( n2[i] );

                    GeneralNames nn = GeneralNames.getInstance(new DLSequence(vec));
                    final X509Extension existingDup = existing.clone();
                    existingDup.setRealValue( nn );
                    unique.put( oid, existingDup );
                }
                catch (IOException ex) { throw getRuntime().newIOErrorFromException(ex); }
                continue;
            }

            // TODO do we need special care for any others here ?!?

            final ASN1EncodableVector vec = new ASN1EncodableVector();
            try {
                final ASN1Encodable existingValue = existing.getRealValue();
                if ( existingValue instanceof ASN1Sequence ) {
                    final ASN1Sequence seq = (ASN1Sequence) existingValue;
                    for ( int i = 0; i < seq.size(); i++ ) {
                        vec.add( seq.getObjectAt(i) );
                    }
                }
                else {
                    vec.add(existingValue);
                }
                vec.add( current.getRealValue() );

                // existing.setRealValue( new DLSequence(vec) );
                final X509Extension existingDup = existing.clone();
                existingDup.setRealValue( new DLSequence(vec) );
                unique.put( oid, existingDup );
            }
            catch (IOException ex) { throw getRuntime().newIOErrorFromException(ex); }

        }
        return unique.values();
    }

    private static GeneralName[] extRealNames(final X509Extension extension) throws IOException {
        final ASN1Encodable value = extension.getRealValue();
        if ( value instanceof GeneralName ) {
            return new GeneralName[] { (GeneralName) value };
        }
        return GeneralNames.getInstance( value ).getNames();
    }

    @Override
    public Object toJava(Class target) {
        if ( target.isAssignableFrom(X509Certificate.class) ) {
            if ( target == X509AuxCertificate.class ) return getAuxCert();
            return cert;
        }
        return super.toJava(target);
    }

}// X509Cert
