org.jruby.ext.openssl.X509Extension.java Source code

Java tutorial

Introduction

Here is the source code for org.jruby.ext.openssl.X509Extension.java

Source

/***** 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.IOException;
import java.lang.reflect.Field;
import java.util.Hashtable;
import java.util.Map;

import org.bouncycastle.asn1.ASN1Boolean;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Encoding;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.BERTags;
import org.bouncycastle.asn1.DERBoolean;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.DERUniversalString;
import org.bouncycastle.asn1.DLSequence;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.util.encoders.Hex;

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyClass;
import org.jruby.RubyHash;
import org.jruby.RubyModule;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
import org.jruby.RubyString;
import org.jruby.anno.JRubyMethod;
import org.jruby.exceptions.RaiseException;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
import org.jruby.util.ConvertBytes;

import static org.jruby.ext.openssl.ASN1._ASN1;
import static org.jruby.ext.openssl.X509._X509;
import static org.jruby.ext.openssl.OpenSSL.*;
import static org.jruby.ext.openssl.StringHelper.*;

/**
 * OpenSSL::X509::Extension
 * @author kares
 */
public class X509Extension extends RubyObject {
    private static final long serialVersionUID = 6463713017143658305L;

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

    public static void createX509Extension(final Ruby runtime, final RubyModule _X509) { // OpenSSL::X509
        final RubyClass _OpenSSLError = runtime.getModule("OpenSSL").getClass("OpenSSLError");
        _X509.defineClassUnder("ExtensionError", _OpenSSLError, _OpenSSLError.getAllocator());

        RubyClass _Extension = _X509.defineClassUnder("Extension", runtime.getObject(), X509Extension.ALLOCATOR);
        _Extension.defineAnnotatedMethods(X509Extension.class);

        X509ExtensionFactory.createX509ExtensionFactory(runtime, _X509);
    }

    private ASN1ObjectIdentifier objectID;
    private Object value;
    private boolean critical;

    protected X509Extension(Ruby runtime, RubyClass type) {
        super(runtime, type);
    }

    static RubyClass _Extension(final Ruby runtime) {
        return _X509(runtime).getClass("Extension");
    }

    static final byte[] critical__ = new byte[] { 'c', 'r', 'i', 't', 'i', 'c', 'a', 'l', ',', ' ' };

    static X509Extension newExtension(final ThreadContext context, final String oid,
            final java.security.cert.X509Extension ext, final boolean critical) throws IOException {

        final byte[] extValue = ext.getExtensionValue(oid); // DER encoded
        // TODO: wired. J9 returns null for an OID given in getNonCriticalExtensionOIDs()
        if (extValue == null) {
            warn(context, ext + " getExtensionValue returns null for '" + oid + "'");
            return null;
        }

        final Ruby runtime = context.runtime;
        final ASN1Encodable value = ASN1.readObject(extValue);
        return newExtension(runtime, ASN1.getObjectID(runtime, oid), value, critical);
    }

    static X509Extension[] newExtension(final ThreadContext context, final String oid, final byte[] extValue,
            final boolean critical) throws IOException {

        final Ruby runtime = context.runtime;
        final ASN1ObjectIdentifier objectId = ASN1.getObjectID(runtime, oid);
        final ASN1Encodable value = ASN1.readObject(extValue);

        if (oid.equals("2.5.29.17") || oid.equals("2.5.29.18")) { // subjectAltName || issuerAltName
            if (value instanceof ASN1OctetString) { // DEROctetString
                final ASN1Encodable oct = ASN1.readObject(((ASN1OctetString) value).getOctets());
                if (oct instanceof ASN1Sequence) {
                    final ASN1Sequence seq = (ASN1Sequence) oct;
                    final X509Extension[] ext = new X509Extension[seq.size()];
                    for (int i = 0; i < ext.length; i++) {
                        ext[i] = newExtension(runtime, objectId, seq.getObjectAt(i), critical);
                    }
                    return ext;
                }
                // NOTE need to unwrap ((ASN1TaggedObject) oct).getObject() - likely not ?!?
                return new X509Extension[] { newExtension(runtime, objectId, oct, critical) };
            }
        }

        return new X509Extension[] { newExtension(runtime, objectId, value, critical) };
    }

    static X509Extension newExtension(final Ruby runtime, ASN1ObjectIdentifier objectId, final ASN1Encodable value,
            final boolean critical) {
        X509Extension ext = new X509Extension(runtime, _Extension(runtime));
        ext.setRealObjectID(objectId);
        ext.setRealValue(value);
        ext.setRealCritical(critical);
        return ext;
    }

    static X509Extension newExtension(final Ruby runtime, ASN1ObjectIdentifier objectId,
            final Extension extension) {
        X509Extension ext = new X509Extension(runtime, _Extension(runtime));
        ext.setRealObjectID(objectId);
        ext.setRealValue(extension.getParsedValue());
        ext.setRealCritical(extension.isCritical());
        return ext;
    }

    ASN1ObjectIdentifier getRealObjectID() {
        return objectID;
    }

    void setRealObjectID(ASN1ObjectIdentifier oid) {
        this.objectID = oid;
    }

    void setRealObjectID(final String oid) {
        setRealObjectID(ASN1.getObjectID(getRuntime(), oid));
    }

    final ASN1Encodable getRealValue() throws IOException {
        if (value instanceof ASN1Encodable) {
            return (ASN1Encodable) value;
        }
        if (value instanceof ASN1.ASN1Data) {
            return ((ASN1.ASN1Data) value).toASN1(getRuntime().getCurrentContext());
        }

        if (value == null)
            throw new IllegalStateException("null extension value");

        return ASN1.readObject(getRealValueEncoded());
    }

    final byte[] getRealValueEncoded() throws IOException {
        if (value instanceof byte[])
            return (byte[]) value;
        if (value instanceof RubyString)
            return ((RubyString) value).getBytes();
        if (value instanceof String)
            return ByteList.plain((String) value);

        if (value instanceof ASN1OctetString) { // initialize
            return ((ASN1OctetString) value).getOctets();
        }

        return getRealValue().toASN1Primitive().getEncoded(ASN1Encoding.DER);
    }

    private IRubyObject getValue(final Ruby runtime) throws IOException {
        if (value instanceof RubyString) {
            return (RubyString) value; // explicitly set value
        }
        final ThreadContext context = runtime.getCurrentContext();
        final byte[] enc = getRealValueEncoded();
        IRubyObject extValue = runtime.newString(new ByteList(enc, false));
        extValue = ASN1.decodeImpl(context, _ASN1(runtime), extValue);
        return extValue.callMethod(context, "value");
    }

    void setRealValue(final ASN1Encodable value) {
        if (value == null) {
            throw new IllegalStateException("null extension value");
        }
        this.value = value;
    }

    //private void setRealValueEncoded(final byte[] value) {
    //    this.value = value;
    //}

    boolean isRealCritical() {
        return critical;
    }

    void setRealCritical(boolean critical) {
        this.critical = critical;
    }

    @JRubyMethod(name = "initialize", rest = true, visibility = Visibility.PRIVATE)
    public IRubyObject initialize(final ThreadContext context, final IRubyObject[] args) {
        if (args.length == 1) {
            final byte[] bytes = to_der_if_possible(context, args[0]).asString().getBytes();
            try {
                ASN1Sequence seq = (ASN1Sequence) ASN1.readObject(bytes);
                setRealObjectID((ASN1ObjectIdentifier) seq.getObjectAt(0));
                final ASN1Encodable criticalOrValue = seq.getObjectAt(1);
                if (criticalOrValue instanceof ASN1Boolean) {
                    setRealCritical(((ASN1Boolean) criticalOrValue).isTrue());
                    this.value = ((DEROctetString) seq.getObjectAt(2)).getOctets(); // byte[]
                } else if (criticalOrValue instanceof DERBoolean) { // NOTE: keep it due BC <= 1.50
                    setRealCritical(((DERBoolean) criticalOrValue).isTrue());
                    this.value = ((DEROctetString) seq.getObjectAt(2)).getOctets(); // byte[]
                } else {
                    this.value = ((DEROctetString) criticalOrValue).getOctets(); // byte[]
                }
            } catch (IOException e) {
                throw newExtensionError(context.runtime, e);
            }
        } else if (args.length > 1) {
            setRealObjectID(ASN1.getObjectID(context.runtime, args[0].toString()));
            this.value = args[1]; // a RubyString
        } else { // args.length < 1
            throw context.runtime.newArgumentError("wrong number of arguments (0 for 1..3)");
        }

        if (args.length > 2)
            setRealCritical(args[2].isTrue());

        return this;
    }

    @JRubyMethod
    public IRubyObject oid(final ThreadContext context) {
        return context.runtime.newString(oidSym(context.runtime));
    }

    private String oidSym(final Ruby runtime) {
        final String name = ASN1.oid2Sym(runtime, objectID, true);
        return name == null ? objectID.toString() : name;
    }

    @JRubyMethod(name = "oid=")
    public IRubyObject set_oid(final ThreadContext context, IRubyObject arg) {
        if (arg instanceof RubyString) {
            setRealObjectID(arg.toString());
            return arg;
        }
        throw context.runtime.newTypeError(arg, context.runtime.getString());
    }

    private static final byte[] CA_ = { 'C', 'A', ':' };
    private static final byte[] TRUE = { 'T', 'R', 'U', 'E' };
    private static final byte[] FALSE = { 'F', 'A', 'L', 'S', 'E' };
    private static final byte[] _ = {};
    private static final byte[] SEP = { ',', ' ' };
    private static final byte[] Decipher_Only = { 'D', 'e', 'c', 'i', 'p', 'h', 'e', 'r', ' ', 'O', 'n', 'l', 'y' };
    private static final byte[] Digital_Signature = { 'D', 'i', 'g', 'i', 't', 'a', 'l', ' ', 'S', 'i', 'g', 'n',
            'a', 't', 'u', 'r', 'e' };
    private static final byte[] Non_Repudiation = { 'N', 'o', 'n', ' ', 'R', 'e', 'p', 'u', 'd', 'i', 'a', 't', 'i',
            'o', 'n' };
    private static final byte[] Key_Encipherment = { 'K', 'e', 'y', ' ', 'E', 'n', 'c', 'i', 'p', 'h', 'e', 'r',
            'm', 'e', 'n', 't' };
    private static final byte[] Data_Encipherment = { 'D', 'a', 't', 'a', ' ', 'E', 'n', 'c', 'i', 'p', 'h', 'e',
            'r', 'm', 'e', 'n', 't' };
    private static final byte[] Key_Agreement = { 'K', 'e', 'y', ' ', 'A', 'g', 'r', 'e', 'e', 'm', 'e', 'n', 't' };
    private static final byte[] Certificate_Sign = { 'C', 'e', 'r', 't', 'i', 'f', 'i', 'c', 'a', 't', 'e', ' ',
            'S', 'i', 'g', 'n' };
    private static final byte[] CRL_Sign = { 'C', 'R', 'L', ' ', 'S', 'i', 'g', 'n' };
    private static final byte[] Encipher_Only = { 'E', 'n', 'c', 'i', 'p', 'h', 'e', 'r', ' ', 'O', 'n', 'l', 'y' };
    private static final byte[] SSL_Client = { 'S', 'S', 'L', ' ', 'C', 'l', 'i', 'e', 'n', 't' };
    private static final byte[] SSL_Server = { 'S', 'S', 'L', ' ', 'S', 'e', 'r', 'v', 'e', 'r' };
    private static final byte[] SSL_CA = { 'S', 'S', 'L', ' ', 'C', 'A' };
    private static final byte[] SMIME = { 'S', '/', 'M', 'I', 'M', 'E' };
    private static final byte[] SMIME_CA = { 'S', '/', 'M', 'I', 'M', 'E', ' ', 'C', 'A' };
    private static final byte[] Object_Signing = { 'O', 'b', 'j', 'e', 'c', 't', ' ', 'S', 'i', 'g', 'n', 'i', 'n',
            'g' };
    private static final byte[] Object_Signing_CA = { 'O', 'b', 'j', 'e', 'c', 't', ' ', 'S', 'i', 'g', 'n', 'i',
            'n', 'g', ' ', 'C', 'A' };
    private static final byte[] Unused = { 'U', 'n', 'u', 's', 'e', 'd' };
    private static final byte[] Unspecified = { 'U', 'n', 's', 'p', 'e', 'c', 'i', 'f', 'i', 'e', 'd' };
    //private static final byte[] Key_Compromise = { 'K','e','y',' ','C','o','m','p','r','o','m','i','s','e' };
    //private static final byte[] CA_Compromise = { 'C','A',' ','C','o','m','p','r','o','m','i','s','e' };
    //private static final byte[] Affiliation_Changed = { 'A','f','f','i','l','i','a','t','i','o','n',' ','C','h','a','n','g','e','d' };
    private static final byte[] keyid_ = { 'k', 'e', 'y', 'i', 'd', ':' };

    @JRubyMethod
    public RubyString value(final ThreadContext context) {
        if (this.value instanceof RubyString) { // return the same as set
            return (RubyString) this.value;
        }

        final Ruby runtime = context.runtime;
        final String oid = getRealObjectID().getId();
        try {
            if (oid.equals("2.5.29.19")) { // basicConstraints
                ASN1Sequence seq2 = (ASN1Sequence) ASN1.readObject(getRealValueEncoded());
                final ByteList val = new ByteList(32);
                if (seq2.size() > 0) {
                    val.append(CA_);
                    ASN1Encodable obj0 = seq2.getObjectAt(0);
                    final boolean bool;
                    if (obj0 instanceof ASN1Boolean) {
                        bool = ((ASN1Boolean) obj0).isTrue();
                    } else { // NOTE: keep it due BC <= 1.50
                        bool = ((DERBoolean) obj0).isTrue();
                    }
                    val.append(bool ? TRUE : FALSE);
                }
                if (seq2.size() > 1) {
                    val.append(", pathlen:".getBytes());
                    val.append(seq2.getObjectAt(1).toString().getBytes());
                }
                return runtime.newString(val);
            }
            if (oid.equals("2.5.29.15")) { // keyUsage
                final byte[] enc = getRealValueEncoded();
                byte b3 = 0;
                byte b2 = enc[2];
                if (enc.length > 3)
                    b3 = enc[3];
                final ByteList val = new ByteList(64);
                byte[] sep = _;
                if ((b2 & (byte) 128) != 0) {
                    val.append(sep);
                    val.append(Decipher_Only);
                    sep = SEP;
                }
                if ((b3 & (byte) 128) != 0) {
                    val.append(sep);
                    val.append(Digital_Signature);
                    sep = SEP;
                }
                if ((b3 & (byte) 64) != 0) {
                    val.append(sep);
                    val.append(Non_Repudiation);
                    sep = SEP;
                }
                if ((b3 & (byte) 32) != 0) {
                    val.append(sep);
                    val.append(Key_Encipherment);
                    sep = SEP;
                }
                if ((b3 & (byte) 16) != 0) {
                    val.append(sep);
                    val.append(Data_Encipherment);
                    sep = SEP;
                }
                if ((b3 & (byte) 8) != 0) {
                    val.append(sep);
                    val.append(Key_Agreement);
                    sep = SEP;
                }
                if ((b3 & (byte) 4) != 0) {
                    val.append(sep);
                    val.append(Certificate_Sign);
                    sep = SEP;
                }
                if ((b3 & (byte) 2) != 0) {
                    val.append(sep);
                    val.append(CRL_Sign);
                    sep = SEP;
                }
                if ((b3 & (byte) 1) != 0) {
                    val.append(sep);
                    val.append(Encipher_Only); // sep = SEP;
                }
                return runtime.newString(val);
            }
            if (oid.equals("2.16.840.1.113730.1.1")) { // nsCertType
                final byte b0 = getRealValueEncoded()[0];
                final ByteList val = new ByteList(64);
                byte[] sep = _;
                if ((b0 & (byte) 128) != 0) {
                    val.append(sep);
                    val.append(SSL_Client);
                    sep = SEP;
                }
                if ((b0 & (byte) 64) != 0) {
                    val.append(sep);
                    val.append(SSL_Server);
                    sep = SEP;
                }
                if ((b0 & (byte) 32) != 0) {
                    val.append(sep);
                    val.append(SMIME);
                    sep = SEP;
                }
                if ((b0 & (byte) 16) != 0) {
                    val.append(sep);
                    val.append(Object_Signing);
                    sep = SEP;
                }
                if ((b0 & (byte) 8) != 0) {
                    val.append(sep);
                    val.append(Unused);
                    sep = SEP;
                }
                if ((b0 & (byte) 4) != 0) {
                    val.append(sep);
                    val.append(SSL_CA);
                    sep = SEP;
                }
                if ((b0 & (byte) 2) != 0) {
                    val.append(sep);
                    val.append(SMIME_CA);
                    sep = SEP;
                }
                if ((b0 & (byte) 1) != 0) {
                    val.append(sep);
                    val.append(Object_Signing_CA);
                }
                return runtime.newString(val);
            }

            if (oid.equals("2.5.29.14")) { // subjectKeyIdentifier
                ASN1Encodable value = getRealValue();
                if (value instanceof ASN1OctetString) {
                    byte[] octets = ((ASN1OctetString) value).getOctets();
                    if (octets.length > 0 && octets[0] == BERTags.OCTET_STRING) {
                        value = ASN1.readObject(octets); // read nested octets
                    }
                }
                return runtime.newString(hexBytes(keyidBytes(value.toASN1Primitive()), 0));
            }

            if (oid.equals("2.5.29.35")) { // authorityKeyIdentifier
                ASN1Encodable value = getRealValue();

                if (value instanceof ASN1OctetString) {
                    value = ASN1.readObject(((ASN1OctetString) value).getOctets());
                }

                final ByteList val = new ByteList(72);
                val.append(keyid_);

                if (value instanceof ASN1Sequence) {
                    final ASN1Sequence seq = (ASN1Sequence) value;
                    final int size = seq.size();
                    if (size == 0)
                        return RubyString.newEmptyString(runtime);

                    ASN1Primitive keyid = seq.getObjectAt(0).toASN1Primitive();
                    hexBytes(keyidBytes(keyid), val).append('\n');

                    for (int i = 1; i < size; i++) {
                        final ASN1Encodable issuer = seq.getObjectAt(i);
                        // NOTE: blindly got OpenSSL tests passing (likely in-complete) :
                        if (issuer instanceof ASN1TaggedObject) {
                            ASN1Primitive obj = ((ASN1TaggedObject) issuer).getObject();
                            switch (((ASN1TaggedObject) issuer).getTagNo()) {
                            case 1:
                                if (obj instanceof ASN1TaggedObject) {
                                    formatGeneralName(GeneralName.getInstance(obj), val, true);
                                }
                                break;
                            case 2: // serial
                                val.append(new byte[] { 's', 'e', 'r', 'i', 'a', 'l', ':' });
                                hexBytes(((ASN1OctetString) obj).getOctets(), val);
                                break;
                            }
                        }
                        val.append('\n');
                    }
                    return runtime.newString(val);
                }

                hexBytes(keyidBytes(value.toASN1Primitive()), val).append('\n');
                return runtime.newString(val);
            }

            if (oid.equals("2.5.29.21")) { // CRLReason
                final IRubyObject value = getValue(runtime);
                switch (RubyNumeric.fix2int(value)) {
                case 0:
                    return runtime.newString(new ByteList(Unspecified));
                case 1:
                    return RubyString.newString(runtime, "Key Compromise");
                case 2:
                    return RubyString.newString(runtime, "CA Compromise");
                case 3:
                    return RubyString.newString(runtime, "Affiliation Changed");
                case 4:
                    return RubyString.newString(runtime, "Superseded");
                case 5:
                    return RubyString.newString(runtime, "Cessation Of Operation");
                case 6:
                    return RubyString.newString(runtime, "Certificate Hold");
                case 8:
                    return RubyString.newString(runtime, "Remove From CRL");
                case 9:
                    return RubyString.newString(runtime, "Privilege Withdrawn");
                default:
                    return runtime.newString(new ByteList(Unspecified));
                }
            }

            if (oid.equals("2.5.29.17") || oid.equals("2.5.29.18")) { // subjectAltName || issuerAltName
                try {
                    ASN1Encodable value = getRealValue();
                    final ByteList val = new ByteList(64);
                    if (value instanceof ASN1TaggedObject) {
                        formatGeneralName(GeneralName.getInstance(value), val, false);
                        return runtime.newString(val);
                    }
                    if (value instanceof GeneralName) {
                        formatGeneralName((GeneralName) value, val, false);
                        return runtime.newString(val);
                    }
                    if (value instanceof ASN1OctetString) {
                        // decoded octets will end up as an ASN1Sequence instance :
                        value = ASN1.readObject(((ASN1OctetString) value).getOctets());
                    }
                    if (value instanceof ASN1TaggedObject) { // DERTaggedObject (issuerAltName wrapping)
                        formatGeneralName(GeneralName.getInstance(value), val, false);
                        return runtime.newString(val);
                    }

                    final GeneralName[] names = GeneralNames.getInstance(value).getNames();
                    for (int i = 0; i < names.length; i++) {
                        boolean other = formatGeneralName(names[i], val, false);
                        if (i < names.length - 1) {
                            if (other)
                                val.append(';');
                            else
                                val.append(',');
                        }
                    }
                    return runtime.newString(val);
                } catch (IllegalArgumentException e) {
                    debugStackTrace(runtime, e);
                    return rawValueAsString(context);
                }
            }

            if (oid.equals("2.5.29.37")) { // extendedKeyUsage
                final ByteList val = new ByteList(64);

                if (this.value instanceof ASN1Sequence) { // opt "short" path
                    final ASN1Sequence seq = (ASN1Sequence) this.value;
                    final int size = seq.size();
                    for (int i = 0; i < size; i++) {
                        ASN1Encodable o = seq.getObjectAt(i);
                        String name = o.toString();
                        Integer nid = ASN1.oid2nid(runtime, new ASN1ObjectIdentifier(name));
                        if (nid != null)
                            name = ASN1.nid2ln(runtime, nid);
                        if (name == null)
                            name = o.toString();
                        val.append(ByteList.plain(name));
                        if (i < size - 1)
                            val.append(',').append(' ');
                    }
                    return runtime.newString(val);
                }

                final IRubyObject value = getValue(runtime);
                if (value instanceof RubyArray) {
                    final RubyArray arr = (RubyArray) value;
                    final int size = arr.size();
                    for (int i = 0; i < size; i++) {
                        IRubyObject entry = arr.eltInternal(i);
                        if ("ObjectId".equals(entry.getMetaClass().getBaseName())) {
                            entry = entry.callMethod(context, "ln");
                        } else if (entry.respondsTo("value")) {
                            entry = entry.callMethod(context, "value");
                        }
                        val.append(entry.asString().getByteList());
                        if (i < size - 1)
                            val.append(',').append(' ');
                    }
                }
                return runtime.newString(val);
            }

            return rawValueAsString(context);
        } catch (IOException e) {
            debugStackTrace(runtime, e);
            throw newExtensionError(runtime, e);
        }
    }

    private RubyString rawValueAsString(final ThreadContext context) throws IOException {
        final Ruby runtime = context.runtime;
        final IRubyObject value = getValue(runtime); // e.g. [ ASN1::UTF8String, ... ]
        if (value instanceof RubyArray) {
            final RubyArray arr = (RubyArray) value;
            final ByteList strVal = new ByteList(64);
            final int len = arr.size();
            for (int i = 0; i < len; i++) {
                IRubyObject entry = arr.eltInternal(i);
                if (entry.respondsTo("value")) {
                    entry = entry.callMethod(context, "value");
                }
                strVal.append(entry.asString().getByteList());
                if (i < len - 1)
                    strVal.append(',').append(' ');
            }
            return runtime.newString(strVal);
        }
        return value.asString();
    }

    private static byte[] keyidBytes(ASN1Primitive keyid) throws IOException {
        if (keyid instanceof ASN1TaggedObject) {
            keyid = ((ASN1TaggedObject) keyid).getObject();
        }
        if (keyid instanceof ASN1OctetString) {
            return ((ASN1OctetString) keyid).getOctets();
        }
        return keyid.getEncoded(ASN1Encoding.DER);
    }

    @SuppressWarnings("unchecked")
    private static boolean formatGeneralName(final GeneralName name, final ByteList out, final boolean slashed) {
        final ASN1Encodable obj = name.getName();
        String val;
        boolean tagged = false;
        switch (name.getTagNo()) {
        case GeneralName.rfc822Name:
            if (!tagged)
                out.append('e').append('m').append('a').append('i').append('l').append(':');
            tagged = true;
        case GeneralName.dNSName:
            if (!tagged)
                out.append('D').append('N').append('S').append(':');
            tagged = true;
        case GeneralName.uniformResourceIdentifier:
            if (!tagged)
                out.append('U').append('R').append('I').append(':');
            val = DERIA5String.getInstance(obj).getString();
            out.append(ByteList.plain(val));
            break;
        case GeneralName.directoryName:
            out.append('D').append('i').append('r').append('N').append('a').append('m').append('e').append(':');
            final X500Name dirName = X500Name.getInstance(obj);
            if (slashed) {
                final RDN[] rdns = dirName.getRDNs();
                final Hashtable defaultSymbols = getDefaultSymbols();
                for (int i = 0; i < rdns.length; i++) {
                    appendRDN(out.append('/'), rdns[i], defaultSymbols);
                }
            } else {
                out.append(ByteList.plain(dirName.toString()));
            }
            break;
        case GeneralName.iPAddress:
            out.append('I').append('P').append(':');
            final byte[] ip = ((ASN1OctetString) name.getName()).getOctets();
            int len = ip.length;
            boolean ip4 = len == 4;
            for (int i = 0; i < ip.length; i++) {
                out.append(ConvertBytes.intToCharBytes(((int) ip[i]) & 0xff));
                if (i != len - 1) {
                    if (ip4)
                        out.append('.');
                    else
                        out.append(':').append(':');
                }
            }
            break;
        case GeneralName.otherName:
            out.append('o').append('t').append('h').append('e').append('r').append('N').append('a').append('m')
                    .append('e').append(':');
            out.append(ByteList.plain(obj.toString()));
            return true;
        //tagged = true;
        case GeneralName.registeredID:
            out.append('R').append('I').append('D').append(':');
            //tagged = true;
        default:
            out.append(ByteList.plain(obj.toString()));
        }
        return false;
    }

    // re-invented IETFUtils.appendRDN related pieces :

    public static ByteList appendRDN(final ByteList out, final RDN rdn,
            final Map<ASN1ObjectIdentifier, String> oidSymbols) {

        if (rdn.isMultiValued()) {
            AttributeTypeAndValue[] atv = rdn.getTypesAndValues();

            boolean firstAtv = true;
            for (int j = 0; j != atv.length; j++) {
                if (firstAtv)
                    firstAtv = false;
                else
                    out.append('+');

                appendTypeAndValue(out, atv[j], oidSymbols);
            }
            return out;
        }
        return appendTypeAndValue(out, rdn.getFirst(), oidSymbols);
    }

    private static ByteList appendTypeAndValue(final ByteList out, final AttributeTypeAndValue typeAndValue,
            final Map<ASN1ObjectIdentifier, String> oidSymbols) {
        ASN1ObjectIdentifier type = typeAndValue.getType();
        final String sym = oidSymbols.get(type);

        if (sym != null) {
            out.append(ByteList.plain(sym));
        } else {
            out.append(ByteList.plain(type.getId()));
        }

        out.append('=');
        valueToString(typeAndValue.getValue(), out);
        return out;
    }

    private static void valueToString(final ASN1Encodable value, final ByteList out) {
        final int size = out.getRealSize();

        if (value instanceof ASN1String && !(value instanceof DERUniversalString)) {
            final String str = ((ASN1String) value).getString();
            if (str.length() > 0 && str.charAt(0) == '#') {
                out.append('\\');
            }
            out.append(ByteList.plain(str));
        } else {
            try {
                byte[] val = value.toASN1Primitive().getEncoded(ASN1Encoding.DER);
                out.append('#').append(Hex.encode(val));
            } catch (IOException e) {
                throw new IllegalArgumentException("Other value has no encoded form", e);
            }
        }

        int index = size; // 0
        int end = out.getRealSize() - size;

        if (end >= 2 && out.charAt(index) == '\\' && out.charAt(index + 1) == '#') {
            index += 2;
        }

        while (index <= end) {
            final char c = out.charAt(index);
            if (c == ',' || c == '"' || c == '\\' || c == '+' || c == '=' || c == '<' || c == '>' || c == ';') {
                out.insert(index, '\\');
                index++;
                end++;
            }
            index++;
        }

        if (out.getRealSize() - size > 0) {
            index = size; // 0
            while (out.charAt(index) == ' ') {
                out.insert(index, '\\');
                index += 2;
            }
        }

        index = out.getRealSize() - 1; // length - 1
        while (index >= size && out.charAt(index) == ' ') {
            out.insert(index, '\\');
            index--;
        }
    }

    private static Hashtable getDefaultSymbols() {
        try {
            Field field = BCStyle.class.getDeclaredField("DefaultSymbols");
            field.setAccessible(true);
            return (Hashtable) field.get(null);
        } catch (NoSuchFieldException ex) {
            debug("getDefaultSymbols", ex);
        } catch (SecurityException ex) {
            debug("getDefaultSymbols", ex);
        } catch (IllegalAccessException ex) {
            debug("getDefaultSymbols", ex);
        }
        return new Hashtable();
    }

    @JRubyMethod(name = "value=")
    public IRubyObject set_value(final ThreadContext context, IRubyObject arg) {
        if (arg instanceof RubyString) {
            this.value = arg;
            return arg;
        }
        throw context.runtime.newTypeError(arg, context.runtime.getString());
    }

    @JRubyMethod(name = "critical?")
    public IRubyObject critical_p(final ThreadContext context) {
        return context.runtime.newBoolean(isRealCritical());
    }

    @JRubyMethod(name = "critical=")
    public IRubyObject set_critical(final ThreadContext context, IRubyObject arg) {
        setRealCritical(arg.isTrue());
        return arg;
    }

    @JRubyMethod
    public IRubyObject to_der() {
        try {
            final byte[] enc = toASN1Sequence().getEncoded(ASN1Encoding.DER);
            return StringHelper.newString(getRuntime(), enc);
        } catch (IOException e) {
            throw newExtensionError(getRuntime(), e);
        }
    }

    ASN1Sequence toASN1Sequence() throws IOException {
        final ASN1EncodableVector vec = new ASN1EncodableVector();
        vec.add(getRealObjectID());
        if (critical)
            vec.add(DERBoolean.TRUE);
        vec.add(new DEROctetString(getRealValueEncoded()));
        return new DLSequence(vec);
    }

    // [ self.oid, self.value, self.critical? ]
    @JRubyMethod
    public RubyArray to_a(final ThreadContext context) {
        RubyArray array = RubyArray.newArray(context.runtime, 3);
        array.append(oid(context));
        array.append(value(context));
        array.append(critical_p(context));
        return array;
    }

    // {"oid"=>self.oid,"value"=>self.value,"critical"=>self.critical?
    @JRubyMethod
    public RubyHash to_h(final ThreadContext context) {
        final Ruby runtime = context.runtime;
        RubyHash hash = RubyHash.newHash(runtime);
        hash.op_aset(context, newStringFrozen(runtime, "oid"), oid(context));
        hash.op_aset(context, newStringFrozen(runtime, "value"), value(context));
        hash.op_aset(context, newStringFrozen(runtime, "critical"), critical_p(context));
        return hash;
    }

    // "oid = critical, value"
    @JRubyMethod
    public RubyString to_s(final ThreadContext context) {
        final Ruby runtime = context.runtime;
        final RubyString str = RubyString.newString(runtime, oidSym(runtime));
        str.getByteList().append(' ').append('=').append(' ');
        if (isRealCritical())
            str.getByteList().append(critical__);
        // self.value.gsub(/\n/, ", ")
        final RubyString value = value(context);
        value.callMethod(context, "gsub!",
                new IRubyObject[] { RubyString.newStringShared(runtime, StringHelper.NEW_LINE),
                        RubyString.newStringShared(runtime, StringHelper.COMMA_SPACE) });
        str.getByteList().append(value.getByteList());
        return str;
    }

    @Override
    public X509Extension clone() {
        try {
            return (X509Extension) super.clone();
        } catch (CloneNotSupportedException ex) {
            throw new RuntimeException(ex);
        }
    }

    @Override
    @SuppressWarnings(value = "unchecked")
    @JRubyMethod
    public IRubyObject inspect() {
        return ObjectSupport.inspect(this);
    }

    static RaiseException newExtensionError(Ruby runtime, Exception e) {
        return Utils.newError(runtime, _X509(runtime).getClass("ExtensionError"), e);
    }

    static RaiseException newExtensionError(Ruby runtime, String message) {
        return Utils.newError(runtime, _X509(runtime).getClass("ExtensionError"), message);
    }

    // our custom "internal" HEX helpers :

    private static boolean isHex(final char c) {
        return ('0' <= c && c <= '9') || ('A' <= c && c <= 'F') || ('a' <= c && c <= 'f');
    }

    static boolean isHex(final String str) {
        for (int i = 0; i < str.length(); i++) {
            if (!isHex(str.charAt(i)))
                return false;
        }
        return true;
    }

    static int upHex(final char c) {
        switch (c) {
        case '0':
            return '0';
        case '1':
            return '1';
        case '2':
            return '2';
        case '3':
            return '3';
        case '4':
            return '4';
        case '5':
            return '5';
        case '6':
            return '6';
        case '7':
            return '7';
        case '8':
            return '8';
        case '9':
            return '9';
        case 'A':
        case 'a':
            return 'A';
        case 'B':
        case 'b':
            return 'B';
        case 'C':
        case 'c':
            return 'C';
        case 'D':
        case 'd':
            return 'D';
        case 'E':
        case 'e':
            return 'E';
        case 'F':
        case 'f':
            return 'F';
        }
        return -1;
    }

    private static ByteList hexBytes(final byte[] data, final int off) {
        final int len = data.length - off;
        return hexBytes(data, off, len, new ByteList(len * 3));
    }

    private static ByteList hexBytes(final byte[] data, final ByteList out) {
        return hexBytes(data, 0, data.length, out);
    }

    //@SuppressWarnings("deprecation")
    //private static ByteList hexBytes(final ByteList data, final ByteList out) {
    //    return hexBytes(data.bytes, data.begin, data.realSize, out);
    //}

    private static ByteList hexBytes(final byte[] data, final int off, final int len, final ByteList out) {
        boolean notFist = false;
        out.ensure(len * 3 - 1);
        for (int i = off; i < (off + len); i++) {
            if (notFist)
                out.append(':');
            final byte b = data[i];
            out.append(HEX[(b >> 4) & 0xF]);
            out.append(HEX[b & 0xF]);
            notFist = true;
        }
        return out;
    }

    private static final char[] HEX = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E',
            'F' };

}