From f403ad6d5a8159ede0003fa9f76215d7a8de1020 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Wed, 18 Jun 2025 15:33:47 +0100 Subject: [PATCH 01/11] uncommenting the last assertion of the application specific der test --- src/test/ruby/test_asn1.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/ruby/test_asn1.rb b/src/test/ruby/test_asn1.rb index 9544fc73..23c3890d 100644 --- a/src/test/ruby/test_asn1.rb +++ b/src/test/ruby/test_asn1.rb @@ -1295,7 +1295,7 @@ def test_decode_application_specific assert_equal 'o=Telstra', asn1_data.value[1].value assert_equal OpenSSL::ASN1::ASN1Data, asn1_data.value[2].class assert_equal :CONTEXT_SPECIFIC, asn1_data.value[2].tag_class -# assert_equal 'ess', asn1_data.value[2].value + assert_equal 'ess', asn1_data.value[2].value # assert_equal raw, asn1.to_der end From 11e9c7fd1b2f1764442928d011c67900603de839 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 24 Jun 2025 18:35:58 +0100 Subject: [PATCH 02/11] adding the basicdata tests from upstream ruby-openssl excluding already the one that I know that I can't solve, as BC does not allow tag > 31 for UNIVERSAL tag class --- src/test/ruby/test_asn1.rb | 85 +++++++++++++------------------------- 1 file changed, 28 insertions(+), 57 deletions(-) diff --git a/src/test/ruby/test_asn1.rb b/src/test/ruby/test_asn1.rb index 23c3890d..81c897f2 100644 --- a/src/test/ruby/test_asn1.rb +++ b/src/test/ruby/test_asn1.rb @@ -509,66 +509,37 @@ def test_generalizedtime end def test_basic_asn1data - # TODO: Import Issue - # Java::JavaLang::ClassCastException: - # class org.jruby.RubyString cannot be cast to class org.jruby.ext.openssl.ASN1$ASN1Data - # org.jruby.ext.openssl.ASN1$ASN1Data.toASN1TaggedObject(ASN1.java:1408) - # org.jruby.ext.openssl.ASN1$ASN1Data.toASN1(ASN1.java:1383) - # org.jruby.ext.openssl.ASN1$ASN1Data.toDER(ASN1.java:1424) - # org.jruby.ext.openssl.ASN1$ASN1Data.to_der(ASN1.java:1414) - #encode_test B(%w{ 00 00 }), OpenSSL::ASN1::ASN1Data.new(B(%w{}), 0, :UNIVERSAL) - #encode_test B(%w{ 01 00 }), OpenSSL::ASN1::ASN1Data.new(B(%w{}), 1, :UNIVERSAL) - #encode_decode_test B(%w{ 41 00 }), OpenSSL::ASN1::ASN1Data.new(B(%w{}), 1, :APPLICATION) - #encode_decode_test B(%w{ 81 00 }), OpenSSL::ASN1::ASN1Data.new(B(%w{}), 1, :CONTEXT_SPECIFIC) - #encode_decode_test B(%w{ C1 00 }), OpenSSL::ASN1::ASN1Data.new(B(%w{}), 1, :PRIVATE) - # TODO: Import Issue - # OpenSSL::ASN1::ASN1Error: tag number for :UNIVERSAL too large - # org/jruby/RubyClass.java:942:in `new' - #encode_decode_test B(%w{ 1F 20 00 }), OpenSSL::ASN1::ASN1Data.new(B(%w{}), 32, :UNIVERSAL) - #encode_decode_test B(%w{ 1F C0 20 00 }), OpenSSL::ASN1::ASN1Data.new(B(%w{}), 8224, :UNIVERSAL) - # TODO: Import Issue (same as start of this test) - # Java::JavaLang::ClassCastException: - # class org.jruby.RubyString cannot be cast to class org.jruby.ext.openssl.ASN1$ASN1Data - # org.jruby.ext.openssl.ASN1$ASN1Data.toASN1TaggedObject(ASN1.java:1408) - # org.jruby.ext.openssl.ASN1$ASN1Data.toASN1(ASN1.java:1383) - # org.jruby.ext.openssl.ASN1$ASN1Data.toDER(ASN1.java:1424) - # org.jruby.ext.openssl.ASN1$ASN1Data.to_der(ASN1.java:1414) - #encode_decode_test B(%w{ 41 02 AB CD }), OpenSSL::ASN1::ASN1Data.new(B(%w{ AB CD }), 1, :APPLICATION) - #encode_decode_test B(%w{ 41 81 80 } + %w{ AB CD } * 64), OpenSSL::ASN1::ASN1Data.new(B(%w{ AB CD } * 64), 1, :APPLICATION) - #encode_decode_test B(%w{ 41 82 01 00 } + %w{ AB CD } * 128), OpenSSL::ASN1::ASN1Data.new(B(%w{ AB CD } * 128), 1, :APPLICATION) - #encode_decode_test B(%w{ 61 00 }), OpenSSL::ASN1::ASN1Data.new([], 1, :APPLICATION) - #obj = OpenSSL::ASN1::ASN1Data.new([OpenSSL::ASN1::ASN1Data.new(B(%w{ AB CD }), 2, :PRIVATE)], 1, :APPLICATION) - #obj.indefinite_length = true - #encode_decode_test B(%w{ 61 80 C2 02 AB CD 00 00 }), obj - #obj = OpenSSL::ASN1::ASN1Data.new([ - # OpenSSL::ASN1::ASN1Data.new(B(%w{ AB CD }), 2, :PRIVATE), - # OpenSSL::ASN1::EndOfContent.new - #], 1, :APPLICATION) - #obj.indefinite_length = true - #encode_test B(%w{ 61 80 C2 02 AB CD 00 00 }), obj - #obj = OpenSSL::ASN1::ASN1Data.new(B(%w{ AB CD }), 1, :UNIVERSAL) - #obj.indefinite_length = true - # TODO: Import Issue - # expected but was <# expected but was <"\x01\x01\xFF"> - #encode_test B(%w{ 01 00 }), OpenSSL::ASN1::Primitive.new(B(%w{}), 1, nil, :UNIVERSAL) - # <"\x81\x00"> expected but was <"\x01\x01\xFF"> - #encode_test B(%w{ 81 00 }), OpenSSL::ASN1::Primitive.new(B(%w{}), 1, nil, :CONTEXT_SPECIFIC) - # <"\x01\x02\xAB\xCD"> expected but was <"\x01\x01\xFF"> - #encode_test B(%w{ 01 02 AB CD }), OpenSSL::ASN1::Primitive.new(B(%w{ AB CD }), 1) - # exception was expected but none was thrown. - #assert_raise(TypeError) { OpenSSL::ASN1::Primitive.new([], 1).to_der } + encode_test B(%w{ 00 00 }), OpenSSL::ASN1::Primitive.new(B(%w{}), 0) + encode_test B(%w{ 01 00 }), OpenSSL::ASN1::Primitive.new(B(%w{}), 1, nil, :UNIVERSAL) + encode_test B(%w{ 81 00 }), OpenSSL::ASN1::Primitive.new(B(%w{}), 1, nil, :CONTEXT_SPECIFIC) + encode_test B(%w{ 01 02 AB CD }), OpenSSL::ASN1::Primitive.new(B(%w{ AB CD }), 1) + assert_raise(TypeError) { OpenSSL::ASN1::Primitive.new([], 1).to_der } prim = OpenSSL::ASN1::Integer.new(50) assert_equal false, prim.indefinite_length From 610a3559b982f987deb775ee65541776d6000a45 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 24 Jun 2025 18:39:29 +0100 Subject: [PATCH 03/11] inlining getConstructiveTag logic, expand on the rest of the information in the tag segment the tag segment also contains info about whether the payload is for a constructed DER, and whether it's indefinite length; this info was buried in the method, with no easy way to piggyback on, so it was easier to inline the logic (only used here anyway), and propagate the rest of the information, which allows setting the indefinite_length ivar for ASN1Data objects it also raises exceptions where it couldn't (or shouldn't?) --- src/main/java/org/jruby/ext/openssl/ASN1.java | 210 ++++++++---------- 1 file changed, 93 insertions(+), 117 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 03a19e81..9ceed047 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -1166,19 +1166,60 @@ private BytesInputStream(final ByteList bytes) { private static IRubyObject decodeImpl(final ThreadContext context, final RubyModule ASN1, final BytesInputStream in) throws IOException, IllegalArgumentException { + final byte[] asn1 = in.bytes(); + int offset = in.offset(); + final int tag = asn1[offset] & 0xFF; + + if ( ( tag & BERTags.CONSTRUCTED ) == 0 ) { + return decodeObject(context, ASN1, readObject(in)); + } + // NOTE: need to handle OpenSSL::ASN1::Constructive wrapping by hand : - final Integer tag = getConstructiveTag(in.bytes(), in.offset()); - IRubyObject decoded = decodeObject(context, ASN1, readObject( in )); - if ( tag != null ) { // OpenSSL::ASN1::Constructive.new( arg ) : - if ( tag.intValue() == SEQUENCE ) { - //type = "Sequence"; // got a OpenSSL::ASN1::Sequence already : - return Constructive.setInfiniteLength(context, decoded); + int tagNo = tag & 0x1f; + if (tagNo == 0x1f) + { + tagNo = 0; + int b = asn1[ ++offset ]; + + // X.690-0207 8.1.2.4.2 + // "c) bits 7 to 1 of the first subsequent octet shall not all be zero." + if ((b & 0x7f) == 0) // Note: -1 will pass + { + throw new IOException("corrupted stream - invalid high tag number found"); + } + + while ((b >= 0) && ((b & 0x80) != 0)) + { + tagNo |= (b & 0x7f); + tagNo <<= 7; + b = asn1[ ++offset ]; + } + + if (b < 0) + { + throw new IOException("EOF found inside tag value."); } - if ( tag.intValue() == SET ) { - //type = "Set"; // got a OpenSSL::ASN1::Set already : - return Constructive.setInfiniteLength(context, decoded); + + tagNo |= (b & 0x7f); + } + final int length = asn1[ ++offset ] & 0xFF; + final boolean isIndefiniteLength = length == 0x80; + + IRubyObject decoded = decodeObject(context, ASN1, readObject(in)); + final boolean isUniversal = ((ASN1Data) decoded).isUniversal(context); + + if (isIndefiniteLength) { + if (tagNo == BERTags.SEQUENCE || tagNo == BERTags.SET) { + return ASN1Data.setInfiniteLength(context, decoded); + } else if (isUniversal) { + decoded = Constructive.newInfiniteLength(context, context.runtime.newArray(decoded), tagNo); + } else { + if (decoded instanceof ASN1Data) { + return ASN1Data.setInfiniteLength(context, decoded); + } else { + decoded = ASN1Data.newInfiniteLength(context, context.runtime.newArray(decoded), tagNo, ((ASN1Data) decoded).tagClass()); + } } - return Constructive.newInfiniteConstructive(context, "Constructive", context.runtime.newArray(decoded), tag); } return decoded; } @@ -1235,89 +1276,6 @@ private static org.bouncycastle.asn1.ASN1Primitive readObject(final InputStream return new ASN1InputStream(bytes).readObject(); } - // NOTE: BC's ASNInputStream internals "reinvented" a bit : - private static Integer getConstructiveTag(final byte[] asn1, int offset) { - final int tag = asn1[ offset ] & 0xFF; - if ( ( tag & BERTags.CONSTRUCTED ) != 0 ) { // isConstructed - // - // calculate tag number - // - // readTagNumber(asn1, ++offset, tag) : - int tagNo = tag & 0x1f; - // - // with tagged object tag number is bottom 5 bits, or stored at the start of the content - // - if (tagNo == 0x1f) - { - tagNo = 0; - - int b = asn1[ ++offset ]; //s.read(); - - // X.690-0207 8.1.2.4.2 - // "c) bits 7 to 1 of the first subsequent octet shall not all be zero." - if ((b & 0x7f) == 0) // Note: -1 will pass - { - return null; //throw new IOException("corrupted stream - invalid high tag number found"); - } - - while ((b >= 0) && ((b & 0x80) != 0)) - { - tagNo |= (b & 0x7f); - tagNo <<= 7; - b = asn1[ ++offset ]; //s.read(); - } - - if (b < 0) - { - return null; //throw new EOFException("EOF found inside tag value."); - } - - tagNo |= (b & 0x7f); - } - - // - // calculate length - // - final int length = asn1[ ++offset ] & 0xFF; - - if ( length == 0x80 ) { - // return -1; // indefinite-length encoding - } - else { - return null; - } - - if ((tag & BERTags.APPLICATION) != 0) { - //return new BERApplicationSpecificParser(tagNo, sp).getLoadedObject(); - } - - if ((tag & BERTags.TAGGED) != 0) { - //return new BERTaggedObjectParser(true, tagNo, sp).getLoadedObject(); - } - - //System.out.println(" tagNo = 0x" + Integer.toHexString(tagNo)); - // TODO There are other tags that may be constructed (e.g. BIT_STRING) - switch (tagNo) { - case BERTags.SEQUENCE : - //return new BERSequenceParser(sp).getLoadedObject(); - return Integer.valueOf( SEQUENCE ); //return "Sequence"; - case BERTags.SET : - //return new BERSetParser(sp).getLoadedObject(); - return Integer.valueOf( SET ); //return "Set"; - case BERTags.OCTET_STRING : - return Integer.valueOf( OCTET_STRING ); - //return new BEROctetStringParser(sp).getLoadedObject(); - case BERTags.EXTERNAL : - //return new DERExternalParser(sp).getLoadedObject(); - default: - return Integer.valueOf( 0 ); //return "Constructive"; - //throw new IOException("unknown BER object encountered"); - } - } - - return null; - } - public static class ASN1Data extends RubyObject { private static final long serialVersionUID = 6117598347932209839L; @@ -1340,9 +1298,38 @@ public IRubyObject initialize(final ThreadContext context, this.callMethod(context, "tag=", tag); this.callMethod(context, "value=", value); this.callMethod(context, "tag_class=", tag_class); + this.setInstanceVariable("@indefinite_length", context.runtime.getFalse()); return this; } + static ASN1Data newInfiniteLength(final ThreadContext context, + final IRubyObject value, final int defaultTag, final IRubyObject tagClass) { + final Ruby runtime = context.runtime; + + final RubyClass klass = _ASN1(runtime).getClass("ASN1Data"); + final ASN1Data self = new Constructive(runtime, klass); + + ASN1Data.newInfiniteLengthImpl(context, self, value, defaultTag, tagClass); + return self; + } + + static void newInfiniteLengthImpl(final ThreadContext context, final ASN1Data self, final IRubyObject value, final int defaultTag, final IRubyObject tagClass) { + self.setInstanceVariable("@tag", context.runtime.newFixnum(defaultTag)); + self.setInstanceVariable("@value", value); + self.setInstanceVariable("@tag_class", tagClass); + self.setInstanceVariable("@tagging", context.nil); + + setInfiniteLength(context, self); + } + + static ASN1Data setInfiniteLength(final ThreadContext context, final IRubyObject constructive) { + final ASN1Data instance = ((ASN1Data) constructive); + final IRubyObject value = instance.value(context); + value.callMethod(context, "<<", EndOfContent.newInstance(context)); + instance.setInstanceVariable("@indefinite_length", context.runtime.getTrue()); + return instance; + } + private void checkTag(final Ruby runtime, final IRubyObject tag, final IRubyObject tagClass) { if ( ! (tagClass instanceof RubySymbol) ) { throw newASN1Error(runtime, "invalid tag class"); @@ -1357,13 +1344,17 @@ boolean isEOC() { } boolean isUniversal(final ThreadContext context) { - return "ASN1Data".equals(getClassBaseName()) && getTagClass(context) == 0; + return getTagClass(context) == BERTags.UNIVERSAL; } IRubyObject tagging() { return getInstanceVariable("@tagging"); } + IRubyObject tagClass() { + return getInstanceVariable("@tag_class"); + } + boolean isExplicitTagging() { return ! isImplicitTagging(); } boolean isImplicitTagging() { return true; } @@ -1459,7 +1450,9 @@ public IRubyObject to_der(final ThreadContext context) { byte[] toDER(final ThreadContext context) throws IOException { if ( isEOC() ) return new byte[] { 0x00, 0x00 }; - if (isUniversal(context)) { + final boolean isIndefiniteLength = getInstanceVariable("@indefinite_length").isTrue(); + + if ("ASN1Data".equals(getClassBaseName()) && isUniversal(context)) { // handstitch conversion final java.io.ByteArrayOutputStream out = new ByteArrayOutputStream(); final IRubyObject value = callMethod(context, "value"); @@ -1686,11 +1679,6 @@ boolean isEOC() { return false; } - @Override - boolean isUniversal(final ThreadContext context) { - return false; - } - private boolean isNull() { return "Null".equals(getMetaClass().getRealClass().getBaseName()); } @@ -1857,27 +1845,15 @@ public IRubyObject initialize(final ThreadContext context, final IRubyObject[] a return this; } - static Constructive newInfiniteConstructive(final ThreadContext context, - final String type, final IRubyObject value, final int defaultTag) { + static Constructive newInfiniteLength(final ThreadContext context, + final IRubyObject value, final int defaultTag) { final Ruby runtime = context.runtime; - final RubyClass klass = _ASN1(context.runtime).getClass(type); + final RubyClass klass = _ASN1(context.runtime).getClass("Constructive"); final Constructive self = new Constructive(runtime, klass); - self.setInstanceVariable("@tag", runtime.newFixnum(defaultTag)); - self.setInstanceVariable("@value", value); - self.setInstanceVariable("@tag_class", runtime.newSymbol("UNIVERSAL")); - self.setInstanceVariable("@tagging", context.nil); - - return setInfiniteLength(context, self); - } - - static Constructive setInfiniteLength(final ThreadContext context, final IRubyObject constructive) { - final Constructive instance = ((Constructive) constructive); - final IRubyObject value = instance.value(context); - value.callMethod(context, "<<", EndOfContent.newInstance(context)); - instance.setInstanceVariable("@indefinite_length", context.runtime.getTrue()); - return instance; + ASN1Data.newInfiniteLengthImpl(context, self, value, defaultTag, runtime.newSymbol("UNIVERSAL")); + return self; } private boolean rawConstructive() { From c781cf99e4357a695f66b470c3215ceb9b3e5fd1 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 24 Jun 2025 18:44:00 +0100 Subject: [PATCH 04/11] fixing encoding of arrays in ASN1Data structures as well as dealing with EOC empty arrays on asn1data are encoded to BERSequences, which fixed some corner cases associated with asn1data EOC isn't supported OOTB by bouncycastle, so these have to be ignored in the ASN1 part, since there's no way to use DERTaggedObject some of the logic to add the EOC bytes are inlined based on the implemented from bouncycastle, which does not allow to compose on anything, as all entities are private and unextendable --- src/main/java/org/jruby/ext/openssl/ASN1.java | 86 ++++++++++++++++--- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 9ceed047..2f0edca0 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -1411,17 +1411,18 @@ final ASN1TaggedObject toASN1TaggedObject(final ThreadContext context) { } } - if (vec.size() > 0) { - // array of asn1 objects as value - return new DERTaggedObject(isExplicitTagging(), tag, new DERSequence(vec)); + if (values.length() > 0) { + return new DERTaggedObject(isExplicitTagging(), tagClass, tag, new DERGeneralString(values.toString())); + } else { + // array of strings as value (default) + return new DERTaggedObject(isExplicitTagging(), tagClass, tag, new BERSequence(vec)); } - - // array of strings as value (default) - return new DERTaggedObject(isExplicitTagging(), tagClass, tag, - new DERGeneralString(values.toString())); } else if (value instanceof ASN1Data) { return new DERTaggedObject(isExplicitTagging(), tagClass, tag, ((ASN1Data) value).toASN1(context)); } else if (value instanceof RubyObject) { + if (isEOC()) { + return null; + } final IRubyObject string = value.checkStringType(); if (string instanceof RubyString) { return new DERTaggedObject(isExplicitTagging(), tagClass, tag, @@ -1481,12 +1482,77 @@ byte[] toDER(final ThreadContext context) throws IOException { "no implicit conversion of " + value.getMetaClass().getBaseName() + " into String"); } } - out.write(getTag(context)); - out.write(valueBytes.length); + // tag + writeDERIdentifier(getTag(context), getTagClass(context), out); + writeDERLength(valueBytes.length, out); + // value out.write(valueBytes); + return out.toByteArray(); } - return toASN1(context).toASN1Primitive().getEncoded(ASN1Encoding.DER); + final ASN1Primitive prim = toASN1(context).toASN1Primitive(); + + if (isIndefiniteLength) { + final java.io.ByteArrayOutputStream tagOut = new ByteArrayOutputStream(); + final java.io.ByteArrayOutputStream contentOut = new ByteArrayOutputStream(); + final java.io.ByteArrayOutputStream out = new ByteArrayOutputStream(); + prim.encodeTo(contentOut, ASN1Encoding.DER); + writeDERIdentifier(getTag(context), getTagClass(context) | BERTags.CONSTRUCTED, tagOut); + + byte[] tagOutArr = tagOut.toByteArray(); + byte[] contentOutArr = contentOut.toByteArray(); + + out.write(tagOutArr); + out.write(0x80); + out.write(contentOutArr, tagOutArr.length + 1, contentOutArr.length - tagOutArr.length - 1); + out.write(0x00); + out.write(0x00); + + return out.toByteArray(); + } else { + return prim.getEncoded(ASN1Encoding.DER); + } + } + + void writeDERIdentifier(int tag, int flags, java.io.ByteArrayOutputStream out) { + if (tag > 0x1f) { + byte[] stack = new byte[6]; + int pos = stack.length; + + stack[--pos] = (byte)(tag & 0x7F); + while (tag > 127) + { + tag >>>= 7; + stack[--pos] = (byte)(tag & 0x7F | 0x80); + } + + stack[--pos] = (byte)(flags | 0x1F); + + out.write(stack, pos, stack.length - pos); + } else { + out.write(flags | tag); + } + } + + void writeDERLength(int length, java.io.ByteArrayOutputStream out) { + if (length < 128) { + out.write(length); + } else { + byte[] stack = new byte[5]; + int pos = stack.length; + + do + { + stack[--pos] = (byte)length; + length >>>= 8; + } + while (length != 0); + + int count = stack.length - pos; + stack[--pos] = (byte)(0x80 | count); + + out.write(stack, pos, count - pos); + } } protected IRubyObject defaultTag() { From 2a52e967ebe85b948de75482c76640500677783b Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 24 Jun 2025 18:46:23 +0100 Subject: [PATCH 05/11] do not allow encoding asn1data with indefinite_length set and a value which is not an array this is the behaviour from upstream --- src/main/java/org/jruby/ext/openssl/ASN1.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 2f0edca0..114faf5e 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -1474,6 +1474,13 @@ byte[] toDER(final ThreadContext context) throws IOException { } valueBytes = valueOut.toByteArray(); } else { + if (isIndefiniteLength) { + throw newASN1Error( + context.runtime, + "indefinite length form cannot be used with primitive encoding" + ); + } + final IRubyObject string = value.checkStringType(); if (string instanceof RubyString) { valueBytes = ((RubyString) string).getBytes(); From de55cdde88239a4a6024184f025449eda8a371f2 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Tue, 24 Jun 2025 18:48:12 +0100 Subject: [PATCH 06/11] making the EndOfContent class a subclass of ASN1Data this is what upstream does --- src/main/java/org/jruby/ext/openssl/ASN1.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 114faf5e..3cf8068c 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -1611,11 +1611,20 @@ static void printArray(final PrintStream out, final int indent, final RubyArray } - public static class EndOfContent { + public static class EndOfContent extends ASN1Data { - private EndOfContent() {} + static ObjectAllocator ALLOCATOR = new ObjectAllocator() { + public IRubyObject allocate(Ruby runtime, RubyClass klass) { + return new EndOfContent(runtime, klass); + } + }; - @JRubyMethod(visibility = Visibility.PRIVATE) + public EndOfContent(Ruby runtime, RubyClass type) { + super(runtime,type); + } + + + @JRubyMethod(required = 0, optional = 0, visibility = Visibility.PRIVATE) public static IRubyObject initialize(final ThreadContext context, final IRubyObject self) { final Ruby runtime = context.runtime; self.getInstanceVariables().setInstanceVariable("@tag", runtime.newFixnum(0)); @@ -1629,6 +1638,12 @@ static IRubyObject newInstance(final ThreadContext context) { return klass.newInstance(context, Block.NULL_BLOCK); } + @Override + boolean isImplicitTagging() { + IRubyObject tagging = tagging(); + if ( tagging.isNil() ) return true; + return "IMPLICIT".equals( tagging.toString() ); + } } public static class Primitive extends ASN1Data { From 7fa9cf9968ab4aee2751220dac5f82c9516ee6fb Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 26 Jun 2025 15:03:21 +0100 Subject: [PATCH 07/11] fixed initialization of Primitive the logic was unaligned with upstream (see ruby rewrite here: https://github.com/ruby/openssl/blob/master/lib/openssl/asn1.rb\#L107C40-L122) --- src/main/java/org/jruby/ext/openssl/ASN1.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 3cf8068c..618bed11 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -1701,12 +1701,20 @@ static void initializeImpl(final ThreadContext context, if ( tag.isNil() ) throw newASN1Error(runtime, "must specify tag number"); - if ( tagging.isNil() ) tagging = runtime.newSymbol("EXPLICIT"); - if ( ! (tagging instanceof RubySymbol) ) { - throw newASN1Error(runtime, "invalid tag default"); + if ( tagging.isNil()) { + if (tag_class.isNil()) { + tag_class = runtime.newSymbol("UNIVERSAL"); + } + } else { + if (!(tagging instanceof RubySymbol)) { + throw newASN1Error(runtime, "invalid tagging method"); + } + + if (tag_class.isNil()) { + tag_class = runtime.newSymbol("CONTEXT_SPECIFIC"); + } } - if ( tag_class.isNil() ) tag_class = runtime.newSymbol("CONTEXT_SPECIFIC"); if ( ! (tag_class instanceof RubySymbol) ) { throw newASN1Error(runtime, "invalid tag class"); } From 5cbd774d2cfdb715ee872115da65ad16f88e25f4 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 26 Jun 2025 15:05:06 +0100 Subject: [PATCH 08/11] rewrote isEOC as a function of its data ruby allows EOC objects to be built via ASN1Data initialization, so one has to use the info of tag and tag class instead --- src/main/java/org/jruby/ext/openssl/ASN1.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 618bed11..43bd608e 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -1339,8 +1339,8 @@ private void checkTag(final Ruby runtime, final IRubyObject tag, final IRubyObje } } - boolean isEOC() { - return "EndOfContent".equals( getClassBaseName() ); + boolean isEOC(final ThreadContext context) { + return getTag(context) == 0 && isUniversal((context)); } boolean isUniversal(final ThreadContext context) { @@ -1420,7 +1420,7 @@ final ASN1TaggedObject toASN1TaggedObject(final ThreadContext context) { } else if (value instanceof ASN1Data) { return new DERTaggedObject(isExplicitTagging(), tagClass, tag, ((ASN1Data) value).toASN1(context)); } else if (value instanceof RubyObject) { - if (isEOC()) { + if (isEOC(context)) { return null; } final IRubyObject string = value.checkStringType(); @@ -1449,7 +1449,7 @@ public IRubyObject to_der(final ThreadContext context) { } byte[] toDER(final ThreadContext context) throws IOException { - if ( isEOC() ) return new byte[] { 0x00, 0x00 }; + if (isEOC(context)) return new byte[] { 0x00, 0x00 }; final boolean isIndefiniteLength = getInstanceVariable("@indefinite_length").isTrue(); @@ -1771,7 +1771,7 @@ boolean isImplicitTagging() { } @Override - boolean isEOC() { + boolean isEOC(final ThreadContext context) { return false; } @@ -2139,7 +2139,7 @@ private static boolean addEntry(final ThreadContext context, final ASN1Encodable } else if ( entry instanceof ASN1Data ) { final ASN1Data data = ( (ASN1Data) entry ); - if ( data.isEOC() ) return true; + if ( data.isEOC(context) ) return true; vec.add( data.toASN1(context) ); } else { From 64ae0c32013fad468b71d66df8336b94092218a2 Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Thu, 26 Jun 2025 15:07:42 +0100 Subject: [PATCH 09/11] implement logic to der-encode objects at the base class since ruby does not have abstract classes, instances of root/intermediate classes may be instantiated, and args will determine how those objects really have to be handled this follows the logic of upstream, which implements der-encode at the base class by outsourcing to specific impls based on ivar state --- src/main/java/org/jruby/ext/openssl/ASN1.java | 218 +++++++++++------- 1 file changed, 134 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 43bd608e..1f5a7c86 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -41,7 +41,6 @@ import java.util.WeakHashMap; import org.bouncycastle.asn1.*; - import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyBignum; @@ -1339,6 +1338,14 @@ private void checkTag(final Ruby runtime, final IRubyObject tag, final IRubyObje } } + private boolean isConstructive() { + return "Constructive".equals(getMetaClass().getRealClass().getBaseName()); + } + + boolean isInfiniteLength() { + return getInstanceVariable("@indefinite_length").isTrue(); + } + boolean isEOC(final ThreadContext context) { return getTag(context) == 0 && isUniversal((context)); } @@ -1449,57 +1456,15 @@ public IRubyObject to_der(final ThreadContext context) { } byte[] toDER(final ThreadContext context) throws IOException { - if (isEOC(context)) return new byte[] { 0x00, 0x00 }; - - final boolean isIndefiniteLength = getInstanceVariable("@indefinite_length").isTrue(); - - if ("ASN1Data".equals(getClassBaseName()) && isUniversal(context)) { - // handstitch conversion - final java.io.ByteArrayOutputStream out = new ByteArrayOutputStream(); - final IRubyObject value = callMethod(context, "value"); - - final byte[] valueBytes; - if (value instanceof RubyArray) { - final RubyArray arr = (RubyArray) value; - final java.io.ByteArrayOutputStream valueOut = new ByteArrayOutputStream(); - - for (final IRubyObject obj : arr.toJavaArray()) { - final IRubyObject string = value.checkStringType(); - if (string instanceof RubyString) { - valueOut.write(((RubyString) string).getBytes()); - } else { - throw context.runtime.newTypeError( - "no implicit conversion of " + obj.getMetaClass().getBaseName() + " into String"); - } - } - valueBytes = valueOut.toByteArray(); - } else { - if (isIndefiniteLength) { - throw newASN1Error( - context.runtime, - "indefinite length form cannot be used with primitive encoding" - ); - } - - final IRubyObject string = value.checkStringType(); - if (string instanceof RubyString) { - valueBytes = ((RubyString) string).getBytes(); - } else { - throw context.runtime.newTypeError( - "no implicit conversion of " + value.getMetaClass().getBaseName() + " into String"); - } - } - // tag - writeDERIdentifier(getTag(context), getTagClass(context), out); - writeDERLength(valueBytes.length, out); - // value - out.write(valueBytes); - - return out.toByteArray(); + if ( + ("ASN1Data".equals(getClassBaseName()) && isUniversal(context)) + ) { + return toDERInternal(context, isConstructive(), isInfiniteLength(), value(context)); } + final ASN1Primitive prim = toASN1(context).toASN1Primitive(); - if (isIndefiniteLength) { + if (isInfiniteLength()) { final java.io.ByteArrayOutputStream tagOut = new ByteArrayOutputStream(); final java.io.ByteArrayOutputStream contentOut = new ByteArrayOutputStream(); final java.io.ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -1521,6 +1486,78 @@ byte[] toDER(final ThreadContext context) throws IOException { } } + byte[] toDERInternal(final ThreadContext context, boolean isConstructed, boolean isIndefiniteLength, final IRubyObject value) throws IOException { + // handstitch conversion + final java.io.ByteArrayOutputStream out = new ByteArrayOutputStream(); + + final byte[] valueBytes; + + if (value == null) { + valueBytes = new byte[] {}; + } else if (value instanceof RubyArray) { + final IRubyObject[] arr = ((RubyArray) value).toJavaArray(); + final java.io.ByteArrayOutputStream valueOut = new ByteArrayOutputStream(); + + + for ( int i = 0; i < arr.length; i++ ) { + final IRubyObject obj = arr[i]; + + if (obj instanceof EndOfContent && i != arr.length - 1) { + throw newASN1Error(context.runtime, "illegal EOC octets in value"); + } + + final byte[] objBytes; + + if (obj.respondsTo("to_der")) { + objBytes = ((RubyString) obj.callMethod(context, "to_der")).getBytes(); + } else { + objBytes = ((RubyString) obj.convertToString()).getBytes(); + } + + valueOut.write(objBytes); + } + + if (isIndefiniteLength) { + if (arr.length != 0 && !(arr[arr.length - 1] instanceof EndOfContent)) { + // indefinite length object with no EOC object in the array. + valueOut.write(0x00); + valueOut.write(0x00); + } + } + + valueBytes = valueOut.toByteArray(); + } else { + if (isIndefiniteLength) { + throw newASN1Error( + context.runtime, + "indefinite length form cannot be used with primitive encoding" + ); + } + + if (value instanceof RubyString) { + valueBytes = ((RubyString) value).getBytes(); + } else { + valueBytes = value.convertToString().getBytes(); + } + } + + int flags = getTagClass(context); + if (isConstructed) { + flags |= BERTags.CONSTRUCTED; + } + // tag + writeDERIdentifier(getTag(context), flags, out); + if (isIndefiniteLength) { + out.write(0x80); + } else { + writeDERLength(valueBytes.length, out); + } + // value + out.write(valueBytes); + + return out.toByteArray(); + } + void writeDERIdentifier(int tag, int flags, java.io.ByteArrayOutputStream out) { if (tag > 0x1f) { byte[] stack = new byte[6]; @@ -1644,6 +1681,11 @@ boolean isImplicitTagging() { if ( tagging.isNil() ) return true; return "IMPLICIT".equals( tagging.toString() ); } + + @Override + byte[] toDER(final ThreadContext context) throws IOException { + return toDERInternal(context, false, false, null); + } } public static class Primitive extends ASN1Data { @@ -1781,6 +1823,21 @@ private boolean isNull() { @Override byte[] toDER(final ThreadContext context) throws IOException { + Class type = typeClass( getMetaClass() ); + final IRubyObject value = value(context); + + if ( type == null ) { + RubyString string; + + if (value instanceof RubyString) { + string = (RubyString) value; + } else { + string = value.convertToString(); + } + + return toDERInternal(context, false, false, string); + } + return toASN1(context).toASN1Primitive().getEncoded(ASN1Encoding.DER); } @@ -1795,16 +1852,6 @@ ASN1Encodable toASN1(final ThreadContext context) { private ASN1Encodable toASN1Primitive(final ThreadContext context) { Class type = typeClass( getMetaClass() ); - if ( type == null ) { - final int tag = getTag(context); - if ( tag == 0 ) return null; // TODO pass EOC to BC ? - if ( isExplicitTagging() ) type = typeClass( tag ); - if ( type == null ) { - throw new IllegalArgumentException( - "no type for: " + getMetaClass() + " or tag: " + getTag(context) - ); - } - } final IRubyObject val = value(context); if ( type == ASN1ObjectIdentifier.class ) { @@ -1964,10 +2011,6 @@ private boolean isSet() { return "Set".equals( getClassBaseName() ); } - private boolean isInfiniteLength() { - return getInstanceVariable("@indefinite_length").isTrue(); - } - private boolean isTagged() { return !tagging().isNil(); } @@ -2013,22 +2056,19 @@ ASN1Encodable toASN1(final ThreadContext context) { @Override @JRubyMethod public IRubyObject to_der(final ThreadContext context) { - if ( rawConstructive() ) { // MRI compatibility - if ( ! isInfiniteLength() && ! super.value(context).isNil() ) { - final Ruby runtime = context.runtime; - throw newASN1Error(runtime, "Constructive shall only be used with indefinite length"); - } - } return super.to_der(context); } @Override byte[] toDER(final ThreadContext context) throws IOException { - if ( isInfiniteLength() ) { - if ( isSequence() ) { + final int tagNo = getTag(context); + final boolean isIndefiniteLength = isInfiniteLength(); + + if ( isIndefiniteLength ) { + if ( isSequence() || tagNo == SEQUENCE ) { return sequenceToDER(context); } - if ( isSet() ) { + if ( isSet() || tagNo == SET) { return setToDER(context); } // "raw" Constructive @@ -2037,12 +2077,18 @@ byte[] toDER(final ThreadContext context) throws IOException { return octetStringToDER(context); case BIT_STRING: return bitStringToDER(context); - case SEQUENCE: - return sequenceToDER(context); - case SET: - return setToDER(context); } - throw new UnsupportedOperationException( this.inspect().toString() ); + return toDERInternal(context, true, isInfiniteLength(), value(context)); + } + + if (isEOC(context)) { + return toDERInternal(context, true, isIndefiniteLength, null); + } + + Class type = typeClass( getMetaClass() ); + + if ( type == null ) { + return toDERInternal(context, true, isIndefiniteLength, valueAsArray(context)); } return super.toDER(context); @@ -2094,19 +2140,23 @@ private byte[] setToDER(final ThreadContext context) throws IOException { private ASN1EncodableVector toASN1EncodableVector(final ThreadContext context) { final ASN1EncodableVector vec = new ASN1EncodableVector(); final IRubyObject value = value(context); - final RubyArray val; + final RubyArray val = valueAsArray(context); + for ( int i = 0; i < val.size(); i++ ) { + if ( addEntry(context, vec, val.entry(i)) ) break; + } + return vec; + } + + private RubyArray valueAsArray(final ThreadContext context) { + final IRubyObject value = value(context); if (value instanceof RubyArray ) { - val = (RubyArray) value; + return (RubyArray) value; } else { if (!value.respondsTo("to_a")) { throw context.runtime.newTypeError("can't convert " + value.getMetaClass().getName() + " into Array"); } - val = (RubyArray) value.callMethod(context, "to_a"); - } - for ( int i = 0; i < val.size(); i++ ) { - if ( addEntry(context, vec, val.entry(i)) ) break; + return (RubyArray) value.callMethod(context, "to_a"); } - return vec; } public ASN1Primitive toASN1Primitive() { From 5140acd0a4495e2f3b67075c2289069f40ca6d7e Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Mon, 30 Jun 2025 17:57:52 +0100 Subject: [PATCH 10/11] loading EndOfContent class correctly so overrides are correctly loaded --- src/main/java/org/jruby/ext/openssl/ASN1.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 1f5a7c86..e8ce374c 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -703,6 +703,10 @@ public static void createASN1(final Ruby runtime, final RubyModule OpenSSL, fina Constructive.addReadWriteAttribute(context, "tagging"); Constructive.defineAnnotatedMethods(Constructive.class); + final ObjectAllocator eocAllocator = EndOfContent.ALLOCATOR; + RubyClass EndOfContent = ASN1.defineClassUnder("EndOfContent", _ASN1Data, eocAllocator); + EndOfContent.defineAnnotatedMethods(EndOfContent.class); + ASN1.defineClassUnder("Boolean", Primitive, primitiveAllocator); // OpenSSL::ASN1::Boolean <=> value is a Boolean ASN1.defineClassUnder("Integer", Primitive, primitiveAllocator); // OpenSSL::ASN1::Integer <=> value is a Number ASN1.defineClassUnder("Null", Primitive, primitiveAllocator); // OpenSSL::ASN1::Null <=> value is always nil @@ -727,9 +731,6 @@ public static void createASN1(final Ruby runtime, final RubyModule OpenSSL, fina ASN1.defineClassUnder("UTCTime", Primitive, primitiveAllocator); // OpenSSL::ASN1::UTCTime <=> value is a Time ASN1.defineClassUnder("GeneralizedTime", Primitive, primitiveAllocator); // OpenSSL::ASN1::GeneralizedTime <=> value is a Time - ASN1.defineClassUnder("EndOfContent", _ASN1Data, asn1DataAllocator). // OpenSSL::ASN1::EndOfContent <=> value is always nil - defineAnnotatedMethods(EndOfContent.class); - ASN1.defineClassUnder("ObjectId", Primitive, primitiveAllocator). defineAnnotatedMethods(ObjectId.class); From ab34440b46dbe66f28f65d9bb49c20c84815698f Mon Sep 17 00:00:00 2001 From: HoneyryderChuck Date: Mon, 30 Jun 2025 17:59:07 +0100 Subject: [PATCH 11/11] adding basic constructive tests from upstream commenting out the one I could only half port, as no BC parser supports random tagged objects --- src/main/java/org/jruby/ext/openssl/ASN1.java | 4 ++- src/test/ruby/test_asn1.rb | 34 +++++++------------ 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index e8ce374c..3e7417e4 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -1204,8 +1204,10 @@ private static IRubyObject decodeImpl(final ThreadContext context, final RubyMod } final int length = asn1[ ++offset ] & 0xFF; final boolean isIndefiniteLength = length == 0x80; + IRubyObject decoded; + + decoded = decodeObject(context, ASN1, readObject(in)); - IRubyObject decoded = decodeObject(context, ASN1, readObject(in)); final boolean isUniversal = ((ASN1Data) decoded).isUniversal(context); if (isIndefiniteLength) { diff --git a/src/test/ruby/test_asn1.rb b/src/test/ruby/test_asn1.rb index 81c897f2..0cc59e27 100644 --- a/src/test/ruby/test_asn1.rb +++ b/src/test/ruby/test_asn1.rb @@ -553,27 +553,19 @@ def test_basic_primitive # def test_basic_constructed - #octet_string = OpenSSL::ASN1::OctetString.new(B(%w{ AB CD })) - # TODO: Import Issue - # OpenSSL::ASN1::ASN1Error: Constructive shall only be used with indefinite length - #encode_test B(%w{ 20 00 }), OpenSSL::ASN1::Constructive.new([], 0) - #encode_test B(%w{ 21 00 }), OpenSSL::ASN1::Constructive.new([], 1, nil, :UNIVERSAL) - #encode_test B(%w{ A1 00 }), OpenSSL::ASN1::Constructive.new([], 1, nil, :CONTEXT_SPECIFIC) - #encode_test B(%w{ 21 04 04 02 AB CD }), OpenSSL::ASN1::Constructive.new([octet_string], 1) - # Java::JavaLang::UnsupportedOperationException: - # #], - # @tag_class=:CONTEXT_SPECIFIC, @tagging=:EXPLICIT, @indefinite_length=true> - # org.jruby.ext.openssl.ASN1$Constructive.toDER(ASN1.java:1881) - # org.jruby.ext.openssl.ASN1$ASN1Data.to_der(ASN1.java:1414) - # org.jruby.ext.openssl.ASN1$Constructive.to_der(ASN1.java:1858) - #obj = OpenSSL::ASN1::Constructive.new([octet_string], 1) - #obj.indefinite_length = true - #encode_decode_test B(%w{ 21 80 04 02 AB CD 00 00 }), obj - # (see above) Java::JavaLang::UnsupportedOperationException - #obj = OpenSSL::ASN1::Constructive.new([octet_string, OpenSSL::ASN1::EndOfContent.new], 1) - #obj.indefinite_length = true - #encode_test B(%w{ 21 80 04 02 AB CD 00 00 }), obj + octet_string = OpenSSL::ASN1::OctetString.new(B(%w{ AB CD })) + encode_test B(%w{ 20 00 }), OpenSSL::ASN1::Constructive.new([], 0) + encode_test B(%w{ 21 00 }), OpenSSL::ASN1::Constructive.new([], 1, nil, :UNIVERSAL) + encode_test B(%w{ A1 00 }), OpenSSL::ASN1::Constructive.new([], 1, nil, :CONTEXT_SPECIFIC) + encode_test B(%w{ 21 04 04 02 AB CD }), OpenSSL::ASN1::Constructive.new([octet_string], 1) + obj = OpenSSL::ASN1::Constructive.new([octet_string], 1) + obj.indefinite_length = true + encode_test B(%w{ 21 80 04 02 AB CD 00 00 }), obj + # TODO: BC doesn't support decoding indef constructive asn1s from unsupported tag types. + # encode_decode_test B(%w{ 21 80 04 02 AB CD 00 00 }), obj + obj = OpenSSL::ASN1::Constructive.new([octet_string, OpenSSL::ASN1::EndOfContent.new], 1) + obj.indefinite_length = true + encode_test B(%w{ 21 80 04 02 AB CD 00 00 }), obj end def test_constructive