mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-08-09 00:04:55 +00:00
reject truncated and oversized tlv lengths when parsing keycard responses
This commit is contained in:
@@ -15,16 +15,39 @@ public class TinyBERTLV {
|
||||
private byte[] buffer;
|
||||
private int pos;
|
||||
|
||||
/**
|
||||
* Reads the length at the given offset, on one to four bytes. The returned length is guaranteed to be present in
|
||||
* the given buffer.
|
||||
*
|
||||
* @param buf the buffer to read from
|
||||
* @param off the offset of the length
|
||||
* @return the length, and the offset of the body following it
|
||||
* @throws IllegalArgumentException if the length is malformed, or declares more bytes than the buffer holds
|
||||
*/
|
||||
public static int[] readNum(byte[] buf, int off) {
|
||||
if(off >= buf.length) {
|
||||
throw new IllegalArgumentException("Truncated TLV: no length byte at offset " + off);
|
||||
}
|
||||
|
||||
int len = buf[off++] & 0xff;
|
||||
int lenlen = 0;
|
||||
|
||||
if((len & 0x80) == 0x80) {
|
||||
lenlen = len & 0x7f;
|
||||
if(lenlen < 1 || lenlen > 4 || (off + lenlen) > buf.length) {
|
||||
throw new IllegalArgumentException("Truncated TLV: length header of " + lenlen + " bytes is unsupported or exceeds the " + (buf.length - off) + " bytes remaining");
|
||||
}
|
||||
|
||||
len = readVal(buf, off, lenlen);
|
||||
}
|
||||
|
||||
return new int[]{len, off + lenlen};
|
||||
off += lenlen;
|
||||
|
||||
if(len < 0 || len > (buf.length - off)) {
|
||||
throw new IllegalArgumentException("Truncated TLV: declared length of " + Integer.toUnsignedString(len) + " exceeds the " + (buf.length - off) + " bytes remaining");
|
||||
}
|
||||
|
||||
return new int[]{len, off};
|
||||
}
|
||||
|
||||
public static int readVal(byte[] val, int off, int len) {
|
||||
@@ -76,7 +99,7 @@ public class TinyBERTLV {
|
||||
*
|
||||
* @param tag the tag to enter
|
||||
* @return the length of the TLV
|
||||
* @throws IllegalArgumentException if the next tag does not match the given one
|
||||
* @throws IllegalArgumentException if the next tag does not match the given one, or the TLV extends past the end of the buffer
|
||||
*/
|
||||
public int enterConstructed(int tag) throws IllegalArgumentException {
|
||||
checkTag(tag, readTag());
|
||||
@@ -88,7 +111,7 @@ public class TinyBERTLV {
|
||||
*
|
||||
* @param tag the tag to read
|
||||
* @return the body of the TLV
|
||||
* @throws IllegalArgumentException if the next tag does not match the given one
|
||||
* @throws IllegalArgumentException if the next tag does not match the given one, or the TLV extends past the end of the buffer
|
||||
*/
|
||||
public byte[] readPrimitive(int tag) throws IllegalArgumentException {
|
||||
checkTag(tag, readTag());
|
||||
@@ -148,9 +171,10 @@ public class TinyBERTLV {
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next tag. The current implementation only reads length on one and two bytes. Can be extended if needed.
|
||||
* Reads the next length. The current implementation reads lengths on one to four bytes. Can be extended if needed.
|
||||
*
|
||||
* @return the tag
|
||||
* @return the length
|
||||
* @throws IllegalArgumentException if the length is malformed, or declares more bytes than the buffer holds
|
||||
*/
|
||||
public int readLength() {
|
||||
int[] len = TinyBERTLV.readNum(buffer, pos);
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.sparrowwallet.sparrow.io.keycard;
|
||||
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
public class TinyBERTLVTest {
|
||||
@Test
|
||||
public void testFourByteLengthRejected() {
|
||||
//tag 0x80 declaring a ~2GiB body from six bytes of card response
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("80847fffffff"));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNegativeLengthRejected() {
|
||||
//a four byte length with the sign bit set
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("8084ffffffff"));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortBodyRejectedNotZeroPadded() {
|
||||
//five bytes declared, two supplied - previously returned three zero bytes of padding
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("80050102"));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongFormShortBodyRejectedNotZeroPadded() {
|
||||
//a long form length of one million, four bytes supplied - previously returned a megabyte of zero padding
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("8084000f424001020304"));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMissingLengthByteRejected() {
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("80"));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTruncatedLengthHeaderRejected() {
|
||||
//0x84 promises four length bytes, only one follows
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("808400"));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIndefiniteLengthRejected() {
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("8080"));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOversizedConstructedLengthRejected() {
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("a4847fffffff"));
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> tlv.enterConstructed(ApplicationInfo.TLV_APPLICATION_INFO_TEMPLATE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testShortFormLengthStillParses() {
|
||||
TinyBERTLV tlv = new TinyBERTLV(Utils.hexToBytes("80020102"));
|
||||
Assertions.assertArrayEquals(Utils.hexToBytes("0102"), tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongFormLengthStillParses() {
|
||||
//a 200 byte body, which requires the 0x81 length form
|
||||
byte[] body = new byte[200];
|
||||
for(int i = 0; i < body.length; i++) {
|
||||
body[i] = (byte)i;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
baos.write(ApplicationInfo.TLV_PUB_KEY);
|
||||
TinyBERTLV.writeNum(baos, body.length);
|
||||
baos.writeBytes(body);
|
||||
|
||||
TinyBERTLV tlv = new TinyBERTLV(baos.toByteArray());
|
||||
Assertions.assertArrayEquals(body, tlv.readPrimitive(ApplicationInfo.TLV_PUB_KEY));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUninitializedCardSelectResponseStillParses() {
|
||||
byte[] pubKey = new byte[65];
|
||||
pubKey[0] = 0x04;
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
writePrimitive(baos, ApplicationInfo.TLV_PUB_KEY, pubKey);
|
||||
|
||||
ApplicationInfo applicationInfo = new ApplicationInfo(baos.toByteArray());
|
||||
Assertions.assertFalse(applicationInfo.isInitializedCard());
|
||||
Assertions.assertArrayEquals(pubKey, applicationInfo.getSecureChannelPubKey());
|
||||
Assertions.assertTrue(applicationInfo.hasSecureChannelCapability());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitializedCardSelectResponseStillParses() {
|
||||
byte[] instanceUID = new byte[16];
|
||||
byte[] pubKey = new byte[65];
|
||||
pubKey[0] = 0x04;
|
||||
byte[] keyUID = new byte[32];
|
||||
|
||||
ByteArrayOutputStream template = new ByteArrayOutputStream();
|
||||
writePrimitive(template, ApplicationInfo.TLV_UID, instanceUID);
|
||||
writePrimitive(template, ApplicationInfo.TLV_PUB_KEY, pubKey);
|
||||
writePrimitive(template, TinyBERTLV.TLV_INT, new byte[] {0x03, 0x01});
|
||||
writePrimitive(template, TinyBERTLV.TLV_INT, new byte[] {0x05});
|
||||
writePrimitive(template, ApplicationInfo.TLV_KEY_UID, keyUID);
|
||||
writePrimitive(template, ApplicationInfo.TLV_CAPABILITIES, new byte[] {ApplicationInfo.CAPABILITIES_ALL});
|
||||
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
writePrimitive(baos, ApplicationInfo.TLV_APPLICATION_INFO_TEMPLATE, template.toByteArray());
|
||||
|
||||
ApplicationInfo applicationInfo = new ApplicationInfo(baos.toByteArray());
|
||||
Assertions.assertTrue(applicationInfo.isInitializedCard());
|
||||
Assertions.assertEquals("3.1", applicationInfo.getAppVersionString());
|
||||
Assertions.assertEquals(5, applicationInfo.getFreePairingSlots());
|
||||
Assertions.assertArrayEquals(keyUID, applicationInfo.getKeyUID());
|
||||
Assertions.assertEquals(ApplicationInfo.CAPABILITIES_ALL, applicationInfo.getCapabilities());
|
||||
}
|
||||
|
||||
private void writePrimitive(ByteArrayOutputStream baos, byte tag, byte[] body) {
|
||||
baos.write(tag);
|
||||
TinyBERTLV.writeNum(baos, body.length);
|
||||
baos.writeBytes(body);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user