From 6799df6c3b001fc96338a1fad873a9708d63887f Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 11 Jul 2024 15:02:03 +0100 Subject: [PATCH 01/11] ASN1: #to_der in pure ruby --- ext/openssl/ossl_asn1.c | 389 +--------------------------------------- lib/openssl/asn1.rb | 293 +++++++++++++++++++++++++++++- 2 files changed, 293 insertions(+), 389 deletions(-) diff --git a/ext/openssl/ossl_asn1.c b/ext/openssl/ossl_asn1.c index 9999664b8..0369abf51 100644 --- a/ext/openssl/ossl_asn1.c +++ b/ext/openssl/ossl_asn1.c @@ -185,68 +185,6 @@ static VALUE sym_IMPLICIT, sym_EXPLICIT; static VALUE sym_UNIVERSAL, sym_APPLICATION, sym_CONTEXT_SPECIFIC, sym_PRIVATE; static ID sivVALUE, sivTAG, sivTAG_CLASS, sivTAGGING, sivINDEFINITE_LENGTH, sivUNUSED_BITS; -/* - * Ruby to ASN1 converters - */ -static ASN1_BOOLEAN -obj_to_asn1bool(VALUE obj) -{ - if (NIL_P(obj)) - ossl_raise(rb_eTypeError, "Can't convert nil into Boolean"); - - return RTEST(obj) ? 0xff : 0x0; -} - -static ASN1_INTEGER* -obj_to_asn1int(VALUE obj) -{ - return num_to_asn1integer(obj, NULL); -} - -static ASN1_BIT_STRING* -obj_to_asn1bstr(VALUE obj, long unused_bits) -{ - ASN1_BIT_STRING *bstr; - - if (unused_bits < 0 || unused_bits > 7) - ossl_raise(eASN1Error, "unused_bits for a bitstring value must be in "\ - "the range 0 to 7"); - StringValue(obj); - if(!(bstr = ASN1_BIT_STRING_new())) - ossl_raise(eASN1Error, NULL); - ASN1_BIT_STRING_set(bstr, (unsigned char *)RSTRING_PTR(obj), RSTRING_LENINT(obj)); - bstr->flags &= ~(ASN1_STRING_FLAG_BITS_LEFT|0x07); /* clear */ - bstr->flags |= ASN1_STRING_FLAG_BITS_LEFT | unused_bits; - - return bstr; -} - -static ASN1_STRING* -obj_to_asn1str(VALUE obj) -{ - ASN1_STRING *str; - - StringValue(obj); - if(!(str = ASN1_STRING_new())) - ossl_raise(eASN1Error, NULL); - ASN1_STRING_set(str, RSTRING_PTR(obj), RSTRING_LENINT(obj)); - - return str; -} - -static ASN1_NULL* -obj_to_asn1null(VALUE obj) -{ - ASN1_NULL *null; - - if(!NIL_P(obj)) - ossl_raise(eASN1Error, "nil expected"); - if(!(null = ASN1_NULL_new())) - ossl_raise(eASN1Error, NULL); - - return null; -} - static ASN1_OBJECT* obj_to_asn1obj(VALUE obj) { @@ -260,50 +198,6 @@ obj_to_asn1obj(VALUE obj) return a1obj; } -static ASN1_UTCTIME * -obj_to_asn1utime(VALUE time) -{ - time_t sec; - ASN1_UTCTIME *t; - - int off_days; - - ossl_time_split(time, &sec, &off_days); - if (!(t = ASN1_UTCTIME_adj(NULL, sec, off_days, 0))) - ossl_raise(eASN1Error, NULL); - - return t; -} - -static ASN1_GENERALIZEDTIME * -obj_to_asn1gtime(VALUE time) -{ - time_t sec; - ASN1_GENERALIZEDTIME *t; - - int off_days; - - ossl_time_split(time, &sec, &off_days); - if (!(t = ASN1_GENERALIZEDTIME_adj(NULL, sec, off_days, 0))) - ossl_raise(eASN1Error, NULL); - - return t; -} - -static ASN1_STRING* -obj_to_asn1derstr(VALUE obj) -{ - ASN1_STRING *a1str; - VALUE str; - - str = ossl_to_der(obj); - if(!(a1str = ASN1_STRING_new())) - ossl_raise(eASN1Error, NULL); - ASN1_STRING_set(a1str, RSTRING_PTR(str), RSTRING_LENINT(str)); - - return a1str; -} - /* * DER to Ruby converters */ @@ -495,100 +389,6 @@ enum {ossl_asn1_info_size = (sizeof(ossl_asn1_info)/sizeof(ossl_asn1_info[0]))}; static VALUE class_tag_map; -static int ossl_asn1_default_tag(VALUE obj); - -static ASN1_TYPE * -ossl_asn1_get_asn1type(VALUE obj) -{ - ASN1_TYPE *ret; - VALUE value, rflag; - void *ptr; - typedef void free_func_type(void *); - free_func_type *free_func; - int tag; - - tag = ossl_asn1_default_tag(obj); - value = ossl_asn1_get_value(obj); - switch(tag){ - case V_ASN1_BOOLEAN: - ptr = (void*)(VALUE)obj_to_asn1bool(value); - free_func = NULL; - break; - case V_ASN1_INTEGER: /* FALLTHROUGH */ - case V_ASN1_ENUMERATED: - ptr = obj_to_asn1int(value); - free_func = (free_func_type *)ASN1_INTEGER_free; - break; - case V_ASN1_BIT_STRING: - rflag = rb_attr_get(obj, sivUNUSED_BITS); - ptr = obj_to_asn1bstr(value, NUM2INT(rflag)); - free_func = (free_func_type *)ASN1_BIT_STRING_free; - break; - case V_ASN1_NULL: - ptr = obj_to_asn1null(value); - free_func = (free_func_type *)ASN1_NULL_free; - break; - case V_ASN1_OCTET_STRING: /* FALLTHROUGH */ - case V_ASN1_UTF8STRING: /* FALLTHROUGH */ - case V_ASN1_NUMERICSTRING: /* FALLTHROUGH */ - case V_ASN1_PRINTABLESTRING: /* FALLTHROUGH */ - case V_ASN1_T61STRING: /* FALLTHROUGH */ - case V_ASN1_VIDEOTEXSTRING: /* FALLTHROUGH */ - case V_ASN1_IA5STRING: /* FALLTHROUGH */ - case V_ASN1_GRAPHICSTRING: /* FALLTHROUGH */ - case V_ASN1_ISO64STRING: /* FALLTHROUGH */ - case V_ASN1_GENERALSTRING: /* FALLTHROUGH */ - case V_ASN1_UNIVERSALSTRING: /* FALLTHROUGH */ - case V_ASN1_BMPSTRING: - ptr = obj_to_asn1str(value); - free_func = (free_func_type *)ASN1_STRING_free; - break; - case V_ASN1_OBJECT: - ptr = obj_to_asn1obj(value); - free_func = (free_func_type *)ASN1_OBJECT_free; - break; - case V_ASN1_UTCTIME: - ptr = obj_to_asn1utime(value); - free_func = (free_func_type *)ASN1_TIME_free; - break; - case V_ASN1_GENERALIZEDTIME: - ptr = obj_to_asn1gtime(value); - free_func = (free_func_type *)ASN1_TIME_free; - break; - case V_ASN1_SET: /* FALLTHROUGH */ - case V_ASN1_SEQUENCE: - ptr = obj_to_asn1derstr(obj); - free_func = (free_func_type *)ASN1_STRING_free; - break; - default: - ossl_raise(eASN1Error, "unsupported ASN.1 type"); - } - if(!(ret = OPENSSL_malloc(sizeof(ASN1_TYPE)))){ - if(free_func) free_func(ptr); - ossl_raise(eASN1Error, "ASN1_TYPE alloc failure"); - } - memset(ret, 0, sizeof(ASN1_TYPE)); - ASN1_TYPE_set(ret, tag, ptr); - - return ret; -} - -static int -ossl_asn1_default_tag(VALUE obj) -{ - VALUE tmp_class, tag; - - tmp_class = CLASS_OF(obj); - while (!NIL_P(tmp_class)) { - tag = rb_hash_lookup(class_tag_map, tmp_class); - if (tag != Qnil) - return NUM2INT(tag); - tmp_class = rb_class_superclass(tmp_class); - } - - return -1; -} - static int ossl_asn1_tag(VALUE obj) { @@ -601,24 +401,6 @@ ossl_asn1_tag(VALUE obj) return NUM2INT(tag); } -static int -ossl_asn1_tag_class(VALUE obj) -{ - VALUE s; - - s = ossl_asn1_get_tag_class(obj); - if (NIL_P(s) || s == sym_UNIVERSAL) - return V_ASN1_UNIVERSAL; - else if (s == sym_APPLICATION) - return V_ASN1_APPLICATION; - else if (s == sym_CONTEXT_SPECIFIC) - return V_ASN1_CONTEXT_SPECIFIC; - else if (s == sym_PRIVATE) - return V_ASN1_PRIVATE; - else - ossl_raise(eASN1Error, "invalid tag class"); -} - static VALUE ossl_asn1_class2sym(int tc) { @@ -632,79 +414,6 @@ ossl_asn1_class2sym(int tc) return sym_UNIVERSAL; } -static VALUE -to_der_internal(VALUE self, int constructed, int indef_len, VALUE body) -{ - int encoding = constructed ? indef_len ? 2 : 1 : 0; - int tag_class = ossl_asn1_tag_class(self); - int tag_number = ossl_asn1_tag(self); - int default_tag_number = ossl_asn1_default_tag(self); - int body_length, total_length; - VALUE str; - unsigned char *p; - - body_length = RSTRING_LENINT(body); - if (ossl_asn1_get_tagging(self) == sym_EXPLICIT) { - int inner_length, e_encoding = indef_len ? 2 : 1; - - if (default_tag_number == -1) - ossl_raise(eASN1Error, "explicit tagging of unknown tag"); - - inner_length = ASN1_object_size(encoding, body_length, default_tag_number); - total_length = ASN1_object_size(e_encoding, inner_length, tag_number); - str = rb_str_new(NULL, total_length); - p = (unsigned char *)RSTRING_PTR(str); - /* Put explicit tag */ - ASN1_put_object(&p, e_encoding, inner_length, tag_number, tag_class); - /* Append inner object */ - ASN1_put_object(&p, encoding, body_length, default_tag_number, V_ASN1_UNIVERSAL); - memcpy(p, RSTRING_PTR(body), body_length); - p += body_length; - if (indef_len) { - ASN1_put_eoc(&p); /* For inner object */ - ASN1_put_eoc(&p); /* For wrapper object */ - } - } - else { - total_length = ASN1_object_size(encoding, body_length, tag_number); - str = rb_str_new(NULL, total_length); - p = (unsigned char *)RSTRING_PTR(str); - ASN1_put_object(&p, encoding, body_length, tag_number, tag_class); - memcpy(p, RSTRING_PTR(body), body_length); - p += body_length; - if (indef_len) - ASN1_put_eoc(&p); - } - assert(p - (unsigned char *)RSTRING_PTR(str) == total_length); - return str; -} - -static VALUE ossl_asn1prim_to_der(VALUE); -static VALUE ossl_asn1cons_to_der(VALUE); -/* - * call-seq: - * asn1.to_der => DER-encoded String - * - * Encodes this ASN1Data into a DER-encoded String value. The result is - * DER-encoded except for the possibility of indefinite length forms. - * Indefinite length forms are not allowed in strict DER, so strictly speaking - * the result of such an encoding would be a BER-encoding. - */ -static VALUE -ossl_asn1data_to_der(VALUE self) -{ - VALUE value = ossl_asn1_get_value(self); - - if (rb_obj_is_kind_of(value, rb_cArray)) - return ossl_asn1cons_to_der(self); - else { - if (RTEST(ossl_asn1_get_indefinite_length(self))) - ossl_raise(eASN1Error, "indefinite length form cannot be used " \ - "with primitive encoding"); - return ossl_asn1prim_to_der(self); - } -} - static VALUE int_ossl_asn1_decode0_prim(unsigned char **pp, long length, long hlen, int tag, VALUE tc, long *num_read) @@ -1014,96 +723,6 @@ ossl_asn1_decode_all(VALUE self, VALUE obj) return ary; } -static VALUE -ossl_asn1eoc_to_der(VALUE self) -{ - return rb_str_new("\0\0", 2); -} - -/* - * call-seq: - * asn1.to_der => DER-encoded String - * - * See ASN1Data#to_der for details. - */ -static VALUE -ossl_asn1prim_to_der(VALUE self) -{ - ASN1_TYPE *asn1; - long alllen, bodylen; - unsigned char *p0, *p1; - int j, tag, tc, state; - VALUE str; - - if (ossl_asn1_default_tag(self) == -1) { - str = ossl_asn1_get_value(self); - return to_der_internal(self, 0, 0, StringValue(str)); - } - - asn1 = ossl_asn1_get_asn1type(self); - alllen = i2d_ASN1_TYPE(asn1, NULL); - if (alllen < 0) { - ASN1_TYPE_free(asn1); - ossl_raise(eASN1Error, "i2d_ASN1_TYPE"); - } - str = ossl_str_new(NULL, alllen, &state); - if (state) { - ASN1_TYPE_free(asn1); - rb_jump_tag(state); - } - p0 = p1 = (unsigned char *)RSTRING_PTR(str); - if (i2d_ASN1_TYPE(asn1, &p0) < 0) { - ASN1_TYPE_free(asn1); - ossl_raise(eASN1Error, "i2d_ASN1_TYPE"); - } - ASN1_TYPE_free(asn1); - ossl_str_adjust(str, p0); - - /* Strip header since to_der_internal() wants only the payload */ - j = ASN1_get_object((const unsigned char **)&p1, &bodylen, &tag, &tc, alllen); - if (j & 0x80) - ossl_raise(eASN1Error, "ASN1_get_object"); /* should not happen */ - - return to_der_internal(self, 0, 0, rb_str_drop_bytes(str, alllen - bodylen)); -} - -/* - * call-seq: - * asn1.to_der => DER-encoded String - * - * See ASN1Data#to_der for details. - */ -static VALUE -ossl_asn1cons_to_der(VALUE self) -{ - VALUE ary, str; - long i; - int indef_len; - - indef_len = RTEST(ossl_asn1_get_indefinite_length(self)); - ary = rb_convert_type(ossl_asn1_get_value(self), T_ARRAY, "Array", "to_a"); - str = rb_str_new(NULL, 0); - for (i = 0; i < RARRAY_LEN(ary); i++) { - VALUE item = RARRAY_AREF(ary, i); - - if (indef_len && rb_obj_is_kind_of(item, cASN1EndOfContent)) { - if (i != RARRAY_LEN(ary) - 1) - ossl_raise(eASN1Error, "illegal EOC octets in value"); - - /* - * EOC is not really part of the content, but we required to add one - * at the end in the past. - */ - break; - } - - item = ossl_to_der_if_possible(item); - StringValue(item); - rb_str_append(str, item); - } - - return to_der_internal(self, 1, indef_len, str); -} /* * call-seq: @@ -1520,7 +1139,6 @@ 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); /* Document-class: OpenSSL::ASN1::Primitive * @@ -1587,7 +1205,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 * @@ -1617,7 +1235,6 @@ 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); #define OSSL_ASN1_DEFINE_CLASS(name, super) \ do{\ @@ -1667,7 +1284,9 @@ 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(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 89fa28e1f..0c0df0626 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -71,6 +71,89 @@ def initialize(value, tag, tag_class) @tag_class = tag_class @indefinite_length = false end + + # + # :call-seq: + # asn1.to_der => DER-encoded String + # + # Encodes this ASN1Data into a DER-encoded String value. The result is + # DER-encoded except for the possibility of indefinite length forms. + # Indefinite length forms are not allowed in strict DER, so strictly speaking + # the result of such an encoding would be a BER-encoding. + # + 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) + + @value.to_str.b + 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_obj = ASN1.put_object(constructed, @indefinite_length, body_len, default_tag, :UNIVERSAL) + + inner_len = body_len + inner_obj.size + + + # Put explicit tag + str = ASN1.put_object(true, @indefinite_length, inner_len, @tag, @tag_class) << inner_obj + + str << body + if @indefinite_length + str << "\x00\x00\x00\x00".b + end + else + str = ASN1.put_object(constructed, @indefinite_length, body_len, @tag, @tag_class) + str << body + if @indefinite_length + str << "\x00\x00".b + end + end + + str + end end module TaggedASN1Data @@ -127,6 +210,11 @@ class Primitive < ASN1Data undef_method :indefinite_length= undef_method :infinite_length= + + + def to_der + prim_to_der + end end class Constructive < ASN1Data @@ -150,11 +238,37 @@ def each(&blk) self end + + def to_der + cons_to_der + end + end + + class Null < Primitive + def der_value + "".b + end end - class Boolean < Primitive ; end - class Integer < Primitive ; end - class Enumerated < Primitive ; 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 Enumerated < Primitive + def der_value + ASN1.put_integer(@value) + end + end class BitString < Primitive attr_accessor :unused_bits @@ -164,16 +278,175 @@ 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 + + return "\x00".b if @value.empty? + + @unused_bits.chr << super + end + end + + class OctetString < Primitive + end + + class UTF8String < Primitive + end + + class NumericString < Primitive + end + + class PrintableString < Primitive + end + + class T61String < Primitive + end + + class VideotexString < Primitive + end + + class IA5String < Primitive + end + + class GraphicString < Primitive + end + + class ISO64String < Primitive + end + + class GeneralString < Primitive + end + + class UniversalString < Primitive + end + + class BMPString < Primitive + end + + class ObjectId < Primitive + def der_value + value = oid.split(".").map(&:to_i) + + return (40 * value[0]).chr if value.length == 1 + + [value[0] * 40 + value[1], *value[2..]].pack("w*") + end + end + + class UTCTime < Primitive + FORMAT = "%y%m%d%H%M%SZ".b.freeze + + def der_value + value = if @value.is_a?(Time) + @value + else + Time.at(Integer(@value)) + end + + value.utc.strftime(FORMAT) + end + end + + class GeneralizedTime < Primitive + FORMAT = "%Y%m%d%H%M%SZ".b.freeze + def der_value + value = if @value.is_a?(Time) + @value + else + Time.at(Integer(@value)) + end + + value.utc.strftime(FORMAT) + end 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 openssl ASN1_put_object + def put_object(constructed, indefinite_length, length, tag, tag_class) + xclass = take_asn1_tag_class(tag_class) + + i = constructed ? 0x20 : 0 + i |= (xclass & 0xc0) # PRIVATE + + if tag < 31 + str = (i | tag).chr + + else + str = [i | 0x1f, tag].pack("Cw") + end + + if constructed && indefinite_length + str << 0x80.chr + else + str << put_length(length) + end + str + end + + + def put_length(length) + 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 OpenSSL::BN" if value.nil? + + value = value.to_bn + if value >= 0 + data = value.to_s(2) + data.prepend("\x00".b) if data.empty? || data.getbyte(0) >= 0x80 + else + value = (1.to_bn << (value.num_bits + 7) / 8 * 8) + value + data = value.to_s(2) + data.prepend("\xff".b) if data.empty? || data.getbyte(0) < 0x80 + 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 +457,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 0x00 + when :APPLICATION then 0x40 + when :CONTEXT_SPECIFIC then 0x80 + when :PRIVATE then 0xc0 + else + raise ASN1Error, "invalid tag class" + end + end end end From 4a641830a0fe5f6a1bf38cc8dd2fcacf708b77bc Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 3 Jul 2025 11:26:53 +0100 Subject: [PATCH 02/11] bn ops: only instantiate BN if there were no errors --- ext/openssl/ossl_bn.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/openssl/ossl_bn.c b/ext/openssl/ossl_bn.c index 8699ce8ec..95de1332c 100644 --- a/ext/openssl/ossl_bn.c +++ b/ext/openssl/ossl_bn.c @@ -494,7 +494,6 @@ BIGNUM_1c(sqr) BIGNUM *bn1, *bn2 = GetBNPtr(other), *result; \ VALUE obj; \ GetBN(self, bn1); \ - obj = NewBN(rb_obj_class(self)); \ if (!(result = BN_new())) { \ ossl_raise(eBNError, NULL); \ } \ @@ -502,6 +501,7 @@ BIGNUM_1c(sqr) BN_free(result); \ ossl_raise(eBNError, NULL); \ } \ + obj = NewBN(rb_obj_class(self)); \ SetBN(obj, result); \ return obj; \ } From 6e8b91faf3d6d9f2fe6319bd8c04f01caaf98ec8 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 3 Jul 2025 11:27:21 +0100 Subject: [PATCH 03/11] moved BN#to_bn to ruby --- ext/openssl/ossl_bn.c | 7 ------- lib/openssl/bn.rb | 6 +++++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/ext/openssl/ossl_bn.c b/ext/openssl/ossl_bn.c index 95de1332c..bc9ad8f43 100644 --- a/ext/openssl/ossl_bn.c +++ b/ext/openssl/ossl_bn.c @@ -388,12 +388,6 @@ ossl_bn_to_i(VALUE self) return num; } -static VALUE -ossl_bn_to_bn(VALUE self) -{ - return self; -} - static VALUE ossl_bn_coerce(VALUE self, VALUE other) { @@ -1328,7 +1322,6 @@ Init_ossl_bn(void) rb_define_method(cBN, "to_s", ossl_bn_to_s, -1); rb_define_method(cBN, "to_i", ossl_bn_to_i, 0); rb_define_alias(cBN, "to_int", "to_i"); - rb_define_method(cBN, "to_bn", ossl_bn_to_bn, 0); rb_define_method(cBN, "coerce", ossl_bn_coerce, 1); /* diff --git a/lib/openssl/bn.rb b/lib/openssl/bn.rb index e4889a140..11efcbb9c 100644 --- a/lib/openssl/bn.rb +++ b/lib/openssl/bn.rb @@ -23,6 +23,10 @@ def pretty_print(q) q.text to_i.to_s } end + + def to_bn + self + end end # BN end # OpenSSL @@ -35,6 +39,6 @@ class Integer # # See `man bn` for more info. def to_bn - OpenSSL::BN::new(self) + OpenSSL::BN.new(self) end end # Integer From 1db218f800e829f36d276d249d91aae4c86e1d90 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 3 Jul 2025 11:29:26 +0100 Subject: [PATCH 04/11] moved #to_bn call after comparison this allows (when value is an integer) the runtime and JIT to optimize the call, instead of relying on C-extension openssl --- lib/openssl/asn1.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index 0c0df0626..fb0555cb1 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -418,12 +418,12 @@ def put_length(length) def put_integer(value) raise TypeError, "Can't convert nil into OpenSSL::BN" if value.nil? - value = value.to_bn if value >= 0 - data = value.to_s(2) + data = value.to_bn.to_s(2) data.prepend("\x00".b) if data.empty? || data.getbyte(0) >= 0x80 else - value = (1.to_bn << (value.num_bits + 7) / 8 * 8) + value + value = value.to_bn + value += (1 << (value.num_bits + 7) / 8 * 8) data = value.to_s(2) data.prepend("\xff".b) if data.empty? || data.getbyte(0) < 0x80 end From c684719bb7ee8ff66b7ab4c996de9ae965138f60 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 3 Jul 2025 11:31:48 +0100 Subject: [PATCH 05/11] deal with array of integers before packing bytes (instead of string appends) the former is a data structure that the runtime can improve processing of, and avoids realloc'ing the string on each call (as well as packing one byte at a time) --- lib/openssl/asn1.rb | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index fb0555cb1..e5a0a6653 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -411,7 +411,8 @@ def put_length(length) length.chr else data = integer_to_octets(length) - (data.size | 0x80).chr << data + data.unshift(data.size | 0x80) + data.pack("C*") end end @@ -432,16 +433,14 @@ def put_integer(value) end def integer_to_octets(i) - if i >= 0 - done = 0 - else - done = -1 + done = i >= 0 ? 0 : -1 + + octets = [] + + until i == done + octets.unshift(i & 0xff) + i >>= 8 end - octets = "".b - begin - octets = (i & 0xff).chr << octets - i = i >> 8 - end until i == done octets end From df728915cd73e6a0b43c6540414c6ffddf09b68c Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 3 Jul 2025 14:01:13 +0100 Subject: [PATCH 06/11] setting encoding of literals in asn1 file to binary by default --- lib/openssl/asn1.rb | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index e5a0a6653..8ed45e343 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -1,3 +1,4 @@ +# coding: binary # frozen_string_literal: true #-- # @@ -102,7 +103,10 @@ def der_value def cons_to_der ary = @value.to_a - str = "".b + + return to_der_internal(nil, true) if ary.empty? + + str = +"" @value.each_with_index do |item, idx| if @indefinite_length && item.is_a?(EndOfContent) @@ -142,13 +146,13 @@ def to_der_internal(body, constructed = false) str << body if @indefinite_length - str << "\x00\x00\x00\x00".b + 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".b + str << "\x00\x00" end end @@ -246,7 +250,7 @@ def to_der class Null < Primitive def der_value - "".b + "" end end @@ -254,7 +258,7 @@ class Boolean < Primitive def der_value raise TypeError, "Can't convert nil into Boolean" if @value.nil? - @value ? "\xff".b : "\x00".b + @value ? "\xff" : "\x00" end end @@ -285,7 +289,7 @@ def der_value "the range 0 to 7" end - return "\x00".b if @value.empty? + return "\x00" if @value.empty? @unused_bits.chr << super end @@ -338,7 +342,7 @@ def der_value end class UTCTime < Primitive - FORMAT = "%y%m%d%H%M%SZ".b.freeze + FORMAT = "%y%m%d%H%M%SZ".freeze def der_value value = if @value.is_a?(Time) @@ -352,7 +356,7 @@ def der_value end class GeneralizedTime < Primitive - FORMAT = "%Y%m%d%H%M%SZ".b.freeze + FORMAT = "%Y%m%d%H%M%SZ".freeze def der_value value = if @value.is_a?(Time) @value @@ -370,7 +374,7 @@ def initialize end def to_der - "\x00\x00".b + "\x00\x00" end end @@ -398,7 +402,7 @@ def put_object(constructed, indefinite_length, length, tag, tag_class) end if constructed && indefinite_length - str << 0x80.chr + str << "\x80" else str << put_length(length) end @@ -421,12 +425,12 @@ def put_integer(value) if value >= 0 data = value.to_bn.to_s(2) - data.prepend("\x00".b) if data.empty? || data.getbyte(0) >= 0x80 + data.prepend("\x00") if data.empty? || data.getbyte(0) >= 0x80 else value = value.to_bn value += (1 << (value.num_bits + 7) / 8 * 8) data = value.to_s(2) - data.prepend("\xff".b) if data.empty? || data.getbyte(0) < 0x80 + data.prepend("\xff") if data.empty? || data.getbyte(0) < 0x80 end data From a53c21f74c3c0526f6523e96aee63de5a8044573 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 3 Jul 2025 14:02:31 +0100 Subject: [PATCH 07/11] improve encoding of oid in order to avoid costly intermediate array creation --- lib/openssl/asn1.rb | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index 8ed45e343..f8be6f516 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -333,11 +333,27 @@ class BMPString < Primitive class ObjectId < Primitive def der_value - value = oid.split(".").map(&:to_i) + value = oid - return (40 * value[0]).chr if value.length == 1 + dot_index = value.index(".") - [value[0] * 40 + value[1], *value[2..]].pack("w*") + if dot_index == value.size - 1 + return (value.to_i * 40).chr + else + codes = [value.byteslice(0..dot_index-1).to_i * 40] + end + + add_to_top = false + value.byteslice(dot_index+1..-1).split(".") do |sub| + if add_to_top + codes << sub.to_i + else + codes[0] += sub.to_i + add_to_top = true + end + end + + codes.pack("w*") end end From 52d7305266130a45644e95990840d3ca015ed289 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 3 Jul 2025 14:03:55 +0100 Subject: [PATCH 08/11] improve der internal method so that it deals with objects with no body which means that it'll be nil; this improves encoding of asn1 nulls and cons with no elements --- lib/openssl/asn1.rb | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index f8be6f516..d9145edf9 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -108,7 +108,7 @@ def cons_to_der str = +"" - @value.each_with_index do |item, idx| + ary.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" @@ -131,7 +131,7 @@ def prim_to_der def to_der_internal(body, constructed = false) default_tag = ASN1.take_default_tag(self.class) - body_len = body.size + body_len = body ? body.size : 0 if @tagging == :EXPLICIT raise ASN1Error, "explicit tagging of unknown tag" unless default_tag @@ -144,13 +144,13 @@ def to_der_internal(body, constructed = false) # Put explicit tag str = ASN1.put_object(true, @indefinite_length, inner_len, @tag, @tag_class) << inner_obj - str << body + str << body if 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 + str << body if body if @indefinite_length str << "\x00\x00" end @@ -250,7 +250,6 @@ def to_der class Null < Primitive def der_value - "" end end From 3f284576e2637f8b319abe1cd1a745bccdadf415 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Fri, 4 Jul 2025 14:47:15 +0100 Subject: [PATCH 09/11] making #der_value private --- lib/openssl/asn1.rb | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index d9145edf9..364a6f4c6 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -93,14 +93,15 @@ def to_der end end + private + + # :nodoc: def der_value raise TypeError, "no implicit conversion of #{self.class} into String" unless @value.respond_to?(:to_str) @value.to_str.b end - private - def cons_to_der ary = @value.to_a @@ -249,11 +250,17 @@ def to_der end class Null < Primitive + private + + # :nodoc: def der_value end end class Boolean < Primitive + private + + # :nodoc: def der_value raise TypeError, "Can't convert nil into Boolean" if @value.nil? @@ -262,12 +269,18 @@ def der_value end class Integer < Primitive + private + + # :nodoc: def der_value ASN1.put_integer(@value) end end class Enumerated < Primitive + private + + # :nodoc: def der_value ASN1.put_integer(@value) end @@ -282,6 +295,9 @@ def initialize(*) @unused_bits = 0 end + private + + # :nodoc: def der_value if @unused_bits < 0 || @unused_bits > 7 raise ASN1Error, "unused_bits for a bitstring value must be in " \ @@ -331,6 +347,9 @@ class BMPString < Primitive end class ObjectId < Primitive + private + + # :nodoc: def der_value value = oid @@ -359,6 +378,9 @@ def der_value class UTCTime < Primitive FORMAT = "%y%m%d%H%M%SZ".freeze + private + + # :nodoc: def der_value value = if @value.is_a?(Time) @value @@ -372,6 +394,10 @@ def der_value class GeneralizedTime < Primitive FORMAT = "%Y%m%d%H%M%SZ".freeze + + private + + # :nodoc: def der_value value = if @value.is_a?(Time) @value From dead3634d48327d8acf92cd4633766f1d6b98d5f Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Fri, 4 Jul 2025 14:52:39 +0100 Subject: [PATCH 10/11] forcing .chr conversion to binary encoding it's US-ASCII for integers < 128 --- lib/openssl/asn1.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index 364a6f4c6..3218f3739 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -306,7 +306,7 @@ def der_value return "\x00" if @value.empty? - @unused_bits.chr << super + @unused_bits.chr.force_encoding(Encoding::BINARY) << super end end @@ -356,7 +356,7 @@ def der_value dot_index = value.index(".") if dot_index == value.size - 1 - return (value.to_i * 40).chr + return (value.to_i * 40).chr.force_encoding(Encoding::BINARY) else codes = [value.byteslice(0..dot_index-1).to_i * 40] end @@ -436,7 +436,7 @@ def put_object(constructed, indefinite_length, length, tag, tag_class) i |= (xclass & 0xc0) # PRIVATE if tag < 31 - str = (i | tag).chr + str = (i | tag).chr.force_encoding(Encoding::BINARY) else str = [i | 0x1f, tag].pack("Cw") @@ -453,7 +453,7 @@ def put_object(constructed, indefinite_length, length, tag, tag_class) def put_length(length) if length < 0x80 - length.chr + length.chr.force_encoding(Encoding::BINARY) else data = integer_to_octets(length) data.unshift(data.size | 0x80) From a46b07b08d200dbf84f6261adc49ec834c9da880 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Fri, 4 Jul 2025 15:37:31 +0100 Subject: [PATCH 11/11] utctime and gentime: inline formats, constrain input data utctime should not take dates into account with a year below 1950 and above 2049 --- lib/openssl/asn1.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/openssl/asn1.rb b/lib/openssl/asn1.rb index 3218f3739..353ac9eda 100644 --- a/lib/openssl/asn1.rb +++ b/lib/openssl/asn1.rb @@ -376,10 +376,11 @@ def der_value end class UTCTime < Primitive - FORMAT = "%y%m%d%H%M%SZ".freeze - private + YEAR_RANGE = 1950..2049 + private_constant :YEAR_RANGE + # :nodoc: def der_value value = if @value.is_a?(Time) @@ -388,13 +389,13 @@ def der_value Time.at(Integer(@value)) end - value.utc.strftime(FORMAT) + raise OpenSSL::ASN1::ASN1Error unless YEAR_RANGE.include?(value.year) + + value.utc.strftime("%y%m%d%H%M%SZ") end end class GeneralizedTime < Primitive - FORMAT = "%Y%m%d%H%M%SZ".freeze - private # :nodoc: @@ -405,7 +406,7 @@ def der_value Time.at(Integer(@value)) end - value.utc.strftime(FORMAT) + value.utc.strftime("%Y%m%d%H%M%SZ") end end