This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/11.0.x by this push:
     new 0451e2f7f5 Review "Fix DER INTEGER encoding in 
Asn1Writer.writeInteger()"
0451e2f7f5 is described below

commit 0451e2f7f54acb60183f5da2d576ec8ed3b6371a
Author: Mark Thomas <[email protected]>
AuthorDate: Mon Nov 2 16:38:29 2026 +0000

    Review "Fix DER INTEGER encoding in Asn1Writer.writeInteger()"
    
    Ensure lengths are written with the minimum number of bytes
    
    Co-authored by: Claude / Sonnet 5.5
---
 java/org/apache/tomcat/util/buf/Asn1Writer.java    | 46 +++++++------
 .../org/apache/tomcat/util/buf/TestAsn1Writer.java | 76 ++++++++++++++++++++++
 2 files changed, 98 insertions(+), 24 deletions(-)

diff --git a/java/org/apache/tomcat/util/buf/Asn1Writer.java 
b/java/org/apache/tomcat/util/buf/Asn1Writer.java
index 838bc2842b..2c9e22cff5 100644
--- a/java/org/apache/tomcat/util/buf/Asn1Writer.java
+++ b/java/org/apache/tomcat/util/buf/Asn1Writer.java
@@ -106,37 +106,35 @@ public class Asn1Writer {
      */
     public static byte[] writeTag(byte tagId, byte[] data) {
         int dataSize = data.length;
-        // How many bytes to write the length?
-        int lengthSize = 1;
-        if (dataSize > 127) {
-            // 1 byte we have is now used to record how many bytes we need to
-            // record a length > 127
-            // Result is lengthSize = 1 + number of bytes to record length
-            // Note the limit is required to terminate the loop since the shift
-            // distance is evaluated modulo 32. Four bytes is the most any Java
-            // array length (a non-negative int) can need.
-            do {
-                lengthSize++;
-            } while (lengthSize < 5 && (dataSize >>> ((lengthSize - 1) * 8)) > 
0);
-        }
 
-        // 1 for tag + lengthSize + dataSize
-        byte[] result = new byte[1 + lengthSize + dataSize];
-        result[0] = tagId;
+        byte[] result;
+        int headerSize;
         if (dataSize < 128) {
+            // Short form. The length is a single byte.
+            headerSize = 2;
+            result = new byte[headerSize + dataSize];
             result[1] = (byte) dataSize;
         } else {
-            // lengthSize is 1 + number of bytes for length
-            result[1] = (byte) (127 + lengthSize);
-            int i = lengthSize;
-            while (dataSize > 0) {
-                result[i] = (byte) (dataSize & 0xFF);
-                dataSize = dataSize >> 8;
-                i--;
+            /*
+             * Long form. The first length byte is 0x80 plus the number of 
bytes required to record the length. DER
+             * (X.690 10.1) requires the minimum number of length bytes. No 
more than 4 are required for a Java array
+             * length. The limit is also required to terminate the loop since 
the shift distance is evaluated modulo
+             * 32.
+             */
+            int lengthBytes = 1;
+            while (lengthBytes < 4 && (dataSize >>> (lengthBytes * 8)) != 0) {
+                lengthBytes++;
+            }
+            headerSize = 2 + lengthBytes;
+            result = new byte[headerSize + dataSize];
+            result[1] = (byte) (0x80 + lengthBytes);
+            for (int i = 0; i < lengthBytes; i++) {
+                result[2 + i] = (byte) (dataSize >>> (8 * (lengthBytes - 1 - 
i)));
             }
         }
+        result[0] = tagId;
 
-        System.arraycopy(data, 0, result, 1 + lengthSize, data.length);
+        System.arraycopy(data, 0, result, headerSize, data.length);
 
         return result;
     }
diff --git a/test/org/apache/tomcat/util/buf/TestAsn1Writer.java 
b/test/org/apache/tomcat/util/buf/TestAsn1Writer.java
index 517ce39462..95de40dff2 100644
--- a/test/org/apache/tomcat/util/buf/TestAsn1Writer.java
+++ b/test/org/apache/tomcat/util/buf/TestAsn1Writer.java
@@ -131,6 +131,82 @@ public class TestAsn1Writer {
     }
 
 
+    // DER (X.690 10.1) requires the length to be encoded using the minimum 
number of octets: the short form up to
+    // 127 and then the fewest length octets that can hold the value.
+    @Test
+    public void testWriteTagShortFormLengths() {
+        assertLengthEncoding(0, new byte[] { 0x04, 0x00 });
+        assertLengthEncoding(1, new byte[] { 0x04, 0x01 });
+        assertLengthEncoding(127, new byte[] { 0x04, 0x7F });
+    }
+
+
+    @Test
+    public void testWriteTagOneLengthOctet() {
+        assertLengthEncoding(128, new byte[] { 0x04, (byte) 0x81, (byte) 0x80 
});
+        assertLengthEncoding(200, new byte[] { 0x04, (byte) 0x81, (byte) 0xC8 
});
+        assertLengthEncoding(255, new byte[] { 0x04, (byte) 0x81, (byte) 0xFF 
});
+    }
+
+
+    @Test
+    public void testWriteTagTwoLengthOctets() {
+        assertLengthEncoding(256, new byte[] { 0x04, (byte) 0x82, 0x01, 0x00 
});
+        assertLengthEncoding(300, new byte[] { 0x04, (byte) 0x82, 0x01, 0x2C 
});
+        assertLengthEncoding(0xFFFF, new byte[] { 0x04, (byte) 0x82, (byte) 
0xFF, (byte) 0xFF });
+    }
+
+
+    @Test
+    public void testWriteTagThreeLengthOctets() {
+        assertLengthEncoding(0x10000, new byte[] { 0x04, (byte) 0x83, 0x01, 
0x00, 0x00 });
+        assertLengthEncoding(0xFFFFFF, new byte[] { 0x04, (byte) 0x83, (byte) 
0xFF, (byte) 0xFF, (byte) 0xFF });
+    }
+
+
+    @Test
+    public void testWriteTagLengthRoundTrip() {
+        // Every length around the short form, one octet and two octet 
boundaries and the transition to three octets.
+        // Check the size of the length field is the minimum and that the 
parser reads back the original content.
+        for (int size = 0; size <= 0x10100; size++) {
+            byte[] data = new byte[size];
+            Arrays.fill(data, (byte) 0x5A);
+            byte[] encoded = Asn1Writer.writeOctetString(data);
+
+            int lengthOctets;
+            if (size < 128) {
+                lengthOctets = 1;
+            } else {
+                // 1 octet for the number of length octets plus the unsigned 
minimal length octets
+                lengthOctets = 1 + (BigInteger.valueOf(size).bitLength() + 7) 
/ 8;
+            }
+            Assert.assertEquals("Unexpected total length for " + size, 1 + 
lengthOctets + size, encoded.length);
+
+            Assert.assertArrayEquals("Round-trip failed for " + size, data,
+                    new Asn1Parser(encoded).parseOctetString());
+        }
+    }
+
+
+    @Test
+    public void testWriteSequenceLengthEncoding() {
+        // A SEQUENCE of 200 bytes of content must use a single length octet
+        byte[] encoded = Asn1Writer.writeSequence(new byte[200]);
+        Assert.assertEquals(0x30, encoded[0] & 0xFF);
+        Assert.assertEquals(0x81, encoded[1] & 0xFF);
+        Assert.assertEquals(200, encoded[2] & 0xFF);
+        Assert.assertEquals(3 + 200, encoded.length);
+    }
+
+
+    private static void assertLengthEncoding(int size, byte[] expectedHeader) {
+        byte[] encoded = Asn1Writer.writeOctetString(new byte[size]);
+        Assert.assertEquals("Unexpected total length for " + size, 
expectedHeader.length + size, encoded.length);
+        Assert.assertArrayEquals("Unexpected header for " + size, 
expectedHeader,
+                Arrays.copyOfRange(encoded, 0, expectedHeader.length));
+    }
+
+
     private static void assertIntegerEncoding(int value, byte[] expected) {
         Assert.assertArrayEquals("Unexpected encoding for " + value, expected, 
Asn1Writer.writeInteger(value));
     }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to