aboutsummaryrefslogtreecommitdiff
path: root/src/main/model/asn1
diff options
context:
space:
mode:
authorYuuta Liang <yuutaw@students.cs.ubc.ca>2023-10-12 12:10:33 +0800
committerYuuta Liang <yuutaw@students.cs.ubc.ca>2023-10-12 12:10:33 +0800
commitd342a45d98c4795b3a3fe1aaef5236ad4a782b55 (patch)
treef4ebc0ad962b138d9371413fcc71c97a559df506 /src/main/model/asn1
parente60c9c76243cfe0a408af98dc60bedb973e815db (diff)
downloadjca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar
jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar.gz
jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar.bz2
jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.zip
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 <yuutaw@students.cs.ubc.ca>
Diffstat (limited to 'src/main/model/asn1')
-rw-r--r--src/main/model/asn1/ASN1Length.java101
-rw-r--r--src/main/model/asn1/ASN1Object.java201
-rw-r--r--src/main/model/asn1/ASN1String.java85
-rw-r--r--src/main/model/asn1/ASN1Time.java66
-rw-r--r--src/main/model/asn1/BitString.java95
-rw-r--r--src/main/model/asn1/Bool.java60
-rw-r--r--src/main/model/asn1/Encodable.java9
-rw-r--r--src/main/model/asn1/GeneralizedTime.java90
-rw-r--r--src/main/model/asn1/IA5String.java53
-rw-r--r--src/main/model/asn1/Int.java81
-rw-r--r--src/main/model/asn1/Null.java45
-rw-r--r--src/main/model/asn1/ObjectIdentifier.java204
-rw-r--r--src/main/model/asn1/OctetString.java47
-rw-r--r--src/main/model/asn1/PrintableString.java49
-rw-r--r--src/main/model/asn1/Tag.java109
-rw-r--r--src/main/model/asn1/TagClass.java42
-rw-r--r--src/main/model/asn1/UTF8String.java40
-rw-r--r--src/main/model/asn1/UtcTime.java91
-rw-r--r--src/main/model/asn1/exceptions/ParseException.java10
-rw-r--r--src/main/model/asn1/parsing/BytesReader.java105
20 files changed, 1583 insertions, 0 deletions
diff --git a/src/main/model/asn1/ASN1Length.java b/src/main/model/asn1/ASN1Length.java
new file mode 100644
index 0000000..e85689c
--- /dev/null
+++ b/src/main/model/asn1/ASN1Length.java
@@ -0,0 +1,101 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.util.Arrays;
+
+/**
+ * Represents the Length part in DER encoding. It appears after Tag and before Value. It represents the length of the
+ * encoded Value in bytes.
+ * For encoding, if the length is <= 127, it is encoded as a single byte, with the highest bit cleared. If it is > 127,
+ * the initial byte will have its highest bit set, with the remaining 7 bits representing how many of bytes in advance
+ * are needed to represent the multibyte length value. Then, following are the multibyte length value, encoded in a
+ * big-endian unsigned integer.
+ */
+public class ASN1Length implements Encodable {
+ /**
+ * The length. It is represented in Java signed 32bit integer, but it should be unsigned.
+ * Operations should use Integer#unsigned* methods.
+ */
+ private final int length;
+
+ /**
+ * EFFECTS: Initialize the object with the given length.
+ * REQUIRES: length >= 0
+ */
+ public ASN1Length(int length) {
+ this.length = length;
+ }
+
+ /**
+ * EFFECTS: Parse the length from the given DER input.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Indefinite length
+ * - Not enough bytes
+ * - Initial byte 0b11111111 (See X.690$8.1.3.5)
+ * - Value too long (compliant to RFC but unsupported by this program): multibyte and # of bytes > 3
+ * MODIFIES: reader (bytes are read, at least one byte, at most 5 bytes)
+ */
+ public ASN1Length(BytesReader reader) throws ParseException {
+ final Byte first = reader.require(1, true)[0];
+ if (first < 0) {
+ // Multibyte
+ // 0b11111111
+ if (first == -1) {
+ throw new ParseException("The initial byte must not be 0xFF");
+ }
+ // Clear the sign bit and get the remaining.
+ int count = first & 127;
+ if (count == 0) {
+ throw new ParseException("Indefinite length is forbidden by DER");
+ }
+ final Byte[] values = reader.require(count, true);
+ // Pad one byte to the top - so it is always unsigned.
+ Byte[] b1 = new Byte[values.length + 1];
+ System.arraycopy(values, 0, b1, 1, values.length);
+ b1[0] = 0x0;
+ this.length = Utils.bytesToInt(b1);
+ } else {
+ this.length = first;
+ }
+ }
+
+ /**
+ * EFFECTS: Compute the length to add in the Tag - Length - Value format. For a detailed description on length, see
+ * class specification.
+ */
+ @Override
+ public Byte[] encodeDER() {
+ // Determine the length of the length.
+ // If the length is <= 127 bytes, use a single byte.
+ // If the length is > 127 bytes, set the highest bit as 1, and the
+ // rest of the bits specify how many more bytes the length is, followed
+ // by a sequence of bytes of the length.
+ // Setting the length 80 (0b10000000) means indefinite length, which is forbidden
+ // by DER.
+ // DER prefers the shortest form.
+ if (length <= 127) {
+ // Possible in a single byte.
+ return new Byte[]{ (byte) length };
+ } else {
+ // Big-endian encoding of the length. DER uses big-endian.
+ final Byte[] lengthBytes = Utils.valToByte(length);
+ final Byte[] result = new Byte[lengthBytes.length + 1];
+ // Turn-on the highest bit.
+ result[0] = (byte) (lengthBytes.length | -128);
+ // Append the length.
+ System.arraycopy(lengthBytes, 0,
+ result, 1, lengthBytes.length);
+ return result;
+ }
+ }
+
+ /**
+ * EFFECT: Returns the unsigned integer length.
+ */
+ public int getLength() {
+ return length;
+ }
+}
diff --git a/src/main/model/asn1/ASN1Object.java b/src/main/model/asn1/ASN1Object.java
new file mode 100644
index 0000000..1af26ce
--- /dev/null
+++ b/src/main/model/asn1/ASN1Object.java
@@ -0,0 +1,201 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Represents an encode-able ASN.1 object. It can be a SEQUENCE, an INTEGER, an OID, or any other ASN.1 type.
+ * It has a parent tag and length (if explicitly encoded), a tag and length, and a value.
+ * Child classes have specific parsed values available.
+ */
+public class ASN1Object implements Encodable {
+ /**
+ * The X.680 UNIVERSAL tag assignment for SEQUENCE and SEQUENCE OF. Because there is no such Java
+ * model class that represents an abstract ASN1Sequence, implementations may just use this constant
+ * as their default tag.
+ */
+ public static final Tag TAG_SEQUENCE = new Tag(TagClass.UNIVERSAL, true, 0x10);
+
+ /**
+ * The X.680 UNIVERSAL tag assignment for SET and SET OF. Because there is no such Java
+ * model class that represents an abstract ASN1Set, implementations may just use this constant
+ * as their default tag.
+ */
+ public static final Tag TAG_SET = new Tag(TagClass.UNIVERSAL, true, 0x11);
+
+ // The ASN.1 type tag.
+ private final Tag tag;
+
+ // The value length for implementation parsing purposes (only available if the object is parsed)
+ private final int length;
+
+ // The parsed raw value (only available if the object is parsed)
+ private final Byte[] value;
+
+ // The parent ASN.1 type tag, if required for EXPLICIT tagging with a CONTEXT SPECIFIC tag number.
+ private final Tag parentTag;
+
+ /**
+ * EFFECTS: Initiate the object with the given tag and an optional context-specific tag number for explicit
+ * encoding. It will always have 0 length and null value.
+ * By default, the tag should be the corresponding default tag specified in the constants
+ * of the corresponding types. However, applications may use context-specific
+ * or private tags for corresponding fields, either implicitly encoded or explicitly encoded.
+ * REQUIRES: Three cases:
+ * 1. No context-specific tag: parentTag must be null.
+ * 2. Implicit encoding: parentTag must be null, and the tag must be CONTEXT_SPECIFIC.
+ * 3. Explicit encoding: parentTag must be constructive and CONTEXT_SPECIFIC.
+ */
+ public ASN1Object(Tag tag, Tag parentTag) {
+ this.tag = tag;
+ this.parentTag = parentTag;
+ this.length = 0;
+ this.value = null;
+ }
+
+ /**
+ * EFFECTS: Init the object (tag, parentTag, length) with the specified input DER bytes. It will have length
+ * and value (length = 0 if no value in DER, but never null). It will fill the value and length but will not mark
+ * the value as read (only the tag will be marked). Subtypes are responsible for deserializing the values. This
+ * method is not appropriate for parsing an unknown input (use subtypes instead) since values will be left unread.
+ * Throws {@link ParseException} if input is invalid:
+ * The input data must have a valid
+ * parentTag (optional) - parentLength (optional) - tag - length - value (optional).
+ * The value must match the corresponding type (e.g., an INTEGER value cannot go to an OctetString type).
+ * The value must be supported by the corresponding type (e.g., a Printable must only contain valid chars).
+ * If parentTag presents, its class must be CONTEXT_SPECIFIC, and it must be constructive.
+ * If parentLength presents, it must not be 0.
+ * MODIFIES: this, encoded (bytes are read)
+ * REQUIRES: If hasParentTag is true, parentTag and parentLength must present. Otherwise, they must be null. Assumes
+ * that the length won't be lower than actual. Assumes parentLength = length(tag + length + value).
+ */
+ public ASN1Object(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ if (hasParentTag) {
+ this.parentTag = new Tag(encoded);
+ if (parentTag.getCls() != TagClass.CONTEXT_SPECIFIC) {
+ throw new ParseException("Parent tag must be CONTEXT_SPECIFIC, but found "
+ + parentTag.getCls() + "[" + parentTag.getNumber() + "].");
+ }
+ if (!parentTag.isConstructive()) {
+ throw new ParseException("Parent tag must be constructive.");
+ }
+ int parentLen = new ASN1Length(encoded).getLength();
+ // Validate length
+ encoded.validateSize(parentLen);
+ if (Integer.compareUnsigned(parentLen, 2) < 0) {
+ throw new ParseException("Parent tag with incorrect length.");
+ }
+ } else {
+ parentTag = null;
+ }
+ // len = the length of value; i = the length of tag - length
+ this.tag = new Tag(encoded);
+ int len = new ASN1Length(encoded).getLength();
+ this.length = len;
+ if (len == 0) {
+ this.value = new Byte[0];
+ } else {
+ this.value = encoded.require(len, false);
+ }
+ }
+
+ /**
+ * EFFECTS: Automatically detect the UNIVERSAL type and parse into the corresponding ASN1Object type, or ASN1Object
+ * if unrecognized or application-defined (SEQUENCE or SET). It will always mark anything to be read, including
+ * unrecognized type values. This method is appropriate to decode an unknown input stream into known or unknown
+ * types. All values will be read.
+ * Throws {@link ParseException} if the input is invalid.
+ * MODIFIES: encoded
+ */
+ public static ASN1Object parse(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ final Tag t = encoded.getTag(hasParentTag);
+ switch (t.getNumber()) {
+ case 0x1: return new Bool(encoded, hasParentTag);
+ case 0x2: return new Int(encoded, hasParentTag);
+ case 0x3: return new BitString(encoded, hasParentTag);
+ case 0x4: return new OctetString(encoded, hasParentTag);
+ case 0x5: return new Null(encoded, hasParentTag);
+ case 0x6: return new ObjectIdentifier(encoded, hasParentTag);
+ case 0xC: return new UTF8String(encoded, hasParentTag);
+ case 0x13: return new PrintableString(encoded, hasParentTag);
+ case 0x16: return new IA5String(encoded, hasParentTag);
+ case 0x17: return new UtcTime(encoded, hasParentTag);
+ case 0x18: return new GeneralizedTime(encoded, hasParentTag);
+ default: {
+ ASN1Object object = new ASN1Object(encoded, hasParentTag);
+ // Mark as read unconditionally because there aren't any type handlers that read them.
+ encoded.require(object.length, true);
+ return object;
+ }
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the object to DER bytes in the tag-length-value format, as specified in DER specs.
+ * The encoding will result in:
+ * (Parent Tag)(Tag)(Length)(Value)
+ * Parent Tag - Only exists if the field has a context-specific parent tag number and use explicit tagging. In this
+ * case, the parent tag is the tag supplied in the constructor. If the field uses implicit tag
+ * encoding or does not have a context-specific tag number, this field does not exist. This field,
+ * as specified in the REQUIRES clause in the constructor, is always constructive.
+ * Parent Length - The length of the following (tag, length, and value). A detailed length description, see follows.
+ * Tag - The tag value.
+ * Length - The length of the value, in number of bytes. If the length is <= 127, it will contain only a single
+ * byte of length value, with the highest bit cleared. If the length is > 127, the first length byte
+ * will have its highest bit set, with the remaining bits representing how many bytes are needed to
+ * store the length integer. Followed are the integer, in multiple bytes, representing the length. The
+ * multibyte integer are encoded in big-endian.
+ * Value - The value, with a total length (in bytes) corresponding to the Length field.
+ * REQUIRES: encodeValueDER() != null
+ */
+ @Override
+ public final Byte[] encodeDER() {
+ final Byte[] val = encodeValueDER();
+ final List<Byte> list = new ArrayList<>(val.length + 3);
+
+ list.addAll(Arrays.asList(tag.encodeDER()));
+ list.addAll(Arrays.asList(new ASN1Length(val.length).encodeDER()));
+ list.addAll(Arrays.asList(encodeValueDER()));
+
+ if (parentTag != null) { // Explicit
+ final List<Byte> newList = new ArrayList<>(list.size() + 3);
+ newList.addAll(Arrays.asList(parentTag.encodeDER()));
+ newList.addAll(Arrays.asList(new ASN1Length(list.size()).encodeDER()));
+ newList.addAll(list);
+ return newList.toArray(new Byte[0]);
+ } else {
+ return list.toArray(new Byte[0]);
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the value of that object to DER bytes. The length of the returned value
+ * is <= (255,255,255,...) (127 in total).
+ */
+ public Byte[] encodeValueDER() {
+ return value;
+ }
+
+ public Tag getTag() {
+ return tag;
+ }
+
+ public Tag getParentTag() {
+ return parentTag;
+ }
+
+ /**
+ * EFFECTS: Get the unsigned int of value length. Only available if the data is parsed.
+ */
+ public int getLength() {
+ return length;
+ }
+}
diff --git a/src/main/model/asn1/ASN1String.java b/src/main/model/asn1/ASN1String.java
new file mode 100644
index 0000000..148c564
--- /dev/null
+++ b/src/main/model/asn1/ASN1String.java
@@ -0,0 +1,85 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Represents an ASN.1 string with an optional character set restriction. Users should choose from one of its
+ * implementations that encode a specific ASN.1 string type (e.g., {@link PrintableString} or {@link IA5String}).
+ */
+public abstract class ASN1String extends ASN1Object {
+ private String rawString;
+
+ /**
+ * EFFECTS: Constructs an ASN1String with the given tag, parent tag, and string.
+ * - Throws {@link ParseException} if the string does not pass corresponding restrictions of the specific
+ * string type (same as {@link ASN1String#validate(String)})
+ * REQUIRES: For the requirements of tag and parentTag, consult {@link ASN1Object}.
+ */
+ public ASN1String(Tag tag, Tag parentTag, String string) throws ParseException {
+ super(tag, parentTag);
+ setString(string);
+ }
+
+ /**
+ * EFFECTS: Parse the input value. See {@link ASN1Object} with the rawString.
+ * Throws {@link ParseException} when invalid:
+ * - String does not pass type restrictions
+ * - Early EOF
+ * - Other cases as seen in {@link ASN1Object}
+ * MODIFIES: this, encoded (bytes are read)
+ */
+ public ASN1String(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ }
+
+ /**
+ * EFFECTS: Validate and set the string.
+ * Throws {@link ParseException} if the string is invalid.
+ * MODIFIES: this
+ */
+ protected void setString(String rawString) throws ParseException {
+ if (!validate(rawString)) {
+ throw new ParseException(String.format("The string '%s' is illegal for '%s'.",
+ rawString,
+ getClass().getSimpleName()));
+ }
+ this.rawString = rawString;
+ }
+
+ /**
+ * EFFECTS: Validate whether the given string matches the ASN.1 restrictions on this specific string type. By
+ * default, it always returns true.
+ */
+ protected boolean validate(String newString) {
+ return true;
+ }
+
+ /**
+ * Same as {@link ASN1String#getString()}.
+ */
+ @Override
+ public String toString() {
+ return rawString;
+ }
+
+ /**
+ * EFFECTS: Encode the string in DER bytes (big-endian UTF-8).
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ final byte[] bytes = rawString.getBytes(StandardCharsets.UTF_8);
+ Byte[] b = new Byte[bytes.length];
+ for (int i = 0; i < bytes.length; i++) {
+ b[i] = bytes[i];
+ }
+ return b;
+ }
+
+ public String getString() {
+ return rawString;
+ }
+}
diff --git a/src/main/model/asn1/ASN1Time.java b/src/main/model/asn1/ASN1Time.java
new file mode 100644
index 0000000..08f861e
--- /dev/null
+++ b/src/main/model/asn1/ASN1Time.java
@@ -0,0 +1,66 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.nio.charset.StandardCharsets;
+import java.time.ZonedDateTime;
+
+/**
+ * Common base-class for models like UTCTime and GeneralizedTime. Despite all the formatting and encoding differences,
+ * it stores a timestamp and corresponding timezone.
+ */
+public abstract class ASN1Time extends ASN1Object {
+ /**
+ * The time.
+ */
+ private ZonedDateTime timestamp;
+
+ /**
+ * EFFECTS: Initialize the time with the specific tag, parentTag, and timestamp. For tag and parentTag, consult
+ * {@link ASN1Object}.
+ */
+ public ASN1Time(Tag tag, Tag parentTag, ZonedDateTime timestamp) {
+ super(tag, parentTag);
+ this.timestamp = timestamp;
+ }
+
+ /**
+ * EFFECTS: Parse and decode DER bytes into the corresponding time type. For more info on decoding, take a look at
+ * {@link ASN1Object}.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Invalid date format
+ * - Zero length
+ * - Other circumstances (e.g., early EOF) as seen in {@link ASN1Object}
+ * MODIFIES: this, encoded
+ */
+ public ASN1Time(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.timestamp = toDate(new String(Utils.byteToByte(encoded.require(getLength(), true))));
+ }
+
+ /**
+ * EFFECTS: Generate the byte array for the formatted string.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Utils.byteToByte(toString().getBytes(StandardCharsets.UTF_8));
+ }
+
+ /**
+ * EFFECTS: Convert the given string into corresponding timestamp.
+ * Throws {@link ParseException} if the time is malformed.
+ */
+ public abstract ZonedDateTime toDate(String str) throws ParseException;
+
+ /**
+ * EFFECTS: Convert getTimestamp() into corresponding ASN.1 time format.
+ */
+ @Override
+ public abstract String toString();
+
+ public ZonedDateTime getTimestamp() {
+ return timestamp;
+ }
+}
diff --git a/src/main/model/asn1/BitString.java b/src/main/model/asn1/BitString.java
new file mode 100644
index 0000000..0561f24
--- /dev/null
+++ b/src/main/model/asn1/BitString.java
@@ -0,0 +1,95 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.math.BigInteger;
+
+/**
+ * Represents the ASN.1 BIT STRING (0x3) type. Bit strings represent bytes by padding to the LSB (like a bitstream).
+ * The bits are encoded as multiple bytes, but the unused bits are stored on the <b>lowest</b> part.
+ * For example, consider:
+ * <pre>
+ * 0b 00000001 10111001 01110111 (pad to the highest)
+ * Will be encoded into
+ * 0b 01101110 01011101 11000000 (pad to the lowest)
+ * ^^^^^^
+ * </pre>
+ * Before the encoded value, there will be another byte denoting how many padding bits are added to the right.
+ * That is, the final encoding is:
+ * <pre>
+ * 0b 00000110 01101110 01011101 11000000
+ * ^ 6 ^ ^ Original Number ^^Pad^
+ * </pre>
+ *
+ * BIT STRING has nothing to do with encoding bytes as printable strings (base10 or base16 or ASCII).
+ */
+public class BitString extends ASN1Object {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x3);
+
+ private final int unused;
+ private final Byte[] val;
+
+ /**
+ * EFFECT: Init with tags, unused, and val. For tags, see {@link ASN1Object}.
+ * REQUIRES: 0 <= unused < 8, the last byte in val must have the lowest $unused bits zero.
+ */
+ public BitString(Tag tag, Tag parentTag,
+ final int unused,
+ final Byte[] val) {
+ super(tag, parentTag);
+ this.unused = unused;
+ this.val = val;
+ }
+
+ /**
+ * EFFECT: Parse the input DER.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Unused is not in 0 <= unused < 8
+ * - The last byte does not have its lowest $unused bits zero
+ * - Other issues found according to {@link ASN1Object}
+ */
+ public BitString(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.unused = encoded.require(1, true)[0];
+ if (unused < 0 || unused > 7) {
+ throw new ParseException("Illegal unused byte: " + unused);
+ }
+ this.val = encoded.require(getLength() - 1, true);
+ if ((byte) (val[val.length - 1] << (8 - unused)) != 0) {
+ throw new ParseException(String.format("The last byte: 0x%02X does not have %d zero bits.",
+ val[val.length - 1],
+ unused));
+ }
+ }
+
+ /**
+ * EFFECTS: Get the converted form that has padding on MSB. The leftmost zero byte, if any, is removed.
+ */
+ public Byte[] getConvertedVal() {
+ return Utils.byteToByte(new BigInteger(Utils.byteToByte(val)).shiftRight(unused).toByteArray());
+ }
+
+ /**
+ * EFFECTS: Encode into DER.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ Byte[] arr = new Byte[val.length + 1];
+ arr[0] = (byte) unused;
+ System.arraycopy(val, 0, arr, 1, val.length);
+ return arr;
+ }
+
+ public int getUnused() {
+ return unused;
+ }
+
+ public Byte[] getVal() {
+ return val;
+ }
+}
diff --git a/src/main/model/asn1/Bool.java b/src/main/model/asn1/Bool.java
new file mode 100644
index 0000000..d9f1851
--- /dev/null
+++ b/src/main/model/asn1/Bool.java
@@ -0,0 +1,60 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+/**
+ * Represents the ASN.1 BOOLEAN type. It always has one byte length. Its content is either 0xFF (true) or 0x00 (false).
+ */
+public class Bool extends ASN1Object {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x1);
+
+ private final boolean value;
+
+ /**
+ * EFFECTS: Initiate the BOOLEAN with the given tag, an optional context-specific tag number for explicit
+ * encoding, and its value. For more information, consult {@link ASN1Object}.
+ * REQUIRES: Consult {@link ASN1Object}.
+ */
+ public Bool(Tag tag, Tag parentTag, boolean value) {
+ super(tag, parentTag);
+ this.value = value;
+ }
+
+ /**
+ * EFFECTS: Parse input bytes. For more information on tags parsing, consult {@link ASN1Object}.
+ * Throws {@link ParseException} if the input data is invalid:
+ * - The length is not 1
+ * - The value is neither 0x00 nor 0xFF
+ * - Other cases as denoted in {@link ASN1Object}
+ */
+ public Bool(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ if (getLength() != 1) {
+ throw new ParseException("Invalid boolean length: " + getLength());
+ }
+ final Byte val = encoded.require(1, true)[0];
+ if (val == 0) {
+ this.value = false;
+ } else if (val == -1) {
+ this.value = true;
+ } else {
+ throw new ParseException("Unknown boolean value: " + val);
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the boolean to either 0x00 or 0xFF.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return new Byte[]{ value ? (byte) -1 : 0 };
+ }
+
+ public boolean getValue() {
+ return value;
+ }
+}
diff --git a/src/main/model/asn1/Encodable.java b/src/main/model/asn1/Encodable.java
new file mode 100644
index 0000000..547029c
--- /dev/null
+++ b/src/main/model/asn1/Encodable.java
@@ -0,0 +1,9 @@
+package model.asn1;
+
+/**
+ * Provides the method of encoding the specific object into a DER sequence of bytes.
+ */
+@FunctionalInterface
+public interface Encodable {
+ Byte[] encodeDER();
+}
diff --git a/src/main/model/asn1/GeneralizedTime.java b/src/main/model/asn1/GeneralizedTime.java
new file mode 100644
index 0000000..385642d
--- /dev/null
+++ b/src/main/model/asn1/GeneralizedTime.java
@@ -0,0 +1,90 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+
+/**
+ * Represents the ASN.1 GeneralizedTime type. It encodes the time in "YYYYMMDDhhmm[ss]Z" string format, in UTC.
+ */
+public class GeneralizedTime extends ASN1Time {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x18);
+
+ /**
+ * Rather stupid impl ...
+ */
+ private static final DateTimeFormatter formatterNoSecs = new DateTimeFormatterBuilder()
+ .appendValue(ChronoField.YEAR, 4)
+ .appendValue(ChronoField.MONTH_OF_YEAR, 2)
+ .appendValue(ChronoField.DAY_OF_MONTH, 2)
+ .appendValue(ChronoField.HOUR_OF_DAY, 2)
+ .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+ .appendLiteral('Z')
+ .toFormatter()
+ .withZone(ZoneId.of("UTC"));
+
+ private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
+ .appendValue(ChronoField.YEAR, 4)
+ .appendValue(ChronoField.MONTH_OF_YEAR, 2)
+ .appendValue(ChronoField.DAY_OF_MONTH, 2)
+ .appendValue(ChronoField.HOUR_OF_DAY, 2)
+ .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+ .optionalStart()
+ .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
+ .optionalEnd()
+ .appendLiteral('Z')
+ .toFormatter()
+ .withZone(ZoneId.of("UTC"));
+
+ /**
+ * EFFECT: Construct the UTCTime with the given tag, parentTag, and timestamp. For tag and parentTag,
+ * consult {@link ASN1Object}.
+ * REQUIRES: timestamp must be in UTC.
+ */
+ public GeneralizedTime(Tag tag, Tag parentTag, ZonedDateTime timestamp) {
+ super(tag, parentTag, timestamp);
+ }
+
+ /**
+ * EFFECT: Parse the given DER input. Time will be assumed to be in UTC.
+ * Throws {@link ParseException}:
+ * - The time is not in the string format specified in class specification
+ * - Other invalid input is found. See {@link ASN1Object} for more details on parsing
+ */
+ public GeneralizedTime(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ }
+
+ /**
+ * EFFECT: Parse the string into time, in the format specified in class specification.
+ * Throws {@link ParseException} if the input is malformed.
+ */
+ @Override
+ public ZonedDateTime toDate(String str) throws ParseException {
+ try {
+ return ZonedDateTime.parse(str, formatter);
+ } catch (DateTimeParseException e) {
+ throw new ParseException(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECT: Convert the time into format "YYYYMMDDhhmm[ss]Z".
+ */
+ @Override
+ public String toString() {
+ if (getTimestamp().getSecond() == 0) {
+ return getTimestamp().format(formatterNoSecs);
+ }
+ return getTimestamp().format(formatter);
+ }
+}
diff --git a/src/main/model/asn1/IA5String.java b/src/main/model/asn1/IA5String.java
new file mode 100644
index 0000000..ea5cf91
--- /dev/null
+++ b/src/main/model/asn1/IA5String.java
@@ -0,0 +1,53 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * Represents an ASN.1 IA5String type. It is a string that is restricted to ISO 646 / T.50 characters.
+ */
+public class IA5String extends ASN1String {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x16);
+
+ /**
+ * EFFECTS: Constructs an IA5String with the given tag and string.
+ * Throws {@link ParseException} if the string is invalid. It must only contain T.50 chars.
+ * REQUIRES: For the requirements of tag and parentTag, consult {@link ASN1Object}.
+ */
+ public IA5String(Tag tag, Tag parentTag, String string) throws ParseException {
+ super(tag, parentTag, string);
+ }
+
+ /**
+ * EFFECTS: Parse from user input. Tags are parsed as-per {@link ASN1Object}. The value will be parsed as UTF-8 big
+ * endian.
+ * Throws {@link ParseException} if the encoded data is invalid:
+ * - Illegal string (containing non-T.50 chars)
+ * - Early EOF
+ * - Other cases in {@link ASN1Object}
+ * MODIFIES: this, encoded
+ */
+ public IA5String(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ setString(new String(Utils.byteToByte(encoded.require(getLength(), true)),
+ StandardCharsets.UTF_8));
+ }
+
+ /**
+ * EFFECTS: Checks whether the given string only contains ISO 646 / T.50 chars.
+ */
+ @Override
+ protected boolean validate(String newString) {
+ // Java doesn't have unsigned bytes - that is, bytes greater than 0x7F will
+ // overflow and become < 0. Thus, just compare b >= 0 will suffice.
+ return Arrays.stream(Utils.byteToByte(newString.getBytes(StandardCharsets.UTF_8)))
+ .noneMatch(b -> b < 0);
+ }
+}
diff --git a/src/main/model/asn1/Int.java b/src/main/model/asn1/Int.java
new file mode 100644
index 0000000..5b75a73
--- /dev/null
+++ b/src/main/model/asn1/Int.java
@@ -0,0 +1,81 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.cert.TbsCertificate;
+import ui.Utils;
+
+import java.math.BigInteger;
+import java.util.Arrays;
+
+/**
+ * An ASN.1 INTEGER type. By spec, it can be arbitrarily long. But just like it's impossible to have an
+ * endless tape in a turing machine, this implementation uses fixed length internally to represent ints.
+ */
+public class Int extends ASN1Object {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x2);
+
+ private final BigInteger value;
+
+ /**
+ * EFFECTS: Initiate the INTEGER object with the given tag and an optional context-specific tag number for explicit
+ * encoding. For more information, consult {@link ASN1Object}.
+ * REQUIRES: Consult {@link ASN1Object}.
+ */
+ public Int(Tag tag, Tag parentTag, long value) {
+ super(tag, parentTag);
+ this.value = BigInteger.valueOf(value);
+ }
+
+ /**
+ * EFFECTS: Parse input and get the int value. Tags are parsed in {@link ASN1Object}.
+ * Throws {@link ParseException} if encoded value are invalid:
+ * - Early EOF (not enough bytes)
+ * - Zero bytes length
+ * - Other issues denoted in {@link ASN1Object}
+ * MODIFIES: this, encoded
+ */
+ public Int(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ if (getLength() == 0) {
+ throw new ParseException("Integer with zero length");
+ }
+ this.value = new BigInteger(Utils.byteToByte(encoded.require(getLength(), true)));
+ }
+
+ /**
+ * EFFECTS: Produce the big-endian two's complement encoding of the value, in the shortest possible way (i.e., no
+ * leading 0x0 bytes, and no leading 0xFF bytes if negative). Notes, if a positive number is desired (or mandated
+ * like {@link TbsCertificate#getSerialNumber()}, append 0x0 to the MSB manually. This method always results in a
+ * signed integer. For simplicity, the first 0x0 is always removed except when the number itself is 0, and others
+ * are kept.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ Byte[] bytes = Utils.byteToByte(value.toByteArray());
+ if (bytes.length == 1) {
+ return bytes;
+ }
+ if (bytes[0] == 0x0) {
+ return Arrays.stream(bytes)
+ .skip(1)
+ .toArray(Byte[]::new);
+ }
+ return bytes;
+ }
+
+ /**
+ * EFFECTS: Get the value in long.
+ * Throws {@link ArithmeticException} if the value is too large for long.
+ */
+ public long getLong() throws ArithmeticException {
+ return value.longValueExact();
+ }
+
+ public BigInteger getValue() {
+ return value;
+ }
+}
diff --git a/src/main/model/asn1/Null.java b/src/main/model/asn1/Null.java
new file mode 100644
index 0000000..019db85
--- /dev/null
+++ b/src/main/model/asn1/Null.java
@@ -0,0 +1,45 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+/**
+ * Represents the ASN.1 NULL type. It always has zero length. With the default assigned tag, it always encodes
+ * into 0x05 0x00.
+ */
+public class Null extends ASN1Object {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x5);
+
+ /**
+ * EFFECTS: Initiate the NULL with the given tag and an optional context-specific tag number for explicit
+ * encoding. For more information, consult {@link ASN1Object}. NULL does not take any other arguments.
+ * REQUIRES: Consult {@link ASN1Object}.
+ */
+ public Null(Tag tag, Tag parentTag) {
+ super(tag, parentTag);
+ }
+
+ /**
+ * EFFECTS: Parse input bytes. For more information on tags parsing, consult {@link ASN1Object}.
+ * Throws {@link ParseException} if the input data is invalid:
+ * - The length is not 0
+ * - Other cases as denoted in {@link ASN1Object}
+ */
+ public Null(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ if (getLength() != 0) {
+ throw new ParseException("NULL must have zero length!");
+ }
+ }
+
+ /**
+ * EFFECTS: Always produce an empty array.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return new Byte[0];
+ }
+}
diff --git a/src/main/model/asn1/ObjectIdentifier.java b/src/main/model/asn1/ObjectIdentifier.java
new file mode 100644
index 0000000..e2b9dfe
--- /dev/null
+++ b/src/main/model/asn1/ObjectIdentifier.java
@@ -0,0 +1,204 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.math.BigInteger;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Represents an X.680 OID, which is a global-unique multi-component int value (with a registry managing all OIDs).
+ */
+public class ObjectIdentifier extends ASN1Object {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x6);
+
+ public static final Integer[] OID_CN = new Integer[]{ 2, 5, 4, 3 };
+ public static final Integer[] OID_SN = new Integer[]{ 2, 5, 4, 4 };
+ public static final Integer[] OID_C = new Integer[]{ 2, 5, 4, 6 };
+ public static final Integer[] OID_L = new Integer[]{ 2, 5, 4, 7 };
+ public static final Integer[] OID_O = new Integer[]{ 2, 5, 4, 10 };
+ public static final Integer[] OID_OU = new Integer[]{ 2, 5, 4, 11 };
+ public static final Integer[] OID_DC = new Integer[]{ 0, 9, 2342, 19200300, 100, 1, 25 };
+
+ public static final Integer[] OID_EXTENSION_REQUEST =
+ new Integer[]{ 1, 2, 840, 113549, 1, 9, 14 };
+
+ public static final Integer[] OID_RSA_ENCRYPTION =
+ new Integer[]{ 1, 2, 840, 113549, 1, 1, 1 };
+ public static final Integer[] OID_SHA256_WITH_RSA_ENCRYPTION =
+ new Integer[]{ 1, 2, 840, 113549, 1, 1, 11 };
+
+ public static final Integer[] OID_EC_PUBLIC_KEY =
+ new Integer[]{ 1, 2, 840, 10045, 2, 1 };
+ public static final Integer[] OID_ECDSA_WITH_SHA256 =
+ new Integer[]{ 1, 2, 840, 10045, 4, 3, 2 };
+ public static final Integer[] OID_ECDSA_WITH_SHA512 =
+ new Integer[]{ 1, 2, 840, 10045, 4, 3, 4 };
+ public static final Integer[] OID_PRIME256_V1 =
+ new Integer[]{ 1, 2, 840, 10045, 3, 1, 7 };
+
+ public static final Integer[] OID_SUBJECT_KEY_IDENTIFIER =
+ new Integer[]{ 2, 5, 29, 14 };
+ public static final Integer[] OID_KEY_USAGE =
+ new Integer[]{ 2, 5, 29, 15 };
+ public static final Integer[] OID_BASIC_CONSTRAINTS =
+ new Integer[]{ 2, 5, 29, 19 };
+ public static final Integer[] OID_AUTHORITY_KEY_IDENTIFIER =
+ new Integer[]{ 2, 5, 29, 35 };
+ public static final Integer[] OID_CRL_DISTRIBUTION_POINTS =
+ new Integer[]{ 2, 5, 29, 31 };
+ public static final Integer[] OID_AUTHORITY_INFO_ACCESS =
+ new Integer[]{ 1, 3, 6, 1, 5, 5, 7, 1, 1 };
+
+ public static final Integer[] OID_CURVED_25519 =
+ new Integer[]{ 1, 3, 101, 112 };
+
+ public static final Integer[] OID_CRL_REASON =
+ new Integer[]{ 2, 5, 29, 21 };
+
+ private final Integer[] ints;
+
+ /**
+ * EFFECTS: Construct the OID object with the given array of OID numbers. For the tag and parentTag,
+ * consult {@link ASN1Object}.
+ * REQUIRES: The ints array must have at least two elements, and the first element must be 0, 1, or 2.
+ * If the first element is 0 or 1, the second element must be < 40. For the tag and parentTag,
+ * consult {@link ASN1Object}.
+ */
+ public ObjectIdentifier(Tag tag, Tag parentTag, Integer[] ints) {
+ super(tag, parentTag);
+ this.ints = ints;
+ }
+
+ /**
+ * EFFECTS: Parse the input DER.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Zero bytes long
+ * - A multibyte integer is unterminated until the end of input
+ */
+ public ObjectIdentifier(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ if (getLength() < 1) {
+ throw new ParseException("Invalid OID");
+ }
+ List<Integer> nums = new ArrayList<>();
+ final Byte[] raw = encoded.require(getLength(), true);
+ Byte first = raw[0];
+ if (first >= 80) {
+ nums.add(2);
+ nums.add(first - 80);
+ } else if (first >= 40) {
+ nums.add(1);
+ nums.add(first - 40);
+ } else {
+ nums.add(0);
+ nums.add((int) first);
+ }
+ List<BitSet> num = new ArrayList<>();
+ for (int i = 1; i < raw.length; i++) {
+ Byte b = raw[i];
+ num.add(BitSet.valueOf(new byte[]{ (byte) (b & 127) }));
+ if ((b & -128) == 0) {
+ BitSet bitSet = new BitSet(num.size() * 7);
+ int z = 0;
+
+ for (int j = num.size() - 1; j >= 0; j--) {
+ for (int k = 0; k < 7; k++) {
+ bitSet.set(z++, num.get(j).get(k));
+ }
+ }
+
+ List<Byte> bs1 = Arrays.asList(Utils.byteToByte(bitSet.toByteArray()));
+ Collections.reverse(bs1);
+ nums.add(new BigInteger(Utils.byteToByte(bs1.toArray(new Byte[0]))).intValueExact());
+ num.clear();
+ }
+ }
+ if (!num.isEmpty()) {
+ throw new ParseException("Unterminated byte. Currently "
+ + num.stream().map(BitSet::toByteArray).map(Utils::byteToByte)
+ .flatMap(Arrays::stream)
+ .map(b -> String.format("0x%02X", b))
+ .collect(Collectors.toList()));
+ }
+ this.ints = nums.toArray(new Integer[0]);
+ }
+
+ /**
+ * EFFECTS: Generate a human-readable output of that OID, in the format of 0.1.2. In case of a well-known OID, its
+ * name is returned.
+ */
+ @Override
+ public String toString() {
+ if (Arrays.equals(ints, OID_CN)) {
+ return "CN";
+ } else if (Arrays.equals(ints, OID_SN)) {
+ return "SN";
+ } else if (Arrays.equals(ints, OID_C)) {
+ return "C";
+ } else if (Arrays.equals(ints, OID_L)) {
+ return "L";
+ } else if (Arrays.equals(ints, OID_O)) {
+ return "O";
+ } else if (Arrays.equals(ints, OID_OU)) {
+ return "OU";
+ } else if (Arrays.equals(ints, OID_DC)) {
+ return "DC";
+ }
+ return Arrays.stream(ints)
+ .map(i -> Integer.toString(i))
+ .collect(Collectors.joining("."));
+ }
+
+ /**
+ * EFFECTS: Encode the OID into DER bytes, following the DER rules as follows:
+ * - First two ints: first * 40 + second
+ * - Remaining: Int components are encoded as-is if they are <= 127. Otherwise, they are encoded into multiple 7bit
+ * bytes, with the MSB set on every byte except for the last (rightmost byte) of each component.
+ * - Integers are in big-endian.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(
+ Arrays.asList(Utils.valToByte(ints[0] * 40 + ints[1])),
+ Stream.of(ints)
+ .skip(2)
+ .map(i -> {
+ BigInteger bi = BigInteger.valueOf(i);
+ List<Byte> bs = Arrays.asList(Utils.byteToByte(bi.toByteArray()));
+ Collections.reverse(bs);
+ final BitSet bitSetOriginal = BitSet.valueOf(Utils.byteToByte(bs.toArray(new Byte[0])));
+ BitSet bitSet = new BitSet(bs.size() * 16);
+ int k = 0;
+ for (int j = 0; j < bs.size() * 8; j++) {
+ if (j == 0 || j % 7 != 0) {
+ bitSet.set(k++, bitSetOriginal.get(j));
+ } else {
+ bitSet.set(k++, j != 7);
+ bitSet.set(k++, bitSetOriginal.get(j));
+ }
+ }
+ byte[] bs1 = bitSet.toByteArray();
+ List<Byte> res =
+ Arrays.asList(Utils.byteToByte(bs1));
+ Collections.reverse(res);
+ return res;
+ })
+ .flatMap(Collection::stream)
+ .collect(Collectors.toList())
+ ).flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Integer[] getInts() {
+ return ints;
+ }
+}
diff --git a/src/main/model/asn1/OctetString.java b/src/main/model/asn1/OctetString.java
new file mode 100644
index 0000000..b552e52
--- /dev/null
+++ b/src/main/model/asn1/OctetString.java
@@ -0,0 +1,47 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+/**
+ * Represents the ASN.1 OCTET STRING type. An OCTET STRING is just a sequence of bytes.
+ */
+public class OctetString extends ASN1Object {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x4);
+
+ private final Byte[] bytes;
+
+ /**
+ * EFFECTS: Initiate the OctetString object with the given bytes array. The byte array can be arbitrary.
+ * REQUIRES: For the requirements of tag and parentTag, consult {@link ASN1Object}.
+ */
+ public OctetString(Tag tag, Tag parentTag, Byte[] bytes) {
+ super(tag, parentTag);
+ this.bytes = bytes;
+ }
+
+ /**
+ * EFFECTS: Parse tags and value from user input. Tags are parsed as-per {@link ASN1Object}.
+ * Throws {@link ParseException} if the encoded data is invalid, see {@link ASN1Object}.
+ * MODIFIES: this, encoded
+ */
+ public OctetString(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.bytes = encoded.require(getLength(), true);
+ }
+
+ /**
+ * EFFECTS: Get the value bytes ready to encode into DER.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return bytes;
+ }
+
+ public Byte[] getBytes() {
+ return bytes;
+ }
+} \ No newline at end of file
diff --git a/src/main/model/asn1/PrintableString.java b/src/main/model/asn1/PrintableString.java
new file mode 100644
index 0000000..73e33a6
--- /dev/null
+++ b/src/main/model/asn1/PrintableString.java
@@ -0,0 +1,49 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * An ASN.1 PrintableString that only allows ([a-z]|[A-Z]| |[0-9]|['()+,-./:=?])*.
+ */
+public class PrintableString extends ASN1String {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x13);
+
+ /**
+ * EFFECTS: Constructs with the given string.
+ * Throws {@link ParseException} if the given string is illegal (contains chars out of the PrintableString set).
+ * REQUIRES: For the requirements of tag and parentTag, consult {@link ASN1Object}.
+ */
+ public PrintableString(Tag tag, Tag parentTag, String rawString) throws ParseException {
+ super(tag, parentTag, rawString);
+ }
+
+ /**
+ * EFFECTS: Parse from user input. Tags are parsed as-per {@link ASN1Object}. The value will be parsed as UTF-8 big
+ * endian.
+ * Throws {@link ParseException} if the encoded data is invalid:
+ * - Early EOF and other cases in {@link ASN1Object}
+ * - Illegal string: Contains non-printable chars
+ * MODIFIES: this, encoded
+ */
+ public PrintableString(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ setString(new String(Utils.byteToByte(encoded.require(getLength(), true)),
+ StandardCharsets.UTF_8));
+ }
+
+ /**
+ * EFFECTS: Validate the given string against PrintableString spec.
+ * REQUIRES: newString != null
+ */
+ @Override
+ protected boolean validate(String newString) {
+ return newString.matches("([a-z]|[A-Z]| |[0-9]|['()+,-./:=?])*");
+ }
+}
diff --git a/src/main/model/asn1/Tag.java b/src/main/model/asn1/Tag.java
new file mode 100644
index 0000000..15c144f
--- /dev/null
+++ b/src/main/model/asn1/Tag.java
@@ -0,0 +1,109 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+/**
+ * Represents the metadata (tag) of an ASN.1 type.
+ */
+public class Tag implements Encodable {
+ private final TagClass cls;
+ private final boolean constructive;
+ private final int number;
+
+ /**
+ * EFFECTS: Construct the ASN.1 tag with the given class, constructive / primitive, and number.
+ * REQUIRES: number > 0 (X.680$8.2) and number <= 31 (no high tag number support currently);
+ * the constructive or primitive value must follow the X.680 spec, and it must be primitive if it
+ * can be both constructive or primitive (according to X.690 DER).
+ */
+ public Tag(TagClass cls, boolean constructive, int number) {
+ this.cls = cls;
+ this.constructive = constructive;
+ this.number = number;
+ }
+
+ /**
+ * EFFECTS: Initialize the tag by parsing class / constructive / number from the encoded DER bytes.
+ * {@link ParseException} is thrown if the input is invalid:
+ * - The encoded array must have at least one byte.
+ * - The tag number is zero if the class is UNIVERSAL.
+ * REQUIRES: The highest two bits must contain the class, and then the constructive bit, and finally the low 5 bits
+ * must contain the tag number <= 31.
+ * MODIFIES: encoded (one byte read)
+ */
+ public Tag(BytesReader encoded) throws ParseException {
+ final Byte val = encoded.require(1, true)[0];
+ // Take the highest two bits
+ // -64 = 2's complement of 0b11000000
+ final int highestTwo = val & -64;
+ if (highestTwo == TagClass.UNIVERSAL.getVal()) {
+ this.cls = TagClass.UNIVERSAL;
+ } else if (highestTwo == TagClass.APPLICATION.getVal()) {
+ this.cls = TagClass.APPLICATION;
+ } else if (highestTwo == TagClass.PRIVATE.getVal()) {
+ this.cls = TagClass.PRIVATE;
+ } else {
+ this.cls = TagClass.CONTEXT_SPECIFIC;
+ }
+
+ // Detect the constructive bit by only keeping the corresponding bit and shifting it to the lowestbit.
+ this.constructive = (val & 0x20) >> 5 == 1;
+
+ // Parse the number by clearing the high 3 bits.
+ this.number = val & 0x1F;
+
+ if (this.cls == TagClass.UNIVERSAL && this.number == 0) {
+ throw new ParseException(String.format("The tag number must not be zero for UNIVERSAL tags"
+ + "(byte 0x%02X @ %d)", val, encoded.getIndex()));
+ }
+ }
+
+ /**
+ * EFFECTS: Encode that tag as DER bytes, as follows:
+ * HI 7 6 | 5 | 4 3 2 1 0 LO
+ * Class | C/P | Tag Number
+ * Notes, In the domain of this application (PKI), a single byte is always returned
+ * (as nothing requires high tag number). However, the return type is held as byte[]
+ * to 1) compliant with the spec, 2) reserve for future scalability.
+ */
+ @Override
+ public Byte[] encodeDER() {
+ // Fill the low 5bits with tag number
+ byte value = (byte) number;
+ // Fill the bit in-between with constructive bit
+ if (constructive) {
+ value |= 0x20; // 0b00100000: Enable the 5th bit
+ } else {
+ value &= 0xDF; // 0b11011111: Disable the 5th bit
+ }
+ // Fill the high two bits with tag class
+ value |= cls.getVal();
+ return new Byte[] { value };
+ }
+
+ /**
+ * EFFECTS: Throw {@link ParseException} if this tag is not exactly the given tag. Useful for parsing.
+ */
+ public void enforce(Tag tag) throws ParseException {
+ if (this.number != tag.number
+ || this.constructive != tag.constructive
+ || this.cls != tag.cls) {
+ throw new ParseException(String.format("Illegal tag: Expected %s[%d] but got %s[%d].",
+ tag.cls, tag.number,
+ cls, number));
+ }
+ }
+
+ public TagClass getCls() {
+ return cls;
+ }
+
+ public boolean isConstructive() {
+ return constructive;
+ }
+
+ public int getNumber() {
+ return number;
+ }
+}
diff --git a/src/main/model/asn1/TagClass.java b/src/main/model/asn1/TagClass.java
new file mode 100644
index 0000000..83dd4e9
--- /dev/null
+++ b/src/main/model/asn1/TagClass.java
@@ -0,0 +1,42 @@
+package model.asn1;
+
+/**
+ * Represents the class (UNIVERSAL, APPLICATION, PRIVATE, CONTEXT-SPECIFIC) of an ASN.1 tag. See X.680$8.1.
+ * The purpose of UNIVERSAL, APPLICATION, PRIVATE, and CONTEXT_SPECIFIC can be found in X.680 spec.
+ * For example, UNIVERSAL means tags specified in the core ASN.1 spec.
+ * This class also represents the value to the two highest bits of DER-encoded tag values.
+ */
+public enum TagClass {
+ UNIVERSAL(Values.UNIVERSAL),
+ APPLICATION(Values.APPLICATION),
+ PRIVATE(Values.PRIVATE),
+ CONTEXT_SPECIFIC(Values.CONTENT_SPECIFIC);
+
+ private final Byte val;
+
+ /**
+ * EFFECT: Constructs the tag class with the given DER tag byte value.
+ * REQUIRES: The Byte value must have low 6bits cleared.
+ */
+ TagClass(Byte val) {
+ this.val = val;
+ }
+
+ public Byte getVal() {
+ return val;
+ }
+
+ /**
+ * The constants of high-two-bit values for Tag DER encoding.
+ */
+ public static final class Values {
+ // 0b00000000
+ public static final Byte UNIVERSAL = 0x0;
+ // 0b01000000
+ public static final Byte APPLICATION = 0x40;
+ // 0b11000000
+ public static final Byte PRIVATE = -64;
+ // 0b10000000
+ public static final Byte CONTENT_SPECIFIC = -128;
+ }
+}
diff --git a/src/main/model/asn1/UTF8String.java b/src/main/model/asn1/UTF8String.java
new file mode 100644
index 0000000..e6b101e
--- /dev/null
+++ b/src/main/model/asn1/UTF8String.java
@@ -0,0 +1,40 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import ui.Utils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+/**
+ * Represents an ASN.1 UTF8String type. It accepts any UTF-8 chars. Because UTF-8 character set is large and its chars
+ * have variable width, this implementation does not validate against legal UTF-8 characters.
+ */
+public class UTF8String extends ASN1String {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x0C);
+
+ /**
+ * EFFECTS: Constructs a UTF8String with the given tag and string.
+ * Throws {@link ParseException} if the string is illegal.
+ * REQUIRES: For the requirements of tag and parentTag, consult {@link ASN1Object}.
+ */
+ public UTF8String(Tag tag, Tag parentTag, String string) throws ParseException {
+ super(tag, parentTag, string);
+ }
+
+ /**
+ * EFFECTS: Parse from user input. Tags are parsed as-per {@link ASN1Object}. The value will be parsed as UTF-8 big
+ * endian.
+ * Throws {@link ParseException} if the encoded data is invalid.
+ * MODIFIES: this, encoded
+ */
+ public UTF8String(BytesReader encoded, boolean hasParentTag) throws ParseException, IllegalArgumentException {
+ super(encoded, hasParentTag);
+ setString(new String(Utils.byteToByte(encoded.require(getLength(), true)),
+ StandardCharsets.UTF_8));
+ }
+}
diff --git a/src/main/model/asn1/UtcTime.java b/src/main/model/asn1/UtcTime.java
new file mode 100644
index 0000000..3acf524
--- /dev/null
+++ b/src/main/model/asn1/UtcTime.java
@@ -0,0 +1,91 @@
+package model.asn1;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+import java.time.format.DateTimeParseException;
+import java.time.temporal.ChronoField;
+
+/**
+ * Represents the ASN.1 UTCTime type. It encodes the time in "YYMMDDhhmm[ss]Z" string format, in UTC.
+ * It is not named UTCTime to get around the checkstyle guidelines, but it is actually called UTCTime in X.680.
+ */
+public class UtcTime extends ASN1Time {
+ /**
+ * The X.680 universal class tag assignment.
+ */
+ public static final Tag TAG = new Tag(TagClass.UNIVERSAL, false, 0x17);
+
+ /**
+ * Rather stupid impl ...
+ */
+ private static final DateTimeFormatter formatterNoSecs = new DateTimeFormatterBuilder()
+ .appendPattern("yy")
+ .appendValue(ChronoField.MONTH_OF_YEAR, 2)
+ .appendValue(ChronoField.DAY_OF_MONTH, 2)
+ .appendValue(ChronoField.HOUR_OF_DAY, 2)
+ .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+ .appendLiteral('Z')
+ .toFormatter()
+ .withZone(ZoneId.of("UTC"));
+
+ private static final DateTimeFormatter formatter = new DateTimeFormatterBuilder()
+ .appendPattern("yy")
+ .appendValue(ChronoField.MONTH_OF_YEAR, 2)
+ .appendValue(ChronoField.DAY_OF_MONTH, 2)
+ .appendValue(ChronoField.HOUR_OF_DAY, 2)
+ .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+ .optionalStart()
+ .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
+ .optionalEnd()
+ .appendLiteral('Z')
+ .toFormatter()
+ .withZone(ZoneId.of("UTC"));
+
+ /**
+ * EFFECT: Construct the UTCTime with the given tag, parentTag, and timestamp. For tag and parentTag,
+ * consult {@link ASN1Object}.
+ * REQUIRES: timestamp must be in UTC.
+ */
+ public UtcTime(Tag tag, Tag parentTag, ZonedDateTime timestamp) {
+ super(tag, parentTag, timestamp);
+ }
+
+ /**
+ * EFFECT: Parse the given DER input. Time will be assumed to be in UTC.
+ * Throws {@link ParseException} if invalid:
+ * - The time is not in the string format specified in class specification
+ * - Other invalid input is found. See {@link ASN1Object} for more details on parsing
+ */
+ public UtcTime(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ }
+
+ /**
+ * EFFECT: Parse the string into time, in the format specified in class specification.
+ * Throws {@link ParseException} if the input is malformed.
+ */
+ @Override
+ public ZonedDateTime toDate(String str) throws ParseException {
+ try {
+ return ZonedDateTime.parse(str, formatter);
+ } catch (DateTimeParseException e) {
+ throw new ParseException(e.getMessage());
+ }
+ }
+
+ /**
+ * EFFECT: Convert the time into format "YYMMDDhhmm[ss]Z".
+ */
+ @Override
+ public String toString() {
+ if (getTimestamp().getSecond() == 0) {
+ return getTimestamp().format(formatterNoSecs);
+ }
+ return getTimestamp().format(formatter);
+ }
+}
diff --git a/src/main/model/asn1/exceptions/ParseException.java b/src/main/model/asn1/exceptions/ParseException.java
new file mode 100644
index 0000000..30533f6
--- /dev/null
+++ b/src/main/model/asn1/exceptions/ParseException.java
@@ -0,0 +1,10 @@
+package model.asn1.exceptions;
+
+/**
+ * Thrown when an invalid user DER input is supplied.
+ */
+public class ParseException extends Exception {
+ public ParseException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/model/asn1/parsing/BytesReader.java b/src/main/model/asn1/parsing/BytesReader.java
new file mode 100644
index 0000000..3e11ea6
--- /dev/null
+++ b/src/main/model/asn1/parsing/BytesReader.java
@@ -0,0 +1,105 @@
+package model.asn1.parsing;
+
+import model.asn1.ASN1Length;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+
+/**
+ * A mutable model represents a one-way pipe of reading the input DER bytes. It keeps track of the total input and the
+ * current location, and it provides useful methods of requiring one or more bytes to present in the next.
+ */
+public class BytesReader {
+ private final Byte[] rawInput;
+ private int index;
+
+ /**
+ * EFFECTS: Initialize the reader with the given input and index set to 0.
+ * REQUIRES: rawInput.length > 0
+ */
+ public BytesReader(Byte[] rawInput) {
+ this.rawInput = rawInput;
+ this.index = 0;
+ }
+
+ /**
+ * EFFECTS: Calculate the number of bytes remaining to read.
+ */
+ public int bytesRemaining() {
+ return rawInput.length - index;
+ }
+
+ /**
+ * EFFECTS: Copy the given number of bytes from the [getIndex(), getIndex() + size) and optionally mark as read.
+ * MODIFIES: this (if markAsRead == true)
+ * REQUIRES: size <= bytesRemaining(), size > 0
+ */
+ public Byte[] read(int size, boolean markAsRead) {
+ Byte[] result = new Byte[size];
+ System.arraycopy(rawInput, index, result, 0, size);
+ if (markAsRead) {
+ index += size;
+ }
+ return result;
+ }
+
+ /**
+ * EFFECTS: Copy the given number of bytes from [getIndex(), getIndex() + size) and optionally mark as read.
+ * Throws {@link ParseException} if size > bytesRemaining().
+ * MODIFIES: this (if markAsRead == true)
+ * REQUIRES: size > 0
+ */
+ public Byte[] require(int size, boolean markAsRead) throws ParseException {
+ validateSize(size);
+ return read(size, markAsRead);
+ }
+
+ /**
+ * EFFECTS: Check if size <= bytesRemaining().
+ * Throws {@link ParseException if not}.
+ * REQUIRES: size > 0
+ */
+ public void validateSize(int size) throws ParseException {
+ if (size > bytesRemaining()) {
+ throw new ParseException(String.format("%d required at location %d, but only has %d before EOF.",
+ size,
+ index,
+ bytesRemaining()));
+ }
+ }
+
+ /**
+ * EFFECTS: Check if the next byte has the desired tag, without changing the index.
+ * Throws {@link ParseException} if the input is illegal (not even a tag or EOF).
+ */
+ public boolean detectTag(Tag desired) throws ParseException {
+ final int i = index;
+ final Tag t = new Tag(this);
+ index = i;
+ return t.getCls() == desired.getCls()
+ && t.isConstructive() == desired.isConstructive()
+ && t.getNumber() == desired.getNumber();
+ }
+
+ /**
+ * EFFECTS: Get the current tag or the tag immediately following (inner) without changing the index.
+ * Throws {@link ParseException} if the input is illegal (not even a tag or EOF).
+ */
+ public Tag getTag(boolean inner) throws ParseException {
+ final int i = index;
+ Tag t = new Tag(this);
+ if (inner) {
+ new ASN1Length(this);
+ t = new Tag(this);
+ }
+ index = i;
+ return t;
+ }
+
+ public Byte[] getRawInput() {
+ return rawInput;
+ }
+
+ public int getIndex() {
+ return index;
+ }
+}