From d342a45d98c4795b3a3fe1aaef5236ad4a782b55 Mon Sep 17 00:00:00 2001 From: Yuuta Liang Date: Thu, 12 Oct 2023 12:10:33 +0800 Subject: Implement data structures from X.680, X.501, X.509, and PKCS#10, with X.690 encoding / decoding support The implementation took four days, and it is still a little bit rough. Updated version should arrive soon. Signed-off-by: Yuuta Liang --- src/test/model/asn1/ASN1LengthTest.java | 94 ++++++++++++ src/test/model/asn1/ASN1ObjectTest.java | 177 +++++++++++++++++++++++ src/test/model/asn1/BitStringTest.java | 75 ++++++++++ src/test/model/asn1/BoolTest.java | 45 ++++++ src/test/model/asn1/GeneralizedTimeTest.java | 119 +++++++++++++++ src/test/model/asn1/IA5StringTest.java | 79 ++++++++++ src/test/model/asn1/IntTest.java | 96 ++++++++++++ src/test/model/asn1/NullTest.java | 38 +++++ src/test/model/asn1/ObjectIdentifierTest.java | 89 ++++++++++++ src/test/model/asn1/OctetStringTest.java | 44 ++++++ src/test/model/asn1/PrintableStringTest.java | 71 +++++++++ src/test/model/asn1/TagClassTest.java | 15 ++ src/test/model/asn1/TagTest.java | 107 ++++++++++++++ src/test/model/asn1/UTF8StringTest.java | 64 ++++++++ src/test/model/asn1/UtcTimeTest.java | 121 ++++++++++++++++ src/test/model/asn1/parsing/BytesReaderTest.java | 95 ++++++++++++ 16 files changed, 1329 insertions(+) create mode 100644 src/test/model/asn1/ASN1LengthTest.java create mode 100644 src/test/model/asn1/ASN1ObjectTest.java create mode 100644 src/test/model/asn1/BitStringTest.java create mode 100644 src/test/model/asn1/BoolTest.java create mode 100644 src/test/model/asn1/GeneralizedTimeTest.java create mode 100644 src/test/model/asn1/IA5StringTest.java create mode 100644 src/test/model/asn1/IntTest.java create mode 100644 src/test/model/asn1/NullTest.java create mode 100644 src/test/model/asn1/ObjectIdentifierTest.java create mode 100644 src/test/model/asn1/OctetStringTest.java create mode 100644 src/test/model/asn1/PrintableStringTest.java create mode 100644 src/test/model/asn1/TagClassTest.java create mode 100644 src/test/model/asn1/TagTest.java create mode 100644 src/test/model/asn1/UTF8StringTest.java create mode 100644 src/test/model/asn1/UtcTimeTest.java create mode 100644 src/test/model/asn1/parsing/BytesReaderTest.java (limited to 'src/test/model/asn1') diff --git a/src/test/model/asn1/ASN1LengthTest.java b/src/test/model/asn1/ASN1LengthTest.java new file mode 100644 index 0000000..44aed8e --- /dev/null +++ b/src/test/model/asn1/ASN1LengthTest.java @@ -0,0 +1,94 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.asn1.parsing.BytesReaderTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ASN1LengthTest { + @Test + void testConstructor() { + assertEquals(0, new ASN1Length(0).getLength()); + assertEquals(1, new ASN1Length(1).getLength()); + } + + @Test + void testParse() throws Exception { // TODO: Exception + BytesReader reader = new BytesReader(new Byte[]{ 0x0 }); + assertEquals(0, new ASN1Length(reader).getLength()); + assertEquals(1, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ 0x20 }); + assertEquals(0x20, new ASN1Length(reader).getLength()); + assertEquals(1, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -127, 0x30 }); + assertEquals(0x30, new ASN1Length(reader).getLength()); + assertEquals(2, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -126, -22, 0x60 }); + assertEquals(60000, new ASN1Length(reader).getLength()); + assertEquals(3, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -127, 127 }); + assertEquals(127, new ASN1Length(reader).getLength()); + assertEquals(2, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -124, 1, 1, 1, 1 }); + assertEquals(16843009, new ASN1Length(reader).getLength()); + assertEquals(5, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -127, -97 }); + assertEquals(159, new ASN1Length(reader).getLength()); + assertEquals(2, reader.getIndex()); + } + + @Test + void testParseFail() throws ParseException { + // First byte 0b10000000 + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -128 })) + ); + + // First byte 0b11111111 + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -1 })) + ); + + // Multibyte, requested 2 bytes but have none. + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -126 })) + ); + + // Multibyte, requested 2 bytes but have one. + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -126, 0x1 })) + ); + + // But this one should work (0b01111111) + new ASN1Length(new BytesReader(new Byte[]{ -127, 127 })); + + // Multibyte, too long. + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -124, -1, -1, -1, -1 })) + ); + // But this one should work, except for it is too large + new ASN1Length(new BytesReader(new Byte[]{ -125, -1, -1, -1 })); + } + + @Test + void testEncode() { + // Short form + assertArrayEquals(new Byte[]{ 0x0 }, new ASN1Length(0).encodeDER()); + assertArrayEquals(new Byte[]{ 0x1 }, new ASN1Length(1).encodeDER()); + assertArrayEquals(new Byte[]{ 127 }, new ASN1Length(127).encodeDER()); + + // Long form + // 0b10000001, 0b10000000 + assertArrayEquals(new Byte[]{ -127, -128 }, new ASN1Length(128).encodeDER()); + // 0b10000010, 0b11111111, 0b11111111 + assertArrayEquals(new Byte[]{ -126, -1, -1 }, new ASN1Length(65535).encodeDER()); + } +} diff --git a/src/test/model/asn1/ASN1ObjectTest.java b/src/test/model/asn1/ASN1ObjectTest.java new file mode 100644 index 0000000..ea765e6 --- /dev/null +++ b/src/test/model/asn1/ASN1ObjectTest.java @@ -0,0 +1,177 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public class ASN1ObjectTest { + @Test + void testParseType() throws ParseException { + assertEquals(Bool.class, + ASN1Object.parse(new BytesReader(new Bool(Bool.TAG, null, true).encodeDER()), + false).getClass()); + assertEquals(Int.class, + ASN1Object.parse(new BytesReader(new Int(Int.TAG, null, 1).encodeDER()), + false).getClass()); + assertEquals(BitString.class, + ASN1Object.parse(new BytesReader(new BitString(BitString.TAG, + null, 0, new Byte[]{ 1 }).encodeDER()), + false).getClass()); + assertEquals(OctetString.class, + ASN1Object.parse(new BytesReader(new OctetString(OctetString.TAG, null, new Byte[]{ 1 }).encodeDER()), + false).getClass()); + assertEquals(Null.class, + ASN1Object.parse(new BytesReader(new Null(Null.TAG, null).encodeDER()), + false).getClass()); + assertEquals(ObjectIdentifier.class, + ASN1Object.parse(new BytesReader(new ObjectIdentifier(ObjectIdentifier.TAG, null, + new Integer[]{ 1, 2, 3 }).encodeDER()), + false).getClass()); + assertEquals(UTF8String.class, + ASN1Object.parse(new BytesReader(new UTF8String(UTF8String.TAG, null, + "qwq").encodeDER()), + false).getClass()); + assertEquals(PrintableString.class, + ASN1Object.parse(new BytesReader(new PrintableString(PrintableString.TAG, null, + "qwq").encodeDER()), + false).getClass()); + assertEquals(IA5String.class, + ASN1Object.parse(new BytesReader(new IA5String(IA5String.TAG, null, + "qwq").encodeDER()), + false).getClass()); + assertEquals(UtcTime.class, + ASN1Object.parse(new BytesReader(new UtcTime(UtcTime.TAG, null, + ZonedDateTime.now(ZoneId.of("UTC"))).encodeDER()), + false).getClass()); + assertEquals(GeneralizedTime.class, + ASN1Object.parse(new BytesReader(new GeneralizedTime(GeneralizedTime.TAG, null, + ZonedDateTime.now(ZoneId.of("UTC"))).encodeDER()), + false).getClass()); + assertEquals(ASN1Object.class, + ASN1Object.parse(new BytesReader(new Byte[]{ 0x30, 1, 0x0 }), false) + .getClass()); + } + + @Test + void testConstructor() { + assertEquals(0, new ASN1Object(Null.TAG, null).getLength()); + assertNull(new ASN1Object(Null.TAG, null).encodeValueDER()); + assertEquals(0x5, + new ASN1Object(new Tag(TagClass.UNIVERSAL, false, 0x5), + null).getTag().getNumber()); + assertEquals(0x6, + new ASN1Object(new Tag(TagClass.UNIVERSAL, false, 0x5), + new Tag(TagClass.UNIVERSAL, false, 0x6)).getParentTag().getNumber()); + } + @Test + void testParseSuccess() throws ParseException { + // No parent tag + assertEquals(0x5, + new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getTag().getNumber()); + assertEquals(TagClass.UNIVERSAL, + new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getTag().getCls()); + assertFalse(new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getTag().isConstructive()); + assertNull(new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getParentTag()); + + assertEquals(0, new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .encodeValueDER().length); + assertEquals(0, new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getLength()); + + // With parent tag + // -95 is the 2's complement represent of 0b10100001 + assertEquals(0x5, + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getTag().getNumber()); + assertEquals(TagClass.UNIVERSAL, + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getTag().getCls()); + assertFalse(new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getTag().isConstructive()); + assertEquals(0x1, + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getParentTag().getNumber()); + assertEquals(TagClass.CONTEXT_SPECIFIC, + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getParentTag().getCls()); + assertTrue(new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getParentTag().isConstructive()); + + // Test index + BytesReader reader = new BytesReader(new Byte[]{ 0xE, 5, 1, 2, 3, 4, 5 }); + ASN1Object obj = new ASN1Object(reader, false); + // Contents should not be read. + assertEquals(2, reader.getIndex()); + // But is copied + assertArrayEquals(new Byte[]{ 1, 2, 3, 4, 5 }, obj.encodeValueDER()); + // If we parse an unknown type + reader = new BytesReader(new Byte[]{ 0xE, 5, 1, 2, 3, 4, 5 }); + obj = ASN1Object.parse(reader, false); + // Contents should be read now + assertEquals(7, reader.getIndex()); + } + + @Test + void testParseFail() { + // Value early EOF + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x1 }), false)); + // Tag early EOF + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 2 }), true)); + // Length not found + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ 0x5 }), false)); + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5 }), true)); + // Parent tag is not CONTEXT_SPECIFIC + // UNIVERSAL + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ 33, 2, 0x5, 0x0 }), true)); + // APPLICATION + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ 97, 2, 0x5, 0x0 }), true)); + // PRIVATE + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -31, 2, 0x5, 0x0 }), true)); + // Parent tag is not constructive + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -127, 2, 0x5, 0x0 }), true)); + // Parent tag length incorrect + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 0, 0x5, 0x0 }), true)); + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 1, 0x5, 0x0 }), true)); + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 3, 0x5, 0x0 }), true)); + } + + @Test + void testEncode() { + // No parent tag + assertArrayEquals(new Byte[] { + 0x5, 0x0 + }, new Null(Null.TAG, null).encodeDER()); + // Custom tag + assertArrayEquals(new Byte[] { + 0x72, 0x0 + }, new Null(new Tag(TagClass.APPLICATION, true, 0x12), null) + .encodeDER()); + // With parent tag + assertArrayEquals(new Byte[] { + -95, 2, + 0x72, 0x0 + }, new Null(new Tag(TagClass.APPLICATION, true, 0x12), + new Tag(TagClass.CONTEXT_SPECIFIC, true, 0x1)) + .encodeDER()); + } +} diff --git a/src/test/model/asn1/BitStringTest.java b/src/test/model/asn1/BitStringTest.java new file mode 100644 index 0000000..c893b36 --- /dev/null +++ b/src/test/model/asn1/BitStringTest.java @@ -0,0 +1,75 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class BitStringTest { + @Test + void testConstructor() { + assertArrayEquals(new Byte[]{ 0x2, 0x3 }, + new BitString(BitString.TAG, null, 0, new Byte[]{ 0x2, 0x3 }) + .getVal()); + assertEquals(3, + new BitString(BitString.TAG, null, 3, new Byte[]{ 0x2, 0x8 }) + .getUnused()); + } + + @Test + void testConvert() { + // 00000010 00001000 + // 00000000 01000001 = 65 + assertArrayEquals(new Byte[]{ 65 }, + new BitString(BitString.TAG, null, 3, new Byte[]{ 0x2, 0x8 }) + .getConvertedVal()); + + assertArrayEquals(new Byte[]{ 0x2, 0x8 }, + new BitString(BitString.TAG, null, 0, new Byte[]{ 0x2, 0x8 }) + .getConvertedVal()); + } + + @Test + void testParse() throws ParseException { + assertArrayEquals(new Byte[]{ 0x6e, 0x5d, -64 }, + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }), false) + .getVal()); + assertEquals(6, + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }), false) + .getUnused()); + assertArrayEquals(new Byte[]{ 0x01, -71, 0x77 }, + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }), false) + .getConvertedVal()); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04 }), false)); + // 0b11100000 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -32 }), false)); + // 0b11000001 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -63 }), false)); + // Unused bits = 8 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x08, 0x6e, 0x5d, -64 }), false)); + // Unused bits = 6 -> 7 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x07, 0x6e, 0x5d, -64 }), false)); + // Illegal unused bits: 8 and -1 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x08, 0x6e, 0x5d, -64 }), false)); + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, -1, 0x6e, 0x5d, -64 }), false)); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }, + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }), false) + .encodeDER()); + } +} diff --git a/src/test/model/asn1/BoolTest.java b/src/test/model/asn1/BoolTest.java new file mode 100644 index 0000000..fed3152 --- /dev/null +++ b/src/test/model/asn1/BoolTest.java @@ -0,0 +1,45 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class BoolTest { + @Test + void testConstructor() { + assertTrue(new Bool(Bool.TAG, null, true).getValue()); + assertFalse(new Bool(Bool.TAG, null, false).getValue()); + } + + @Test + void testParse() throws ParseException { + assertFalse(new Bool(new BytesReader(new Byte[]{ 0x1, 1, 0 }), false) + .getValue()); + assertTrue(new Bool(new BytesReader(new Byte[]{ 0x1, 1, -1 }), false) + .getValue()); + } + + @Test + void testParseFail() throws ParseException { + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 0 }), false)); + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 1 }), false)); + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 1, 1 }), false)); + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 1, -2 }), false)); + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 2, -1, 2 }), false)); + } + + @Test + void testEncode() { + assertArrayEquals(new Byte[]{ 0 }, + new Bool(Bool.TAG, null, false).encodeValueDER()); + assertArrayEquals(new Byte[]{ -1 }, + new Bool(Bool.TAG, null, true).encodeValueDER()); + } +} diff --git a/src/test/model/asn1/GeneralizedTimeTest.java b/src/test/model/asn1/GeneralizedTimeTest.java new file mode 100644 index 0000000..4660de7 --- /dev/null +++ b/src/test/model/asn1/GeneralizedTimeTest.java @@ -0,0 +1,119 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public class GeneralizedTimeTest { + @Test + void testConstructor() throws ParseException { + final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); + assertEquals(now, new GeneralizedTime(GeneralizedTime.TAG, null, now).getTimestamp()); + final ASN1Time parsed = new GeneralizedTime(new BytesReader(new Byte[] { + 0x18, 15, + '1', '9', '1', '9', '0', '8', '1', '0', '1', '1', '4', '5', '1', '4', 'Z' + }), false); + assertEquals("19190810114514Z", + parsed.toString()); + assertEquals(ZonedDateTime.of(1919, 8, 10, 11, 45, 14, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + } + + @Test + void testParse() throws ParseException { + ASN1Time parsed = new GeneralizedTime(new BytesReader(new Byte[] { + 0x18, 15, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), false); + assertEquals(ZonedDateTime.of(2023, 9, 27, 11, 45, 14, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + + // No seconds + parsed = new GeneralizedTime(new BytesReader(new Byte[] { + 0x18, 13, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }), false); + assertEquals(ZonedDateTime.of(2023, 9, 27, 11, 45, 0, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + + // Length 0 + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 0 + }), false)); + // Early EOF + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15 + }), false)); + // No tailing Z + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 14, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4' + }), false)); + // Custom timezone + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 18, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z', '-', '0', '8' + }), false)); + // Invalid month / day / hour / minute / second + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '3', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '0', '3', '2', '1', '1', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '0', '3', '0', '2', '5', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '0', '3', '0', '2', '4', '6', '1', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '0', '3', '0', '2', '4', '6', '0', '6', '1', 'Z' + }), false)); + } + + @Test + void testEncode() throws ParseException { + assertEquals("20230927114514Z", new GeneralizedTime(new BytesReader(new Byte[] { + -95, 17, + 0x18, 15, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), true).toString()); + // No seconds + assertEquals("202309271145Z", new GeneralizedTime(new BytesReader(new Byte[] { + -95, 15, + 0x18, 13, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }), true).toString()); + + // To byte array + assertArrayEquals(new Byte[] { + 0x18, 13, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }, new GeneralizedTime(GeneralizedTime.TAG, null, ZonedDateTime.of(2023, 9, + 27, 11, 45, 0, 0, ZoneId.of("UTC"))) + .encodeDER()); + } +} diff --git a/src/test/model/asn1/IA5StringTest.java b/src/test/model/asn1/IA5StringTest.java new file mode 100644 index 0000000..dfaa1aa --- /dev/null +++ b/src/test/model/asn1/IA5StringTest.java @@ -0,0 +1,79 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class IA5StringTest { + private String stringToTest = ""; + + @BeforeEach + void setup() { + stringToTest = IntStream.range(0, 127) + .mapToObj(Character::toString) + .collect(Collectors.joining()); + } + + @Test + void testAcceptedString() throws ParseException { + assertEquals(stringToTest, + new IA5String(IA5String.TAG, null, stringToTest) + .getString()); + } + + @Test + void testIllegalStrings() { + assertThrows(ParseException.class, + () -> new IA5String(IA5String.TAG, null, + stringToTest + new String(new byte[]{ -128 }, StandardCharsets.UTF_8))); + assertThrows(ParseException.class, + () -> new IA5String(IA5String.TAG, null, + stringToTest + new String(new byte[]{ -1 }, StandardCharsets.UTF_8))); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals( + new Byte[] { 0x00, 0x01, 0x02 }, + new IA5String(IA5String.TAG, null, new String(new byte[]{ 0, 1, 2}, StandardCharsets.UTF_8)) + .encodeValueDER()); + assertArrayEquals( + new Byte[] { + 0x16, 0x02, // Tag - Length + 0x68, 0x69 // Value + }, new IA5String(IA5String.TAG, null, "hi").encodeDER()); + assertArrayEquals( + new Byte[] { + -85, 0x05, // Parent Tag - Length + 0x16, 0x03, // Inner Tag - Length + 0x68, 0x69, 0x69 // Value + }, new IA5String(IA5String.TAG, + new Tag(TagClass.CONTEXT_SPECIFIC, true, 11), + "hii").encodeDER()); + } + + @Test + void testParse() throws ParseException { + assertEquals("123", + new IA5String(new BytesReader(new Byte[]{ 0x16, 3, '1', '2', '3' }), false) + .getString()); + assertEquals("", + new IA5String(new BytesReader(new Byte[]{ 0x16, 0 }), false) + .getString()); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> + new IA5String(new BytesReader(new Byte[]{ 0x16, 3, '1', '2' }), false)); + assertThrows(ParseException.class, () -> + new IA5String(new BytesReader(new Byte[]{ 0x16, 2, '1', -128 }), false)); + } +} diff --git a/src/test/model/asn1/IntTest.java b/src/test/model/asn1/IntTest.java new file mode 100644 index 0000000..6e92eb0 --- /dev/null +++ b/src/test/model/asn1/IntTest.java @@ -0,0 +1,96 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class IntTest { + @Test + void testConstructor() { + assertEquals(0x2, new Int(Int.TAG, null, 255).getTag().getNumber()); + assertEquals(255, new Int(Int.TAG, null, 255).getLong()); + } + + @Test + void testEncode() { + // Single-byte + assertArrayEquals(new Byte[] { 0x0 }, new Int(Int.TAG, null, 0).encodeValueDER()); + assertArrayEquals(new Byte[] { 0x1 }, new Int(Int.TAG, null, 1).encodeValueDER()); + assertArrayEquals(new Byte[] { -1 }, new Int(Int.TAG, null, 255).encodeValueDER()); + + // Multiple bytes + assertArrayEquals(new Byte[] { 0x01, 0x00 }, new Int(Int.TAG, null, 256).encodeValueDER()); + assertArrayEquals(new Byte[] { -1, -1 }, new Int(Int.TAG, null, 65535).encodeValueDER()); + assertArrayEquals(new Byte[] { 0x01, 0x00, 0x00 }, new Int(Int.TAG, null, 65536).encodeValueDER()); + assertArrayEquals(new Byte[] { -1, -1, -1 }, new Int(Int.TAG, null, 16777215).encodeValueDER()); + assertArrayEquals(new Byte[] { 0x01, 0x00, 0x00, 0x00 }, new Int(Int.TAG, null, 16777216).encodeValueDER()); + assertArrayEquals(new Byte[] { -1, -1, -1, -1 }, new Int(Int.TAG, null, 4294967295L).encodeValueDER()); + assertArrayEquals(new Byte[] { -1, -1, -1, -1 }, new Int(Int.TAG, null, 4294967295L).encodeValueDER()); + + // 2 ^ 63 + 1 + assertArrayEquals(new Byte[] { -128, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, + new Int(Int.TAG, null, Long.parseUnsignedLong("9223372036854775809")).encodeValueDER()); + + // Test no leading zeros + // Not 0x00, 0xFF + assertArrayEquals(new Byte[] { -1 }, new Int(Int.TAG, null, 255).encodeValueDER()); + + // Test no leading ones + // Not 0xFF, 0x80 + assertArrayEquals(new Byte[] { -128 }, new Int(Int.TAG, null, -128).encodeValueDER()); + + // Encode DER + assertArrayEquals(new Byte[] { 0x02, 2, 0x01, 0x00 }, new Int(Int.TAG, null, 256).encodeDER()); + } + + @Test + void testParse() throws ParseException { + // Single-byte + assertEquals(0, new Int(new BytesReader(new Byte[] { 0x2, 1, 0x0 }), false).getLong()); + assertEquals(1, new Int(new BytesReader(new Byte[] { 0x2, 1, 0x1 }), false).getLong()); + assertEquals(-1, new Int(new BytesReader(new Byte[] { 0x2, 1, -1 }), false).getLong()); + + // Multiple bytes + assertEquals(256, + new Int(new BytesReader(new Byte[] { 0x2, 2, 0x01, 0x00 }), false).getLong()); + assertEquals(-1, + new Int(new BytesReader(new Byte[] { 0x2, 2, -1, -1 }), false).getLong()); + assertEquals(65536, + new Int(new BytesReader(new Byte[] { 0x2, 3, 0x01, 0x00, 0x00 }), false).getLong()); + assertEquals(-1, + new Int(new BytesReader(new Byte[] { 0x2, 3, -1, -1, -1 }), false).getLong()); + assertEquals(16777216, + new Int(new BytesReader(new Byte[] { 0x2, 4, 0x01, 0x00, 0x00, 0x00 }), false).getLong()); + assertEquals(-1, + new Int(new BytesReader(new Byte[] { 0x2, 4, -1, -1, -1, -1 }), false).getLong()); + assertEquals(4294967296L, + new Int(new BytesReader(new Byte[] { 0x2, 5, 0x01, 0x00, 0x00, 0x00, 0x00 }),false).getLong()); + + // 2 ^ 63 + 1 + assertEquals(Long.parseUnsignedLong("9223372036854775809"), + new Int(new BytesReader(new Byte[] { 0x2, 9, 0x00, -128, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }), + false).getValue().longValue()); + + // Test no leading zeros + // Not 0x00, 0xFF + assertEquals(255, new Int(new BytesReader(new Byte[] { 0x02, 0x2, 0x0, -1 }), false).getLong()); + + // Test no leading ones + // Not 0xFF, 0x80 + assertArrayEquals(new Byte[] { -128 }, new Int(Int.TAG, null, -128).encodeValueDER()); + } + + @Test + void testParseFail() { + // Not enough bytes + assertThrows(ParseException.class, () -> + new Int(new BytesReader(new Byte[]{ 0x2, 0x7, -1, -1, -1, -1, -1, -1 }), + false)); + // Zero len + assertThrows(ParseException.class, () -> + new Int(new BytesReader(new Byte[]{ 0x2, 0x0, -1, -1, -1, -1, -1, -1 }), + false)); + } +} diff --git a/src/test/model/asn1/NullTest.java b/src/test/model/asn1/NullTest.java new file mode 100644 index 0000000..4ec2c64 --- /dev/null +++ b/src/test/model/asn1/NullTest.java @@ -0,0 +1,38 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class NullTest { + @Test + void testConstructor() { + assertEquals(Null.TAG, new Null(Null.TAG, null).getTag()); + assertEquals(0x01, + new Null(Null.TAG, new Tag(TagClass.CONTEXT_SPECIFIC, true, 0x01)).getParentTag().getNumber()); + } + + @Test + void testEncode() { + assertEquals(0, new Null(Null.TAG, null).encodeValueDER().length); + assertArrayEquals(new Byte[] { + 0x5, 0x0 // Tag - Length + }, new Null(Null.TAG, null).encodeDER()); + } + + @Test + void testParse() throws ParseException { + new Null(new BytesReader(new Byte[]{ 0x5, 0x0 }), false); + new Null(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> + new Null(new BytesReader(new Byte[]{ 0x5, 0x2 }), false)); + assertThrows(ParseException.class, () -> + new Null(new BytesReader(new Byte[]{ 0x5, 0x2, 1, 1 }), false)); + } +} diff --git a/src/test/model/asn1/ObjectIdentifierTest.java b/src/test/model/asn1/ObjectIdentifierTest.java new file mode 100644 index 0000000..f6f1049 --- /dev/null +++ b/src/test/model/asn1/ObjectIdentifierTest.java @@ -0,0 +1,89 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ObjectIdentifierTest { + @Test + void testConstructor() { + assertArrayEquals(new Integer[]{ 1, 3, 6, 1, 4}, + new ObjectIdentifier(ObjectIdentifier.TAG, null, new Integer[]{ 1, 3, 6, 1, 4 }).getInts()); + assertArrayEquals(new Integer[]{ 1, 2, 3, 4, 5}, + new ObjectIdentifier(ObjectIdentifier.TAG, null, new Integer[]{ 1, 2, 3, 4, 5 }).getInts()); + } + + @Test + void testToString() { + assertEquals("1.3.6.1.4", + new ObjectIdentifier(ObjectIdentifier.TAG, null, new Integer[]{ 1, 3, 6, 1, 4 }).toString()); + assertEquals("1.2.3.4.5.6", + new ObjectIdentifier(ObjectIdentifier.TAG, null, new Integer[]{ 1, 2, 3, 4, 5, 6 }).toString()); + assertEquals("CN", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_CN).toString()); + assertEquals("SN", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SN).toString()); + assertEquals("C", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_C).toString()); + assertEquals("L", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_L).toString()); + assertEquals("O", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_O).toString()); + assertEquals("OU", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_OU).toString()); + assertEquals("DC", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_DC).toString()); + } + + @Test + void testEncode() { + assertArrayEquals(new Byte[]{ 0x55, 0x04, 0x0A }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_O).encodeValueDER()); + assertArrayEquals(new Byte[]{ 0x67, -127, 0x0C, 0x01, 0x02, 0x01 }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + new Integer[]{ 2, 23, 140, 1, 2, 1 }).encodeValueDER()); + assertArrayEquals(new Byte[]{ 0x2B, 0x06, 0x01, 0x04, 0x01, -126, -33, 0x13, 0x01, 0x01, 0x01 }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + new Integer[]{ 1, 3, 6, 1, 4, 1, 44947, 1, 1, 1 }).encodeValueDER()); + assertArrayEquals(new Byte[]{ 0x2A, -122, 0x48, -50, 0x3D, 0x02, 0x01 }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + new Integer[]{ 1, 2, 840, 10045, 2, 1 }).encodeValueDER()); + assertArrayEquals(new Byte[]{ 0x2A, -122, 0x48, -122, -9, 0x0D, 0x01, 0x01, 0x0B }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION).encodeValueDER()); + + } + + @Test + void testParse() throws ParseException { + assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 0x9, 0x2A, -122, 0x48, -122, -9, 0x0D, + 0x01, 0x01, 0x0B }),false).getInts()); + assertArrayEquals(new Integer[]{ 1, 2, 840, 10045, 2, 1 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 7, 0x2A, -122, 0x48, -50, 0x3D, 0x02, 0x01 }), + false).getInts()); + assertArrayEquals(new Integer[]{ 0, 2, 840, 10045, 2, 1 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 7, 2, -122, 0x48, -50, 0x3D, 0x02, 0x01 }), + false).getInts()); + assertArrayEquals(new Integer[]{ 2, 2, 840, 10045, 2, 1 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 7, 82, -122, 0x48, -50, 0x3D, 0x02, 0x01 }), + false).getInts()); + assertArrayEquals(new Integer[]{ 2, 42, 840, 10045, 2, 1 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 7, 122, -122, 0x48, -50, 0x3D, 0x02, 0x01 }), + false).getInts()); + assertArrayEquals(new Integer[]{ 1, 2, 840, 113549, 1, 9, 14 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x06, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, 0x01, + 0x09, 0x0E }), false).getInts()); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 0x0 }), false)); + assertThrows(ParseException.class, () -> + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 0x9, 0x2A, -122, 0x48, -122, -9, 0x0D, + 0x01, 0x01, -117 }), false)); + } +} diff --git a/src/test/model/asn1/OctetStringTest.java b/src/test/model/asn1/OctetStringTest.java new file mode 100644 index 0000000..9e6e8a9 --- /dev/null +++ b/src/test/model/asn1/OctetStringTest.java @@ -0,0 +1,44 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class OctetStringTest { + @Test + void testConstructor() { + assertArrayEquals(new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }, + new OctetString(OctetString.TAG, null, new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }) + .getBytes()); + assertArrayEquals(new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }, + new OctetString(OctetString.TAG, null, new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }) + .encodeValueDER()); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(new Byte[]{ 0x04, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }, + new OctetString(OctetString.TAG, null, new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }) + .encodeDER()); + } + + @Test + void testParse() throws ParseException { + assertArrayEquals(new Byte[]{ 0x0A, 0x0B, 0x0C }, + new OctetString(new BytesReader(new Byte[]{ 0x4, 3, 0x0A, 0x0B, 0x0C }), false) + .getBytes()); + } + + @Test + void testParseFail() { + // EOF + assertThrows(ParseException.class, () -> + new OctetString(new BytesReader(new Byte[]{ 0x4, 2, 0x0 }), false)); + } +} diff --git a/src/test/model/asn1/PrintableStringTest.java b/src/test/model/asn1/PrintableStringTest.java new file mode 100644 index 0000000..f46f400 --- /dev/null +++ b/src/test/model/asn1/PrintableStringTest.java @@ -0,0 +1,71 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class PrintableStringTest { + private final String ILLEGAL_CHARS_TO_TEST = + "<>\\[]{}@!*&^%$#_|`~"; + + @Test + void testAcceptedString() throws ParseException { + final PrintableString string = + new PrintableString(PrintableString.TAG, null, "0123456789abCdExYz '()+,-./:=?"); + assertEquals("0123456789abCdExYz '()+,-./:=?", string.getString()); + } + + @Test + void testIllegalStrings() { + ILLEGAL_CHARS_TO_TEST.chars() + .forEach(c -> + assertThrows(ParseException.class, + () -> new PrintableString(PrintableString.TAG, null, Character.toString(c)), + String.format("Expected failing validation by char '%c'.", c)) + ); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals( + new Byte[] { 0x68, 0x68, 0x68 }, + new PrintableString(PrintableString.TAG, null, "hhh").encodeValueDER()); + assertArrayEquals( + new Byte[] { + 0x13, 0x02, // Tag - Length + 0x68, 0x69 // Value + }, new PrintableString(PrintableString.TAG, null, "hi").encodeDER()); + assertArrayEquals( + new Byte[] { + -86, 0x05, // Parent Tag - Length + 0x13, 0x03, // Inner Tag - Length + 0x68, 0x69, 0x69 // Value + }, new PrintableString(PrintableString.TAG, + new Tag(TagClass.CONTEXT_SPECIFIC, true, 10), + "hii").encodeDER()); + } + + @Test + void testParse() throws ParseException { + assertEquals("123", + new PrintableString(new BytesReader(new Byte[]{ 0x13, 3, '1', '2', '3' }), false) + .getString()); + assertEquals("", + new PrintableString(new BytesReader(new Byte[]{ 0x13, 0, '1', '2', '3' }), false) + .getString()); + } + + @Test + void testParseFail() { + // EOF + assertThrows(ParseException.class, () -> + new PrintableString(new BytesReader(new Byte[]{ 0x13, 1 }), false)); + // Illegal chars + assertThrows(ParseException.class, () -> + new PrintableString(new BytesReader(new Byte[]{ 0x13, 2, '1', '*' }), false)); + assertThrows(ParseException.class, () -> + new PrintableString(new BytesReader(new Byte[]{ 0x13, 2, '1', '@' }), false)); + } +} diff --git a/src/test/model/asn1/TagClassTest.java b/src/test/model/asn1/TagClassTest.java new file mode 100644 index 0000000..d510618 --- /dev/null +++ b/src/test/model/asn1/TagClassTest.java @@ -0,0 +1,15 @@ +package model.asn1; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TagClassTest { + @Test + void testConstructor() { + assertEquals(TagClass.Values.UNIVERSAL, TagClass.UNIVERSAL.getVal()); + assertEquals(TagClass.Values.PRIVATE, TagClass.PRIVATE.getVal()); + assertEquals(TagClass.Values.CONTENT_SPECIFIC, TagClass.CONTEXT_SPECIFIC.getVal()); + assertEquals(TagClass.Values.APPLICATION, TagClass.APPLICATION.getVal()); + } +} diff --git a/src/test/model/asn1/TagTest.java b/src/test/model/asn1/TagTest.java new file mode 100644 index 0000000..02df91f --- /dev/null +++ b/src/test/model/asn1/TagTest.java @@ -0,0 +1,107 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TagTest { + @Test + void testConstructor() { + assertEquals(ASN1Object.TAG_SEQUENCE.getNumber(), + new Tag(TagClass.UNIVERSAL, false, ASN1Object.TAG_SEQUENCE.getNumber()).getNumber()); + assertEquals(ASN1Object.TAG_SEQUENCE.getCls(), + new Tag(TagClass.UNIVERSAL, false, ASN1Object.TAG_SEQUENCE.getNumber()).getCls()); + assertEquals(ASN1Object.TAG_SEQUENCE.isConstructive(), + new Tag(TagClass.UNIVERSAL, + ASN1Object.TAG_SEQUENCE.isConstructive(), + ASN1Object.TAG_SEQUENCE.getNumber()) + .isConstructive()); + } + + @Test + void testParseSuccess() throws ParseException { + // Test basic parsing + assertEquals(0x1, + new Tag(new BytesReader(new Byte[]{ 0x1 })).getNumber()); + assertEquals(TagClass.UNIVERSAL, + new Tag(new BytesReader(new Byte[]{ 0x1 })).getCls()); + assertFalse(new Tag(new BytesReader(new Byte[]{ 0x1 })).isConstructive()); + // Test basic parsing with different class + assertEquals(5, + new Tag(new BytesReader(new Byte[]{ 101 })).getNumber()); + assertEquals(TagClass.APPLICATION, + new Tag(new BytesReader(new Byte[]{ 101 })).getCls()); + assertTrue(new Tag(new BytesReader(new Byte[]{ 101 })).isConstructive()); + // Test different classes + assertEquals(TagClass.UNIVERSAL, + new Tag(new BytesReader(new Byte[]{ 1 })).getCls()); // 0b00000001 + assertEquals(TagClass.PRIVATE, + new Tag(new BytesReader(new Byte[]{ -63 })).getCls()); // 0b11000001 + assertEquals(TagClass.CONTEXT_SPECIFIC, + new Tag(new BytesReader(new Byte[]{ -127 })).getCls()); // 0b10000001 + assertEquals(TagClass.APPLICATION, + new Tag(new BytesReader(new Byte[]{ 65 })).getCls()); // 0b01000001 + // Test different numbers + assertEquals(0x10, + new Tag(new BytesReader(new Byte[]{ 0x10 })).getNumber()); + assertEquals(31, + new Tag(new BytesReader(new Byte[]{ 31 })).getNumber()); + // Test constructive bit + assertFalse(new Tag(new BytesReader(new Byte[]{ 0x1 })).isConstructive()); + assertTrue(new Tag(new BytesReader(new Byte[]{ 33 })).isConstructive()); + // Test modification + BytesReader reader = new BytesReader(new Byte[]{ 33 }); + assertEquals(0, reader.getIndex()); + new Tag(reader); + assertEquals(1, reader.getIndex()); + } + + @Test + void testParseFail() { + // No enough bytes + assertThrows(ParseException.class, () -> { + BytesReader reader = new BytesReader(new Byte[]{ 33 }); + reader.require(1, true); + new Tag(reader); + }); + // Number zero + assertThrows(ParseException.class, () -> new Tag(new BytesReader(new Byte[]{ 0 }))); + } + + @Test + void testEncode() { + // Basic encoding + assertArrayEquals(new Byte[]{ 1 }, new Tag(TagClass.UNIVERSAL, false, 1).encodeDER()); + assertArrayEquals(new Byte[]{ 31 }, new Tag(TagClass.UNIVERSAL, false, 31).encodeDER()); + // With different class + assertArrayEquals(new Byte[]{ -127 }, new Tag(TagClass.CONTEXT_SPECIFIC, false, 1).encodeDER()); + assertArrayEquals(new Byte[]{ -61 }, new Tag(TagClass.PRIVATE, false, 3).encodeDER()); + assertArrayEquals(new Byte[]{ 71 }, new Tag(TagClass.APPLICATION, false, 7).encodeDER()); + // With different constructive bit + assertArrayEquals(new Byte[]{ 63 }, new Tag(TagClass.UNIVERSAL, true, 31).encodeDER()); + } + + @Test + void testEnforce() { + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.UNIVERSAL, true, 9))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.UNIVERSAL, true, 11))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.UNIVERSAL, false, 10))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.APPLICATION, true, 10))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.PRIVATE, true, 10))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 10))); + } +} diff --git a/src/test/model/asn1/UTF8StringTest.java b/src/test/model/asn1/UTF8StringTest.java new file mode 100644 index 0000000..a2518ae --- /dev/null +++ b/src/test/model/asn1/UTF8StringTest.java @@ -0,0 +1,64 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +public class UTF8StringTest { + private static final String[] UTF8_CHARS = new String[] { + new String(new byte[]{ -16, -97, -113, -77, -17, -72, -113, -30, + -128, -115, -30, -102, -89, -17, -72, -113 }, StandardCharsets.UTF_8), + new String(new byte[]{ -16, -97, -112, -79 }, StandardCharsets.UTF_8) + }; + + @Test + void testAcceptedString() throws ParseException { + final ASN1String string = + new UTF8String(PrintableString.TAG, null, "0123456789abCdExYz '()+,-./:=?*@" + + String.join("", UTF8_CHARS)); + assertEquals("0123456789abCdExYz '()+,-./:=?*@" + String.join("", UTF8_CHARS), + string.getString()); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals( + new Byte[]{ + -16, -97, -113, -77, -17, -72, -113, -30, + -128, -115, -30, -102, -89, -17, -72, -113, + -16, -97, -112, -79 + }, new UTF8String(UTF8String.TAG, null, UTF8_CHARS[0] + UTF8_CHARS[1]) + .encodeValueDER()); + assertArrayEquals( + new Byte[] { + 0x0C, 6, // Tag - Length + 0x68, 0x69, // Value + -16, -97, -112, -79 + }, new UTF8String(UTF8String.TAG, null, "hi" + UTF8_CHARS[1]).encodeDER()); + } + + @Test + void testParse() throws ParseException { + assertEquals(UTF8_CHARS[0], + new UTF8String(new BytesReader(new Byte[]{ 0x0C, 16, + -16, -97, -113, -77, -17, -72, -113, -30, + -128, -115, -30, -102, -89, -17, -72, -113 + }), false).getString()); + assertEquals("", + new UTF8String(new BytesReader(new Byte[]{ 0x0C, 0, '1', '2', '3' }), false) + .getString()); + } + + @Test + void testParseFail() { + // EOF + assertThrows(ParseException.class, () -> + new UTF8String(new BytesReader(new Byte[]{ 0x0C, 1 }), false)); + } +} diff --git a/src/test/model/asn1/UtcTimeTest.java b/src/test/model/asn1/UtcTimeTest.java new file mode 100644 index 0000000..5ba7e13 --- /dev/null +++ b/src/test/model/asn1/UtcTimeTest.java @@ -0,0 +1,121 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public class UtcTimeTest { + @Test + void testConstructor() throws ParseException { + final ZonedDateTime now = ZonedDateTime.now(); + assertEquals(now, new UtcTime(UtcTime.TAG, null, now).getTimestamp()); + + final ASN1Time parsed = new UtcTime(new BytesReader(new Byte[] { + 0x17, 13, + '1', '9', '0', '8', '1', '0', '1', '1', '4', '5', '1', '4', 'Z' + }), false); + assertEquals("190810114514Z", + parsed.toString()); + assertEquals(ZonedDateTime.of(2019, 8, 10, 11, 45, 14, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + } + + @Test + void testParse() throws ParseException { + ASN1Time parsed = new UtcTime(new BytesReader(new Byte[] { + 0x17, 13, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), false); + + assertEquals(ZonedDateTime.of(2023, 9, 27, 11, 45, 14, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + + // No seconds + parsed = new UtcTime(new BytesReader(new Byte[] { + 0x17, 11, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }), false); + assertEquals(ZonedDateTime.of(2023, 9, 27, 11, 45, 0, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + + // Length 0 + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 0 + }), false)); + // Early EOF + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13 + }), false)); + // No tailing Z + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 12, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4' + }), false)); + // Custom timezone + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 18, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z', '-', '0', '8' + }), false)); + // Invalid month / day / hour / minute / second + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '3', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '0', '3', '2', '1', '1', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '0', '3', '0', '2', '5', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '0', '3', '0', '2', '4', '6', '1', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '0', '3', '0', '2', '4', '6', '0', '6', '1', 'Z' + }), false)); + } + + @Test + void testEncode() throws ParseException { + assertEquals("230927114514Z", new UtcTime(new BytesReader(new Byte[] { + -95, 15, + 0x17, 13, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), true).toString()); + // No seconds + assertEquals("2309271145Z", new UtcTime(new BytesReader(new Byte[] { + -95, 13, + 0x17, 11, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }), true).toString()); + + // To byte array + assertArrayEquals(new Byte[] { + 0x17, 11, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }, new UtcTime(UtcTime.TAG, null, ZonedDateTime.of(2023, 9, + 27, 11, 45, 0, 0, ZoneId.of("UTC"))) + .encodeDER()); + } +} diff --git a/src/test/model/asn1/parsing/BytesReaderTest.java b/src/test/model/asn1/parsing/BytesReaderTest.java new file mode 100644 index 0000000..3b63a79 --- /dev/null +++ b/src/test/model/asn1/parsing/BytesReaderTest.java @@ -0,0 +1,95 @@ +package model.asn1.parsing; + +import model.asn1.Tag; +import model.asn1.TagClass; +import model.asn1.exceptions.ParseException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class BytesReaderTest { + private BytesReader target; + + @BeforeEach + void setup() { + target = new BytesReader(new Byte[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + } + + @Test + void testConstructor() { + assertEquals(0, target.getIndex()); + assertEquals(10, target.getRawInput().length); + assertArrayEquals(new Byte[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, target.getRawInput()); + } + + @Test + void testBytesRemaining() { + assertEquals(10, target.bytesRemaining()); + target.read(5, true); + assertEquals(5, target.bytesRemaining()); + } + + @Test + void testRead() { + assertEquals(10, target.bytesRemaining()); + assertEquals(0, target.getIndex()); + assertArrayEquals(new Byte[]{ 1, 2 }, target.read(2, true)); + assertEquals(8, target.bytesRemaining()); + assertEquals(2, target.getIndex()); + assertArrayEquals(new Byte[]{ 3 }, target.read(1, false)); + assertEquals(8, target.bytesRemaining()); + assertEquals(2, target.getIndex()); + assertArrayEquals(new Byte[]{ 3, 4, 5, 6, 7 }, target.read(5, true)); + assertEquals(3, target.bytesRemaining()); + assertEquals(7, target.getIndex()); + assertArrayEquals(new Byte[]{ 8, 9, 10 }, target.read(3, false)); + assertEquals(3, target.bytesRemaining()); + assertEquals(7, target.getIndex()); + assertArrayEquals(new Byte[]{ 8, 9, 10 }, target.read(3, true)); + assertEquals(0, target.bytesRemaining()); + assertEquals(10, target.getIndex()); + } + + @Test + void testRequire() throws Exception { // TODO: Exception testing + assertEquals(10, target.bytesRemaining()); + assertEquals(0, target.getIndex()); + assertArrayEquals(new Byte[]{ 1, 2 }, target.require(2, true)); + assertArrayEquals(new Byte[]{ 3, 4, 5, 6, 7, 8, 9 }, target.require(7, true)); + assertArrayEquals(new Byte[]{ 10 }, target.require(1, false)); + assertThrows(ParseException.class, () -> target.require(2, true)); + assertArrayEquals(new Byte[]{ 10 }, target.require(1, true)); + assertThrows(ParseException.class, () -> target.require(1, true)); + } + + @Test + void testValidateSize() throws Exception { // TODO: Exception testing + assertEquals(10, target.bytesRemaining()); + assertEquals(0, target.getIndex()); + assertArrayEquals(new Byte[]{ 1, 2 }, target.require(2, true)); + assertArrayEquals(new Byte[]{ 3, 4, 5, 6, 7, 8, 9 }, target.require(7, true)); + assertArrayEquals(new Byte[]{ 10 }, target.require(1, false)); + target.validateSize(1); + assertThrows(ParseException.class, () -> target.validateSize(2)); + assertArrayEquals(new Byte[]{ 10 }, target.require(1, true)); + assertThrows(ParseException.class, () -> target.validateSize(1)); + } + + @Test + void testDetectTag() throws Exception { + final BytesReader reader = new BytesReader(new Byte[]{ -95, 0 }); + assertTrue(reader.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, true, 1))); + assertFalse(reader.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, true, 0))); + assertFalse(reader.detectTag(new Tag(TagClass.UNIVERSAL, true, 0))); + assertFalse(reader.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, false, 0))); + } + + @Test + void testGetTag() throws Exception { + BytesReader reader = new BytesReader(new Byte[]{ -95, 0 }); + assertEquals(1, reader.getTag(false).getNumber()); + reader = new BytesReader(new Byte[]{ -96, 0, -95, 0 }); + assertEquals(1, reader.getTag(true).getNumber()); + } +} -- cgit v1.2.3