Skip to content

Commit d2b0076

Browse files
committed
[feat] implement OpenSSL::ASN1.traverse helper
1 parent 91dd7f5 commit d2b0076

File tree

2 files changed

+191
-36
lines changed

2 files changed

+191
-36
lines changed

src/main/java/org/jruby/ext/openssl/ASN1.java

Lines changed: 166 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,8 +1216,8 @@ private static IRubyObject decodeImpl(final ThreadContext context, final RubyMod
12161216
}
12171217

12181218
@JRubyMethod(meta = true, required = 1)
1219-
public static IRubyObject decode_all(final ThreadContext context,
1220-
final IRubyObject self, IRubyObject obj) {
1219+
public static IRubyObject decode_all(final ThreadContext context, final IRubyObject self,
1220+
IRubyObject obj) {
12211221
obj = to_der_if_possible(context, obj);
12221222

12231223
BytesInputStream in = new BytesInputStream( obj.asString().getByteList() );
@@ -1239,10 +1239,170 @@ public static IRubyObject decode_all(final ThreadContext context,
12391239
return arr;
12401240
}
12411241

1242-
@JRubyMethod(meta = true, required = 1)
1243-
public static IRubyObject traverse(final ThreadContext context, final IRubyObject self, IRubyObject arg) {
1244-
warn(context, "WARNING: unimplemented method called: OpenSSL::ASN1#traverse");
1245-
return context.runtime.getNil();
1242+
@JRubyMethod(meta = true)
1243+
public static IRubyObject traverse(final ThreadContext context, final IRubyObject self,
1244+
IRubyObject arg, Block block) {
1245+
arg = to_der_if_possible(context, arg);
1246+
1247+
final ByteList byteList = arg.asString().getByteList();
1248+
final byte[] bytes = byteList.unsafeBytes();
1249+
final int begin = byteList.getBegin();
1250+
final int length = byteList.getRealSize();
1251+
1252+
final TraverseResult result = traverse(context, bytes, begin, begin + length, begin, 0, block);
1253+
if (length != 0 && result.read != length) {
1254+
throw newASN1Error(context.runtime,
1255+
"Type mismatch. Total bytes read: " + result.read +
1256+
" Bytes available: " + length + " Offset: " + result.offset);
1257+
}
1258+
return context.nil;
1259+
}
1260+
1261+
private static TraverseResult traverse(final ThreadContext context,
1262+
final byte[] bytes, final int begin, final int end,
1263+
final int base, final int depth,
1264+
final Block block) {
1265+
1266+
final Ruby runtime = context.runtime;
1267+
1268+
if (begin >= end) throw newASN1Error(runtime, "header too short");
1269+
1270+
int cursor = begin;
1271+
final int start = begin;
1272+
final int identifier = bytes[cursor++] & 0xFF;
1273+
final int tagClass = identifier & 0xC0;
1274+
final boolean constructed = (identifier & BERTags.CONSTRUCTED) != 0;
1275+
1276+
int tag = identifier & 0x1F;
1277+
if (tag == 0x1F) {
1278+
tag = 0;
1279+
if (cursor >= end) throw newASN1Error(runtime, "EOF found inside tag value");
1280+
1281+
int b = bytes[cursor++] & 0xFF;
1282+
if ((b & 0x7F) == 0) {
1283+
throw newASN1Error(runtime, "corrupted stream - invalid high tag number found");
1284+
}
1285+
1286+
while ((b & 0x80) != 0) {
1287+
tag |= (b & 0x7F);
1288+
tag <<= 7;
1289+
if (cursor >= end) throw newASN1Error(runtime, "EOF found inside tag value.");
1290+
b = bytes[cursor++] & 0xFF;
1291+
}
1292+
tag |= (b & 0x7F);
1293+
}
1294+
1295+
if (cursor >= end) throw newASN1Error(runtime, "header too short");
1296+
1297+
final int lengthByte = bytes[cursor++] & 0xFF;
1298+
final boolean indefinite = lengthByte == 0x80;
1299+
long contentLength = 0;
1300+
if (!indefinite) {
1301+
if ((lengthByte & 0x80) == 0) {
1302+
contentLength = lengthByte;
1303+
}
1304+
else {
1305+
final int lengthBytes = lengthByte & 0x7F;
1306+
if (lengthBytes == 0 || lengthBytes > 8) {
1307+
throw newASN1Error(runtime, "invalid length encoding");
1308+
}
1309+
if (cursor + lengthBytes > end) throw newASN1Error(runtime, "header too short");
1310+
for (int i = 0; i < lengthBytes; i++) {
1311+
contentLength = (contentLength << 8) | (bytes[cursor++] & 0xFFL);
1312+
}
1313+
}
1314+
if (contentLength > (long) end - cursor) {
1315+
throw newASN1Error(runtime, "value is too short");
1316+
}
1317+
}
1318+
1319+
final int headerLength = cursor - start;
1320+
if (block.isGiven()) {
1321+
block.yield(context,
1322+
runtime.newArray(
1323+
RubyBignum.bignorm(runtime, BigInteger.valueOf(depth)),
1324+
RubyBignum.bignorm(runtime, BigInteger.valueOf(start - base)),
1325+
RubyBignum.bignorm(runtime, BigInteger.valueOf(headerLength)),
1326+
RubyBignum.bignorm(runtime, BigInteger.valueOf(contentLength)),
1327+
runtime.newBoolean(constructed),
1328+
tagClassSymbol(runtime, tagClass),
1329+
RubyBignum.bignorm(runtime, BigInteger.valueOf(tag))
1330+
)
1331+
);
1332+
}
1333+
1334+
int read = headerLength;
1335+
int offset = (start - base) + headerLength;
1336+
1337+
if (constructed) {
1338+
if (indefinite) {
1339+
int available = end - cursor;
1340+
while (available > 0) {
1341+
final TraverseResult inner = traverse(context, bytes, cursor, end, base, depth + 1, block);
1342+
read += inner.read;
1343+
cursor += inner.read;
1344+
available -= inner.read;
1345+
offset = inner.offset;
1346+
1347+
if (inner.isEoc() && inner.tagClass == BERTags.UNIVERSAL) break;
1348+
if (available == 0) {
1349+
throw newASN1Error(context.runtime, "EOC missing in indefinite length encoding");
1350+
}
1351+
}
1352+
}
1353+
else {
1354+
final int contentEnd = cursor + (int) contentLength;
1355+
while (cursor < contentEnd) {
1356+
final TraverseResult inner = traverse(context, bytes, cursor, contentEnd, base, depth + 1, block);
1357+
read += inner.read;
1358+
cursor += inner.read;
1359+
offset = inner.offset;
1360+
}
1361+
}
1362+
}
1363+
else {
1364+
if (indefinite) throw newASN1Error(context.runtime, "indefinite length for primitive value");
1365+
read += contentLength;
1366+
offset += (int) contentLength;
1367+
}
1368+
1369+
if (!indefinite && read != headerLength + contentLength) {
1370+
throw newASN1Error(context.runtime,
1371+
"Type mismatch. Bytes read: " + read + " Bytes available: " + (headerLength + contentLength));
1372+
}
1373+
1374+
return new TraverseResult(read, offset, tag, tagClass);
1375+
}
1376+
1377+
private static IRubyObject tagClassSymbol(final Ruby runtime, final int tagClass) {
1378+
switch (tagClass) {
1379+
case BERTags.PRIVATE:
1380+
return runtime.newSymbol("PRIVATE");
1381+
case BERTags.APPLICATION:
1382+
return runtime.newSymbol("APPLICATION");
1383+
case BERTags.CONTEXT_SPECIFIC:
1384+
return runtime.newSymbol("CONTEXT_SPECIFIC");
1385+
default:
1386+
return runtime.newSymbol("UNIVERSAL");
1387+
}
1388+
}
1389+
1390+
private static final class TraverseResult {
1391+
private final int read;
1392+
private final int offset;
1393+
private final int tag;
1394+
private final int tagClass;
1395+
1396+
private TraverseResult(final int read, final int offset, final int tag, final int tagClass) {
1397+
this.read = read;
1398+
this.offset = offset;
1399+
this.tag = tag;
1400+
this.tagClass = tagClass;
1401+
}
1402+
1403+
boolean isEoc() {
1404+
return tag == 0;
1405+
}
12461406
}
12471407

12481408
public static RaiseException newASN1Error(Ruby runtime, String message) {

src/test/ruby/test_asn1.rb

Lines changed: 25 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -759,30 +759,26 @@ def test_recursive_octet_string_parse
759759
end
760760

761761
def test_decode_constructed_overread
762-
#test = %w{ 31 06 31 02 30 02 05 00 }
763-
## ^ <- invalid
764-
#raw = [test.join].pack("H*")
765-
#ret = []
766-
# <OpenSSL::ASN1::ASN1Error> exception was expected but none was thrown.
767-
#assert_raise(OpenSSL::ASN1::ASN1Error) {
768-
# OpenSSL::ASN1.traverse(raw) { |x| ret << x }
769-
#}
770-
# <2> expected but was <0>
771-
#assert_equal 2, ret.size
772-
# NoMethodError: undefined method `[]' for nil:NilClass
773-
#assert_equal 17, ret[0][6]
774-
#assert_equal 17, ret[1][6]
775-
776-
#test = %w{ 31 80 30 03 00 00 }
777-
## ^ <- invalid
778-
#raw = [test.join].pack("H*")
779-
#ret = []
780-
# <OpenSSL::ASN1::ASN1Error> exception was expected but none was thrown.
781-
#assert_raise(OpenSSL::ASN1::ASN1Error) {
782-
# OpenSSL::ASN1.traverse(raw) { |x| ret << x }
783-
#}
784-
#assert_equal 1, ret.size
785-
#assert_equal 17, ret[0][6]
762+
test = %w[31 06 31 02 30 02 05 00]
763+
# ^ <- invalid
764+
raw = [test.join].pack('H*')
765+
ret = []
766+
assert_raise(OpenSSL::ASN1::ASN1Error) do
767+
OpenSSL::ASN1.traverse(raw) { |x| ret << x }
768+
end
769+
assert_equal 2, ret.size
770+
assert_equal 17, ret[0][6]
771+
assert_equal 17, ret[1][6]
772+
773+
test = %w[31 80 30 03 00 00]
774+
# ^ <- invalid
775+
raw = [test.join].pack('H*')
776+
ret = []
777+
assert_raise(OpenSSL::ASN1::ASN1Error) do
778+
OpenSSL::ASN1.traverse(raw) { |x| ret << x }
779+
end
780+
assert_equal 1, ret.size
781+
assert_equal 17, ret[0][6]
786782
end
787783

788784
def test_constructive_nesting
@@ -1228,14 +1224,13 @@ def test_decode_all
12281224
def test_decode_application_specific
12291225
raw = "0\x18\x02\x01\x01`\x13\x02\x01\x03\x04\to=Telstra\x80\x03ess"
12301226
asn1 = OpenSSL::ASN1.decode(raw)
1231-
pp asn1 if false
12321227

12331228
assert_equal OpenSSL::ASN1::Sequence, asn1.class
12341229
assert_equal 2, asn1.value.size
12351230
assert_equal OpenSSL::ASN1::Integer, asn1.value[0].class
1236-
assert_equal 1, asn1.value[0].value
1231+
assert_equal 1, asn1.value[0].value
12371232
assert_equal OpenSSL::ASN1::ASN1Data, asn1.value[1].class
1238-
assert_equal :APPLICATION, asn1.value[1].tag_class
1233+
assert_equal :APPLICATION, asn1.value[1].tag_class
12391234

12401235
asn1_data = asn1.value[1]
12411236
assert_equal 3, asn1_data.value.size
@@ -1245,10 +1240,10 @@ def test_decode_application_specific
12451240
assert_equal OpenSSL::ASN1::OctetString, asn1_data.value[1].class
12461241
assert_equal 'o=Telstra', asn1_data.value[1].value
12471242
assert_equal OpenSSL::ASN1::ASN1Data, asn1_data.value[2].class
1248-
assert_equal :CONTEXT_SPECIFIC, asn1_data.value[2].tag_class
1243+
assert_equal :CONTEXT_SPECIFIC, asn1_data.value[2].tag_class
12491244
assert_equal 'ess', asn1_data.value[2].value
12501245

1251-
# assert_equal raw, asn1.to_der
1246+
# assert_equal raw, asn1.to_der
12521247
end
12531248

12541249

@@ -1312,7 +1307,7 @@ def encode_decode_test(der, obj)
13121307
decode_test(der, obj)
13131308
end
13141309

1315-
def assert_universal(tag, asn1, inf_len=false)
1310+
def assert_universal(tag, asn1, inf_len = false)
13161311
assert_equal(tag, asn1.tag)
13171312
assert_equal(:UNIVERSAL, asn1.tag_class)
13181313
assert_equal(inf_len, asn1.infinite_length)

0 commit comments

Comments
 (0)