From e92f837698c21c9ac0ee09be9447244cb9685b0b Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 11 Jul 2024 15:02:03 +0100 Subject: [PATCH] ASN1: #to_der in pure ruby --- ext/openssl/ossl_asn1.c | 11 +- lib/openssl/asn1.rb | 426 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 429 insertions(+), 8 deletions(-) diff --git a/ext/openssl/ossl_asn1.c b/ext/openssl/ossl_asn1.c index 502be101..aa0cbab6 100644 --- a/ext/openssl/ossl_asn1.c +++ b/ext/openssl/ossl_asn1.c @@ -1523,7 +1523,7 @@ Init_ossl_asn1(void) * puts int2.value # => 1 */ cASN1Data = rb_define_class_under(mASN1, "ASN1Data", rb_cObject); - rb_define_method(cASN1Data, "to_der", ossl_asn1data_to_der, 0); + // rb_define_method(cASN1Data, "to_der", ossl_asn1data_to_der, 0); /* Document-class: OpenSSL::ASN1::Primitive * @@ -1590,7 +1590,7 @@ Init_ossl_asn1(void) * prim_zero_tagged_explicit = .new(value, 0, :EXPLICIT) */ cASN1Primitive = rb_define_class_under(mASN1, "Primitive", cASN1Data); - rb_define_method(cASN1Primitive, "to_der", ossl_asn1prim_to_der, 0); + // rb_define_method(cASN1Primitive, "to_der", ossl_asn1prim_to_der, 0); /* Document-class: OpenSSL::ASN1::Constructive * @@ -1620,7 +1620,7 @@ Init_ossl_asn1(void) * set = OpenSSL::ASN1::Set.new( [ int, str ] ) */ cASN1Constructive = rb_define_class_under(mASN1,"Constructive", cASN1Data); - rb_define_method(cASN1Constructive, "to_der", ossl_asn1cons_to_der, 0); + // rb_define_method(cASN1Constructive, "to_der", ossl_asn1cons_to_der, 0); #define OSSL_ASN1_DEFINE_CLASS(name, super) \ do{\ @@ -1670,7 +1670,10 @@ do{\ rb_define_alias(cASN1ObjectId, "long_name", "ln"); rb_define_method(cASN1ObjectId, "==", ossl_asn1obj_eq, 1); - rb_define_method(cASN1EndOfContent, "to_der", ossl_asn1eoc_to_der, 0); + // rb_define_method(cASN1EndOfContent, "to_der", ossl_asn1eoc_to_der, 0); + rb_define_method(cASN1ObjectId, "to_der", ossl_asn1prim_to_der, 0); + rb_define_method(cASN1UTCTime, "to_der", ossl_asn1prim_to_der, 0); + rb_define_method(cASN1GeneralizedTime, "to_der", ossl_asn1prim_to_der, 0); class_tag_map = rb_hash_new(); rb_hash_aset(class_tag_map, cASN1EndOfContent, INT2NUM(V_ASN1_EOC)); diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index 89fa28e1..90734484 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -10,6 +10,47 @@ module OpenSSL module ASN1 + INT_MAX = begin + n_bytes = [42].pack('i').size + n_bits = n_bytes * 16 + 2 ** (n_bits - 2) - 1 + end + + V_ASN1_UNIVERSAL= 0x00 + V_ASN1_APPLICATION = 0x40 + V_ASN1_CONTEXT_SPECIFIC = 0x80 + V_ASN1_PRIVATE = 0xc0 + V_ASN1_CONSTRUCTED = 0x20 + V_ASN1_PRIMITIVE_TAG = 0x1f + + V_ASN1_INTEGER = 2 + V_ASN1_BIT_STRING = 3 + V_ASN1_OCTET_STRING = 4 + V_ASN1_NULL = 5 + V_ASN1_OBJECT = 6 + V_ASN1_OBJECT_DESCRIPTOR = 7 + V_ASN1_EXTERNAL = 8 + V_ASN1_REAL = 9 + V_ASN1_ENUMERATED = 10 + V_ASN1_UTF8STRING = 12 + V_ASN1_SEQUENCE = 16 + V_ASN1_SET = 17 + V_ASN1_NUMERICSTRING = 18 + V_ASN1_PRINTABLESTRING = 19 + V_ASN1_T61STRING = 20 + V_ASN1_TELETEXSTRING = 20 + V_ASN1_VIDEOTEXSTRING = 21 + V_ASN1_IA5STRING = 22 + V_ASN1_UTCTIME = 23 + V_ASN1_GENERALIZEDTIME = 24 + V_ASN1_GRAPHICSTRING = 25 + V_ASN1_ISO64STRING = 26 + V_ASN1_VISIBLESTRING = 26 + V_ASN1_GENERALSTRING = 27 + V_ASN1_UNIVERSALSTRING = 28 + V_ASN1_BMPSTRING = 30 + + class ASN1Data # # Carries the value of a ASN.1 type. @@ -71,6 +112,79 @@ def initialize(value, tag, tag_class) @tag_class = tag_class @indefinite_length = false end + + def to_der + if @value.is_a?(Array) + cons_to_der + elsif @indefinite_length + raise ASN1Error, "indefinite length form cannot be used " \ + "with primitive encoding" + else + to_der_internal(der_value) + end + end + + def der_value + raise TypeError, "no implicit conversion of #{self.class} into String" unless @value.respond_to?(:to_str) + + String(@value) + end + + private + + def cons_to_der + ary = @value.to_a + str = "".b + + @value.each_with_index do |item, idx| + if @indefinite_length && item.is_a?(EndOfContent) + if idx != ary.size - 1 + raise ASN1Error, "illegal EOC octets in value" + end + + break + end + + item = item.to_der if item.respond_to?(:to_der) + + str << item + end + + to_der_internal(str, true) + end + + def prim_to_der + to_der_internal(der_value) + end + + def to_der_internal(body, constructed = false) + default_tag = ASN1.take_default_tag(self.class) + body_len = body.size + + if @tagging == :EXPLICIT + raise ASN1Error, "explicit tagging of unknown tag" unless default_tag + + inner_len = ASN1.object_size(constructed && @indefinite_length, body_len, default_tag) + + # Put explicit tag + str = ASN1.put_object(true, @indefinite_length, inner_len, @tag, @tag_class) << + # Append inner object + ASN1.put_object(constructed, @indefinite_length, body_len, default_tag, :UNIVERSAL) + + str << body + if @indefinite_length + str << "\x00\x00\x00\x00" + end + else + str = ASN1.put_object(constructed, @indefinite_length, body_len, @tag, @tag_class) + str << body + if @indefinite_length + str << "\x00\x00" + end + end + + str + end end module TaggedASN1Data @@ -127,6 +241,11 @@ class Primitive < ASN1Data undef_method :indefinite_length= undef_method :infinite_length= + + + def to_der + prim_to_der + end end class Constructive < ASN1Data @@ -150,13 +269,43 @@ def each(&blk) self end + + def to_der + cons_to_der + end + end + + class Null < Primitive + def to_der + raise ASN1Error, "nil expected" unless @value.nil? + + "\x05\x00" + end + end + + class Boolean < Primitive + def der_value + raise TypeError, "Can't convert nil into Boolean" if @value.nil? + + @value ? "\xff".b : "\x00".b + end + end + + class Integer < Primitive + def der_value + ASN1.put_integer(@value) + end end - class Boolean < Primitive ; end - class Integer < Primitive ; end - class Enumerated < Primitive ; end + class Enumerated < Primitive + def der_value + ASN1.put_integer(@value) + end + end class BitString < Primitive + ASN1_STRING_FLAG_BITS_LEFT = 0x08 + attr_accessor :unused_bits def initialize(*) @@ -164,16 +313,273 @@ def initialize(*) @unused_bits = 0 end + + def der_value + if @unused_bits < 0 || @unused_bits > 7 + raise ASN1Error, "unused_bits for a bitstring value must be in " \ + "the range 0 to 7" + end + + flags = ASN1_STRING_FLAG_BITS_LEFT | @unused_bits + bits = 0 + + if @value.size > 0 + if flags & ASN1_STRING_FLAG_BITS_LEFT + bits = flags & 0x07 + else + j = @value[-1].ord + case + when j & 0x01 + bits = 0 + when j & 0x02 + bits = 1 + when j & 0x04 + bits = 2 + when j & 0x08 + bits = 3 + when j & 0x10 + bits = 4 + when j & 0x20 + bits = 5 + when j & 0x40 + bits = 6 + when j & 0x80 + bits = 7 + else + bits = 0 + end + end + end + + bits.chr << String(@value) + end + end + + class OctetString < Primitive + def der_value() = String(value) + end + + class UTF8String < Primitive + def der_value() = String(value) + end + + class NumericString < Primitive + def der_value() = String(value) + end + + class PrintableString < Primitive + def der_value() = String(value) + end + + class T61String < Primitive + def der_value() = String(value) + end + + class VideotexString < Primitive + def der_value() = String(value) + end + + class IA5String < Primitive + def der_value() = String(value) + end + + class GraphicString < Primitive + def der_value() = String(value) + end + + class ISO64String < Primitive + def der_value() = String(value) + end + + class GeneralString < Primitive + def der_value() = String(value) + end + + class UniversalString < Primitive + def der_value() = String(value) + end + + class BMPString < Primitive + def der_value() = String(value) + end + + class ObjectId < Primitive + # def der_value + # value = oid.split(".").map(&:to_i) + + # return (40 * value[0]).chr if value.length == 1 + + # data = "".b + # data << (40 * value[0] + value[1]).chr + + # rest = data[2..-1] + # return data unless rest + + # rest.each do |v| + # if v < 0x80 + # data << v.chr + # else + # octets = "".b + # n = v + # begin + # octets.prepend((n & 0x7f | 0x80).chr) + # n = n >> 7 + # end until n == 0 + # octets[-1] = (octets[-1].ord & 0x7f).chr + # data << octets + # end + # end + + # data + # end + end + + class UTCTime < Primitive + end + + class GeneralizedTime < Primitive end class EndOfContent < ASN1Data def initialize super("", 0, :UNIVERSAL) end + + def to_der + "\x00\x00".b + end + end + + class Set < Constructive + end + + class Sequence < Constructive + + end + + module_function + + # ruby port of ASN1_object_size + def object_size(indefinite_length, length, tag) + ret = 1 + + return -1 if length < 0 + + if tag >= 31 + while tag > 0 + tag >>= 7 + ret += 1 + end + end + if indefinite_length + ret += 3 + else + ret += 1 + if length > 127 + tmplen = length + while tmplen > 0 + tmplen >>= 8 + ret+=1 + end + end + end + + return -1 if (ret >= INT_MAX - length) + + + ret + length + end + + # ruby port of openssl ASN1_put_object + def put_object(constructed, indefinite_length, length, tag, tag_class) + str = "".b + xclass = take_asn1_tag_class(tag_class) + + i = constructed ? V_ASN1_CONSTRUCTED : 0 + i |= (xclass & V_ASN1_PRIVATE) + + if tag < 31 + str << (i | (tag & V_ASN1_PRIMITIVE_TAG)).chr + + else + str << (i | V_ASN1_PRIMITIVE_TAG).chr + + i = 0 + ttag = tag + + while ttag > 0 + i += 1 + ttag >>= 7 + end + + ttag = i + + while i > 0 + i -= 1 + tag_str = tag & 0x7f + if (i != (ttag - 1)) + tag_str |= 0x80 + end + str.insert(1, tag_str.chr) + tag >>= 7 + end + end + + if constructed && indefinite_length + str << 0x80.chr + else + str << put_length(length) + end + str + end + + + def put_length(length) + raise ASN1Error, "invalid length" if length < 0 + + if length < 0x80 + length.chr + else + data = integer_to_octets(length) + (data.size | 0x80).chr << data + end + end + + def put_integer(value) + raise TypeError, "Can't convert nil into Boolean" if value.nil? + + value = value.to_i + + if value > 0 && value < 0x80 + data = value.chr + else + data = ASN1.integer_to_octets(value) + if value > 0 && data[0].ord > 0x7f + data = "\000".b << data + elsif value < 0 && data[0].ord < 0x80 + data = "\377".b << data + end + end + + data + end + + def integer_to_octets(i) + if i >= 0 + done = 0 + else + done = -1 + end + octets = "".b + begin + octets = (i & 0xff).chr << octets + i = i >> 8 + end until i == done + octets end # :nodoc: - def self.take_default_tag(klass) + def take_default_tag(klass) tag = CLASS_TAG_MAP[klass] return tag if tag @@ -184,5 +590,17 @@ def self.take_default_tag(klass) take_default_tag(sklass) end + + # from ossl_asn1.c : ossl_asn1_tag_class + def take_asn1_tag_class(tag_class) + case tag_class + when :UNIVERSAL, nil then V_ASN1_UNIVERSAL + when :APPLICATION then V_ASN1_APPLICATION + when :CONTEXT_SPECIFIC then V_ASN1_CONTEXT_SPECIFIC + when :PRIVATE then V_ASN1_PRIVATE + else + raise ASN1Error, "invalid tag class" + end + end end end