diff --git a/test/lib-test/jdk/test/lib/hexdump/ASN1FormatterTest.java b/test/lib-test/jdk/test/lib/hexdump/ASN1FormatterTest.java
index 6bcd9dc2a9a50..f49a68921b917 100644
--- a/test/lib-test/jdk/test/lib/hexdump/ASN1FormatterTest.java
+++ b/test/lib-test/jdk/test/lib/hexdump/ASN1FormatterTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -32,6 +32,8 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;
+import java.util.HexFormat;
+import java.util.Set;
import static org.junit.jupiter.api.Assertions.*;
@@ -110,12 +112,32 @@ void testIndefinite() {
assertEquals(1, result.lines().filter(s -> s.contains("OCTET STRING [INDEFINITE]")).count(),
"Indefinite length");
- assertEquals(2, result.lines().filter(s -> s.contains("; OCTET STRING [2]")).count(),
+ assertEquals(2, result.lines().filter(s -> s.contains("OCTET STRING [2]")).count(),
"Octet Sequences");
- assertEquals(1, result.lines().filter(s -> s.contains("; END-OF-CONTENT")).count(),
+ assertEquals(1, result.lines().filter(s -> s.contains("END-OF-CONTENT")).count(),
"end of content");
}
+ @Test
+ void testPositions() {
+ byte[] bytes = HexFormat.of().parseHex("3009040730050201050500");
+ HexPrinter p = HexPrinter.simple()
+ .formatter(ASN1Formatter.formatter(), "; ", 100);
+ String result = p.toString(bytes);
+ System.out.println(result);
+
+ assertTrue(result.contains("[0]: OCTET STRING [7] (try --drill=0)"));
+
+ p = HexPrinter.simple()
+ .formatter(ASN1Formatter.formatter(Set.of("0")), "; ", 100);
+ result = p.toString(bytes);
+ System.out.println(result);
+
+ assertFalse(result.contains("try --drill"));
+ assertTrue(result.contains("[0]: OCTET STRING [7]"));
+ assertTrue(result.contains("[0c0]: BYTE 5"));
+ }
+
@Test
void testMain() {
String file = "openssl.p12.pem";
diff --git a/test/lib/jdk/test/lib/hexdump/ASN1Formatter.java b/test/lib/jdk/test/lib/hexdump/ASN1Formatter.java
index 41b13ec67e3ce..c855d632cffc1 100644
--- a/test/lib/jdk/test/lib/hexdump/ASN1Formatter.java
+++ b/test/lib/jdk/test/lib/hexdump/ASN1Formatter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -29,12 +29,14 @@
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
-import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Arrays;
import java.util.Base64;
+import java.util.HashSet;
+import java.util.Set;
/**
* ASN.1 stream formatter; a debugging utility for visualizing the contents of an ASN.1 stream.
@@ -44,6 +46,10 @@
* with the {@code HexPrinter} as a formatter to display the ASN.1 tagged values
* with the corresponding bytes.
*
+ * The formatter can be configured with a set of drill paths, which will drill into
+ * the OCTET STRING or BIT STRING at the path to parse the content as another ASN.1
+ * stream.
+ *
* The formatter reads a single tag from the stream and prints a description
* of the tag and its contents. If the tag is a constructed tag, set or sequence,
* each of the contained tags is read and printed.
@@ -88,9 +94,26 @@
* // done
* }
* }
+ * As a standalone program, it can be launched with
+ *
+ * java ASN1Formatter [--drill=path[,path...]] [asn1file]
+ *
+ * It parses {@code asn1file} as a binary DER or BER encoded ASN.1 data block
+ * and print out both the HEX dump and annotations. If no file is supplied,
+ * the program reads from the standard input.
+ *
+ * When the program detects that the content of an OCTET STRING or BIT STRING
+ * might be an embedded DER or BER encoding, it prints out hints in the form
+ * of "try --drill=110". User can launch the program again with the option
+ * and the content will be displayed. Multiple drill-into path can be provided
+ * in comma-separated-value format.
+ *
+ * If {@code --no-dump} option is provided, the HEX dump will not be displayed.
*/
public class ASN1Formatter implements HexPrinter.Formatter {
+ private final Set drillPaths;
+
/**
* Returns an ASN1Formatter.
* @return an ASN1Formatter
@@ -99,10 +122,25 @@ public static ASN1Formatter formatter() {
return new ASN1Formatter();
}
+ /**
+ * Returns an ASN1Formatter that drills into OCTET STRING or BIT STRING
+ * content at the specified paths.
+ * @param drillPaths paths like {@code 110} that should be drilled into
+ * @return an ASN1Formatter
+ */
+ public static ASN1Formatter formatter(Set drillPaths) {
+ return new ASN1Formatter(drillPaths);
+ }
+
/**
* Create a ANS1Formatter.
*/
private ASN1Formatter() {
+ this.drillPaths = Set.of();
+ }
+
+ private ASN1Formatter(Set drillPaths) {
+ this.drillPaths = drillPaths;
}
/**
@@ -139,22 +177,30 @@ public String annotate(DataInputStream in) {
* @throws IOException if an I/O error occurs
*/
public void annotate(DataInputStream in, Appendable out) throws IOException {
- annotate(in, out, -1, "");
+ annotate(in, out, -1, true, "");
}
/**
* Read bytes from the stream and annotate the stream as ASN.1.
*
- * @param in a DataInputStream
+ * @param in a DataInputStream
* @param out an Appendable for the output
* @param available the number of bytes to read from the stream (if greater than zero)
+ * @param isRoot true for outermost call and drilling into OCTET STRING
* @param prefix a string to prefix each line of output, used for indentation
* @throws IOException if an I/O error occurs
*/
@SuppressWarnings("fallthrough")
- private int annotate(DataInputStream in, Appendable out, int available, String prefix) throws IOException {
+ private int annotate(DataInputStream in, Appendable out, int available,
+ boolean isRoot, String prefix) throws IOException {
int origAvailable = available;
+ int position = 0;
+ String currentPrefix;
while (available != 0 || origAvailable < 0) {
+ // When isRoot is true, there is only one ASN.1 value inside.
+ // Do not use a new prefix.
+ currentPrefix = isRoot ? prefix : (prefix + position);
+ position++;
// Read the tag
int tag = in.readByte() & 0xff;
available--;
@@ -196,7 +242,7 @@ private int annotate(DataInputStream in, Appendable out, int available, String p
// started out unknown; set available to the length of this tagged value
available = len;
}
- out.append(prefix); // start with indent
+ out.append('[').append(currentPrefix).append("]: "); // start with path
switch (tag) {
case TAG_EndOfContent: // End-of-contents octets; len == 0
out.append("END-OF-CONTENT");
@@ -223,12 +269,8 @@ private int annotate(DataInputStream in, Appendable out, int available, String p
available -= 8;
break;
default:
- byte[] bytes = new byte[len];
- int l = in.read(bytes);
- BigInteger big = new BigInteger(bytes);
- out.append("BIG INTEGER [" + len + "] ");
- out.append(big.toString());
- out.append(".");
+ in.readNBytes(len);
+ out.append("BIG INTEGER [" + len + "]");
available -= len;
break;
}
@@ -243,7 +285,30 @@ private int annotate(DataInputStream in, Appendable out, int available, String p
out.append(' ');
break;
- case TAG_OctetString:
+ case TAG_BitString:
+ case TAG_OctetString: {
+ out.append(String.format("%s [%d]", tagName(tag), len));
+ if (tag == TAG_BitString) {
+ in.read();
+ len--;
+ available--;
+ }
+ String drillPath = currentPrefix + "c";
+ if (drillPaths.contains(currentPrefix)) {
+ out.append(" encapsulates");
+ out.append(System.lineSeparator());
+ annotate(in, out, len, true, drillPath);
+ available -= len;
+ continue;
+ } else {
+ byte[] content = in.readNBytes(len);
+ available -= content.length;
+ if (looksLikeDrillable(content)) {
+ out.append(" (try --drill=").append(currentPrefix).append(")");
+ }
+ }
+ break;
+ }
case TAG_UtcTime:
case TAG_GeneralizedTime:
out.append(tagName(tag) + " [" + len + "] ");
@@ -300,37 +365,26 @@ private int annotate(DataInputStream in, Appendable out, int available, String p
out.append(' ');
available -= len;
break;
- case TAG_BitString:
- out.append(String.format("%s [%d]", tagName(tag), len));
- do {
- var skipped = (int) in.skip(len);
- len -= skipped;
- available -= skipped;
- } while (len > 0);
- break;
default: {
- if (tag == TAG_Sequence ||
- tag == TAG_Set ||
- isApplication(tag) ||
- isConstructed(tag)) {
- String lenStr = (len < 0) ? "INDEFINITE" : Integer.toString(len);
- // Handle nesting
- if (isApplication(tag)) {
- out.append(String.format("APPLICATION %d. [%s] {%n", tagType(tag), lenStr));
- } else {
- out.append(String.format("%s [%s]%n", tagName(tag), lenStr));
- }
- int remaining = annotate(in, out, len, prefix + " ");
+ String lenStr = (len < 0) ? "INDEFINITE" : Integer.toString(len);
+ // Handle nesting
+ if (isApplication(tag)) {
+ out.append(String.format("APPLICATION %d. [%s] {%n", tagType(tag), lenStr));
+ } else {
+ out.append(String.format("%s [%s]%n", tagName(tag), lenStr));
+ }
+ if (isConstructed(tag)) {
+ int remaining = annotate(in, out, len, false, currentPrefix);
if (len > 0) {
available -= len - remaining;
}
continue;
} else {
- // Any other tag not already handled, dump the bytes
- out.append(String.format("%s[%d]: ", tagName(tag), len));
- formatBytes(in, out, len);
- available -= len;
- break;
+ do {
+ int skipped = (int) in.skip(len);
+ len -= skipped;
+ available -= skipped;
+ } while (len > 0);
}
}
}
@@ -339,20 +393,20 @@ private int annotate(DataInputStream in, Appendable out, int available, String p
return available;
}
- /**
- * Reads bytes from the stream and annotates them as hexadecimal.
- * @param in an inputStream
- * @param out the Appendable for the formatted bytes
- * @param len the number of bytes to read
- * @throws IOException if an I/O error occurs
- */
- private void formatBytes(DataInputStream in, Appendable out, int len) throws IOException {
- int b = in.readByte() & 0xff;
- out.append(String.format("%02x", b));
- for (int i = 1; i < len; i++) {
- b = in.readByte() & 0xff;
- out.append(String.format(",%02x", b));
+ private static boolean looksLikeDrillable(byte[] bytes) {
+ int len = bytes.length;
+ if (len < 2) {
+ return false;
}
+ int b1 = bytes[1] & 0xff;
+ return switch (b1) {
+ case 0x80 -> true;
+ case 0x81 -> bytes.length >= 3 && (bytes[2] & 0xff) == len - 3;
+ case 0x82 -> bytes.length >= 4
+ && (((bytes[2] & 0xff) << 8) | (bytes[3] & 0xff)) == len - 4;
+ case 0x83, 0x84 -> false;
+ default -> b1 == len - 2;
+ };
}
/**
@@ -375,7 +429,7 @@ private String getString(DataInputStream in, int len, Charset charset) throws IO
* @return a String representation of the tag.
*/
private String tagName(int tag) {
- String tagString = (isConstructed(tag) ? "CONSTRUCTED " : "") + tagNames[tagType(tag)];
+ String tagString = (isConstructed(tag) ? "CONSTRUCTED " : "") + tagNames[tagType(tag)];
switch (tag & 0xc0) {
case TAG_APPLICATION:
return "APPLICATION " + tagString;
@@ -424,7 +478,8 @@ private static String oidName(byte[] bytes) {
Method findMatch = cl.getDeclaredMethod("findMatch", String.class);
Object oid = findMatch.invoke(null, noid);
return (oid == null) ? noid : noid + " (" + oid.toString() + ")";
- } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
+ } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException |
+ InvocationTargetException e) {
return noid;
}
}
@@ -565,18 +620,14 @@ private int tagType(int tag) {
* These values are the high order bits for the other kinds of tags.
*/
- /**
- * Returns true if the tag class is UNIVERSAL.
- */
+ /** Returns true if the tag class is UNIVERSAL. */
private boolean isUniversal(int tag) { return ((tag & 0xc0) == 0x0); }
- /**
- * Returns true if the tag class is APPLICATION.
- */
+ /** Returns true if the tag class is APPLICATION. */
private boolean isApplication(int tag) { return ((tag & 0xc0) == TAG_APPLICATION); }
- /** Returns true iff the CONSTRUCTED bit is set in the type tag. */
- private boolean isConstructed(int tag) { return ((tag & 0x20) == 0x20); }
+ /** Returns true iff the CONSTRUCTED bit is set in the type tag. */
+ private boolean isConstructed(int tag) { return ((tag & 0x20) == 0x20); }
// Names for tags.
private static final String[] tagNames = new String[] {
@@ -597,26 +648,40 @@ private int tagType(int tag) {
*/
public static void main(String[] args) {
if (args.length < 1) {
- System.out.println("Usage: ");
+ System.out.println("Usage: java ASN1Formatter [--drill=path[,path...]] [asn.1 file]");
return;
}
- ASN1Formatter fmt = ASN1Formatter.formatter();
- for (String file : args) {
- System.out.printf("%s%n", file);
- try (InputStream fis = Files.newInputStream(Path.of(file));
- BufferedInputStream is = new BufferedInputStream(fis);
- InputStream in = wrapIfBase64Mime(is)) {
-
- DataInputStream dis = new DataInputStream(in);
+ Set drillPaths = new HashSet<>();
+ boolean dump = true;
+ String file = null;
+ for (String arg : args) {
+ if (arg.startsWith("--drill=")) {
+ drillPaths.addAll(Arrays.asList(
+ arg.substring("--drill=".length()).split(",")));
+ } else if (arg.equals("--no-dump")) {
+ dump = false;
+ } else {
+ file = arg;
+ }
+ }
+ ASN1Formatter fmt = ASN1Formatter.formatter(drillPaths);
+ try (InputStream fis = file == null ? System.in : Files.newInputStream(Path.of(file));
+ BufferedInputStream is = new BufferedInputStream(fis);
+ InputStream in = wrapIfBase64Mime(is)) {
+
+ DataInputStream dis = new DataInputStream(in);
+ if (dump) {
HexPrinter p = HexPrinter.simple()
.dest(System.out)
- .formatter(ASN1Formatter.formatter(), "; ", 100);
+ .formatter(fmt, "; ", 100);
p.format(dis);
- } catch (EOFException eof) {
- System.out.println();
- } catch (IOException ioe) {
- System.out.printf("%s: %s%n", file, ioe);
+ } else {
+ System.out.println(fmt.annotate(dis));
}
+ } catch (EOFException eof) {
+ System.out.println();
+ } catch (IOException ioe) {
+ System.out.printf("%s: %s%n", file, ioe);
}
}