From c3db4004935d6fabb1dadf377250fa64e1fa78b9 Mon Sep 17 00:00:00 2001 From: kares Date: Sat, 27 Apr 2024 18:46:31 +0200 Subject: [PATCH] [fix] encoding/decoding of all ASN1 string types --- src/main/java/org/jruby/ext/openssl/ASN1.java | 103 ++++++++++-------- src/test/ruby/test_asn1.rb | 33 ++---- 2 files changed, 65 insertions(+), 71 deletions(-) diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 840b06e5..e57642c8 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -31,9 +31,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.util.Date; import java.util.Enumeration; @@ -548,12 +547,12 @@ private static Map getSymLookup(final Ruby runtime { "NUMERICSTRING", org.bouncycastle.asn1.DERNumericString.class, "NumericString" }, { "PRINTABLESTRING", org.bouncycastle.asn1.DERPrintableString.class, "PrintableString" }, { "T61STRING", org.bouncycastle.asn1.DERT61String.class, "T61String" }, - { "VIDEOTEXSTRING", null, "VideotexString" }, + { "VIDEOTEXSTRING", org.bouncycastle.asn1.DERVideotexString.class, "VideotexString" }, { "IA5STRING", org.bouncycastle.asn1.DERIA5String.class, "IA5String" }, { "UTCTIME", org.bouncycastle.asn1.DERUTCTime.class, "UTCTime" }, { "GENERALIZEDTIME", org.bouncycastle.asn1.DERGeneralizedTime.class, "GeneralizedTime" }, - { "GRAPHICSTRING", null, "GraphicString" }, - { "ISO64STRING", null, "ISO64String" }, + { "GRAPHICSTRING", org.bouncycastle.asn1.DERGraphicString.class, "GraphicString" }, + { "ISO64STRING", org.bouncycastle.asn1.DERVisibleString.class, "ISO64String" }, { "GENERALSTRING", org.bouncycastle.asn1.DERGeneralString.class, "GeneralString" }, // OpenSSL::ASN1::UNIVERSALSTRING (28) : { "UNIVERSALSTRING", org.bouncycastle.asn1.DERUniversalString.class, "UniversalString" }, @@ -662,25 +661,6 @@ static Class typeClassSafe(final int typeId) { return typeClass(typeId); } - static ASN1Encodable typeInstance(Class type, Object value) - throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - Method getInstance = null; - try { - getInstance = type.getMethod("getInstance", Object.class); - } - catch (NoSuchMethodException e) { - Class superType = type.getSuperclass(); - try { - if ( superType != Object.class ) { - getInstance = type.getSuperclass().getMethod("getInstance", Object.class); - } - } - catch (NoSuchMethodException e2) { } - if ( getInstance == null ) throw e; - } - return (ASN1Encodable) getInstance.invoke(null, value); - } - public static void createASN1(final Ruby runtime, final RubyModule OpenSSL, final RubyClass OpenSSLError) { final RubyModule ASN1 = OpenSSL.defineModuleUnder("ASN1"); ASN1.defineClassUnder("ASN1Error", OpenSSLError, OpenSSLError.getAllocator()); @@ -976,7 +956,22 @@ static IRubyObject decodeObject(final ThreadContext context, final ByteList bytes; if ( obj instanceof ASN1UTF8String ) { if ( type == null ) type = "UTF8String"; - bytes = new ByteList(((ASN1UTF8String) obj).getString().getBytes("UTF-8"), false); + bytes = new ByteList(((ASN1UTF8String) obj).getString().getBytes(StandardCharsets.UTF_8), false); + } + else if ( obj instanceof ASN1UniversalString ) { + if ( type == null ) type = "UniversalString"; + bytes = new ByteList(((ASN1UniversalString) obj).getOctets(), false); + } + else if ( obj instanceof ASN1BMPString ) { + if ( type == null ) type = "BMPString"; + final String val = ((ASN1BMPString) obj).getString(); + final byte[] valBytes = new byte[val.length() * 2]; + for (int i = 0; i < val.length(); i++) { + char c = val.charAt(i); + valBytes[i * 2] = (byte) ((c >> 8) & 0xff); + valBytes[i * 2 + 1] = (byte) (c & 0xff); + } + bytes = new ByteList(valBytes, false); } else { if ( type == null ) { @@ -995,14 +990,16 @@ else if ( obj instanceof ASN1T61String ) { else if ( obj instanceof ASN1GeneralString ) { type = "GeneralString"; } - else if ( obj instanceof ASN1UniversalString ) { - type = "UniversalString"; + else if ( obj instanceof ASN1VideotexString ) { + type = "VideotexString"; } - else if ( obj instanceof ASN1BMPString ) { - type = "BMPString"; + else if ( obj instanceof ASN1VisibleString ) { + type = "ISO64String"; + } + else if ( obj instanceof ASN1GraphicString ) { + type = "GraphicString"; } else { - // NOTE "VideotexString", "GraphicString", "ISO64String" not-handled in BC ! throw new IllegalArgumentException("could not handle ASN1 string type: " + obj + " (" + obj.getClass().getName() + ")"); } } @@ -1652,38 +1649,50 @@ ASN1Encodable toASN1(final ThreadContext context) { return new DERUTF8String( val.asString().toString() ); } if ( type == DERBMPString.class ) { - return new DERBMPString( val.asString().toString() ); + return new DERBMPString(new String(toBMPChars(val.asString().getByteList()))); } if ( type == DERUniversalString.class ) { return new DERUniversalString( val.asString().getBytes() ); } if ( type == DERGeneralString.class ) { - return ASN1GeneralString.getInstance( val.asString().getBytes() ); + return new DERGeneralString( val.asString().toString() ); } if ( type == DERVisibleString.class ) { - return ASN1VisibleString.getInstance( val.asString().getBytes() ); + return new DERVisibleString( val.asString().toString() ); } if ( type == DERNumericString.class ) { - return ASN1NumericString.getInstance( val.asString().getBytes() ); + return new DERNumericString( val.asString().toString() ); } - - if ( val instanceof RubyString ) { - try { - return typeInstance(type, ( (RubyString) val ).getBytes()); - } - catch (Exception e) { // TODO exception handling - debugStackTrace(context.runtime, e); - throw Utils.newError(context.runtime, context.runtime.getRuntimeError(), e); - } + if ( type == DERPrintableString.class ) { + return new DERPrintableString( val.asString().toString() ); + } + if ( type == DERT61String.class ) { + return new DERT61String( val.asString().toString() ); + } + if ( type == DERVideotexString.class ) { + return new DERVideotexString( val.asString().getBytes() ); + } + if ( type == DERGraphicString.class ) { + return new DERGraphicString( val.asString().getBytes() ); } - // TODO throw an exception here too? - if ( isDebug(context.runtime) ) { + if (isDebug(context.runtime)) { debug(this + " toASN1() could not handle class " + getMetaClass() + " and value " + val.inspect() + " (" + val.getClass().getName() + ")"); } - warn(context, "WARNING: unimplemented method called: OpenSSL::ASN1Data#toASN1 (" + type + ")"); - return null; + throw context.runtime.newNotImplementedError("unimplemented method called: OpenSSL::ASN1Data#toASN1 (" + type + ")"); + } + + private static char[] toBMPChars(final ByteList string) { + assert string.length() % 2 == 0; + + final int len = string.length() / 2; + final char[] chars = new char[len]; + for (int i = 0; i < len; i++) { + int si = i * 2; + chars[i] = (char)((string.get(si) << 8) | (string.get(si + 1) & 0xff)); + } + return chars; } private static BigInteger bigIntegerValue(final IRubyObject val) { diff --git a/src/test/ruby/test_asn1.rb b/src/test/ruby/test_asn1.rb index 4a9dd285..d0f12531 100644 --- a/src/test/ruby/test_asn1.rb +++ b/src/test/ruby/test_asn1.rb @@ -1220,31 +1220,16 @@ def test_string_basic } test.(4, OpenSSL::ASN1::OctetString) test.(12, OpenSSL::ASN1::UTF8String) - # TODO: Import Issue - # The below tests cause NPEs in the first call to `encode_test` above - # org.jruby.ext.openssl.ASN1$Primitive.toDER(ASN1.java:1610) - # org.jruby.ext.openssl.ASN1$ASN1Data.to_der(ASN1.java:1414) - # org.jruby.ext.openssl.ASN1$Primitive.to_der(ASN1.java:1522) - # org.jruby.ext.openssl.ASN1$Primitive$INVOKER$i$0$0$to_der.call(ASN1$Primitive$INVOKER$i$0$0$to_der.gen) - #test.(18, OpenSSL::ASN1::NumericString) - #test.(19, OpenSSL::ASN1::PrintableString) - #test.(20, OpenSSL::ASN1::T61String) - #test.(21, OpenSSL::ASN1::VideotexString) + test.(18, OpenSSL::ASN1::NumericString) + test.(19, OpenSSL::ASN1::PrintableString) + test.(20, OpenSSL::ASN1::T61String) + test.(21, OpenSSL::ASN1::VideotexString) test.(22, OpenSSL::ASN1::IA5String) - # See above - #test.(25, OpenSSL::ASN1::GraphicString) - #test.(26, OpenSSL::ASN1::ISO64String) - #test.(27, OpenSSL::ASN1::GeneralString) - - # TODO: Import Issue - # This fails with: - # <""> expected but was <"#1C00"> - #test.(28, OpenSSL::ASN1::UniversalString) - - # TODO: Import Issue - # This fails with: - # <"\x1E\x02\x00\x01">(US-ASCII) expected but was <"\x1E\x04\x00\x00\x00\x01">(ASCII-8BIT) - #test.(30, OpenSSL::ASN1::BMPString) + test.(25, OpenSSL::ASN1::GraphicString) + test.(26, OpenSSL::ASN1::ISO64String) + test.(27, OpenSSL::ASN1::GeneralString) + test.(28, OpenSSL::ASN1::UniversalString) + test.(30, OpenSSL::ASN1::BMPString) end def test_constructive_each