aboutsummaryrefslogtreecommitdiff
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
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>
-rw-r--r--src/main/model/MyModel.java5
-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
-rw-r--r--src/main/model/csr/Attribute.java83
-rw-r--r--src/main/model/csr/Attributes.java65
-rw-r--r--src/main/model/csr/CertificationRequest.java110
-rw-r--r--src/main/model/csr/CertificationRequestInfo.java127
-rw-r--r--src/main/model/csr/Values.java69
-rw-r--r--src/main/model/pki/AlgorithmIdentifier.java105
-rw-r--r--src/main/model/pki/SubjectPublicKeyInfo.java83
-rw-r--r--src/main/model/pki/cert/Certificate.java127
-rw-r--r--src/main/model/pki/cert/Extension.java113
-rw-r--r--src/main/model/pki/cert/Extensions.java67
-rw-r--r--src/main/model/pki/cert/TbsCertificate.java263
-rw-r--r--src/main/model/pki/cert/Validity.java95
-rw-r--r--src/main/model/pki/crl/CertificateList.java76
-rw-r--r--src/main/model/pki/crl/CertificateListContent.java106
-rw-r--r--src/main/model/pki/crl/Reason.java27
-rw-r--r--src/main/model/pki/crl/RevokedCertificate.java72
-rw-r--r--src/main/model/x501/AttributeTypeAndValue.java90
-rw-r--r--src/main/model/x501/Name.java79
-rw-r--r--src/main/model/x501/RelativeDistinguishedName.java78
-rw-r--r--src/main/ui/Utils.java105
-rw-r--r--src/test/model/MyModelTest.java5
-rw-r--r--src/test/model/TestConstants.java544
-rw-r--r--src/test/model/asn1/ASN1LengthTest.java94
-rw-r--r--src/test/model/asn1/ASN1ObjectTest.java177
-rw-r--r--src/test/model/asn1/BitStringTest.java75
-rw-r--r--src/test/model/asn1/BoolTest.java45
-rw-r--r--src/test/model/asn1/GeneralizedTimeTest.java119
-rw-r--r--src/test/model/asn1/IA5StringTest.java79
-rw-r--r--src/test/model/asn1/IntTest.java96
-rw-r--r--src/test/model/asn1/NullTest.java38
-rw-r--r--src/test/model/asn1/ObjectIdentifierTest.java89
-rw-r--r--src/test/model/asn1/OctetStringTest.java44
-rw-r--r--src/test/model/asn1/PrintableStringTest.java71
-rw-r--r--src/test/model/asn1/TagClassTest.java15
-rw-r--r--src/test/model/asn1/TagTest.java107
-rw-r--r--src/test/model/asn1/UTF8StringTest.java64
-rw-r--r--src/test/model/asn1/UtcTimeTest.java121
-rw-r--r--src/test/model/asn1/parsing/BytesReaderTest.java95
-rw-r--r--src/test/model/csr/AttributeTest.java53
-rw-r--r--src/test/model/csr/AttributesTest.java69
-rw-r--r--src/test/model/csr/CertificationRequestInfoTest.java174
-rw-r--r--src/test/model/csr/CertificationRequestTest.java114
-rw-r--r--src/test/model/csr/ValuesTest.java133
-rw-r--r--src/test/model/pki/AlgorithmIdentifierTest.java84
-rw-r--r--src/test/model/pki/SubjectPublicKeyInfoTest.java115
-rw-r--r--src/test/model/pki/cert/CertificateTest.java81
-rw-r--r--src/test/model/pki/cert/ExtensionTest.java118
-rw-r--r--src/test/model/pki/cert/ExtensionsTest.java112
-rw-r--r--src/test/model/pki/cert/TbsCertificateTest.java184
-rw-r--r--src/test/model/pki/cert/ValidityTest.java118
-rw-r--r--src/test/model/pki/crl/CertificateListContentTest.java37
-rw-r--r--src/test/model/pki/crl/CertificateListTest.java52
-rw-r--r--src/test/model/pki/crl/RevokedCertificateTest.java25
-rw-r--r--src/test/model/x501/AttributeTypeAndValueTest.java90
-rw-r--r--src/test/model/x501/NameTest.java167
-rw-r--r--src/test/model/x501/RelativeDistinguishedNameTest.java95
77 files changed, 7217 insertions, 10 deletions
diff --git a/src/main/model/MyModel.java b/src/main/model/MyModel.java
deleted file mode 100644
index f9a3dd7..0000000
--- a/src/main/model/MyModel.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package model;
-
-public class MyModel {
- // delete or rename this class!
-}
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;
+ }
+}
diff --git a/src/main/model/csr/Attribute.java b/src/main/model/csr/Attribute.java
new file mode 100644
index 0000000..2fa319b
--- /dev/null
+++ b/src/main/model/csr/Attribute.java
@@ -0,0 +1,83 @@
+package model.csr;
+
+import model.asn1.ASN1Object;
+import model.asn1.ObjectIdentifier;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Implements the following:
+ * <pre>
+ * Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE {
+ * type ATTRIBUTE.&id({IOSet}),
+ * values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type})
+ * }
+ * </pre>
+ *
+ * Represents a key - values pair in the CSR attribute.
+ */
+public class Attribute extends ASN1Object {
+ /**
+ * The type of that attribute. For example, <pre>2.5.29.14</pre> is subjectKeyIdentifier.
+ * It determines the format of the value.
+ */
+ private final ObjectIdentifier type;
+
+ /**
+ * Value set.
+ */
+ private final Values values;
+
+ /**
+ * EFFECT: Init the object with tag, parentTag, type, and values. For tag and parentTag, see {@link ASN1Object}.
+ * REQUIRES: The values must match the type. Type tag should be UNIVERSAL OID, and values should be SET OF.
+ */
+ public Attribute(Tag tag, Tag parentTag,
+ ObjectIdentifier type, Values values) {
+ super(tag, parentTag);
+ this.type = type;
+ this.values = values;
+ }
+
+ /**
+ * EFFECTS: Parse input DER. Value is not checked against the type.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing (info, algorithm, signature)
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public Attribute(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.type = new ObjectIdentifier(encoded, false);
+ this.type.getTag().enforce(ObjectIdentifier.TAG);
+
+ this.values = new Values(encoded, false);
+ this.values.getTag().enforce(TAG_SET);
+ }
+
+ /**
+ * EFFECTS: Encode the fields into DER, in the order.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(type.encodeDER()),
+ Arrays.asList(values.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public ObjectIdentifier getType() {
+ return type;
+ }
+
+ public Values getValues() {
+ return values;
+ }
+}
diff --git a/src/main/model/csr/Attributes.java b/src/main/model/csr/Attributes.java
new file mode 100644
index 0000000..6819e71
--- /dev/null
+++ b/src/main/model/csr/Attributes.java
@@ -0,0 +1,65 @@
+package model.csr;
+
+import model.asn1.ASN1Object;
+import model.asn1.Encodable;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * Represents a CSR Attributes list.
+ * <pre>
+ * Attributes { ATTRIBUTE:IOSet } ::= SET OF Attribute{{ IOSet }}
+ * </pre>
+ */
+public class Attributes extends ASN1Object {
+ private final Attribute[] array;
+
+ /**
+ * EFFECT: Initialize the list with the given tag, parentTag, and array. For tag and parentTag, consult
+ * {@link ASN1Object}.
+ */
+ public Attributes(Tag tag, Tag parentTag, Attribute[] array) {
+ super(tag, parentTag);
+ this.array = array;
+ }
+
+ /**
+ * EFFECT: Parse the list from input DER bytes. For details on parsing, refer to {@link ASN1Object}.
+ * Throws {@link ParseException} for invalid input.
+ * MODIFIES: this, encoded
+ */
+ public Attributes(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ final List<Attribute> list = new ArrayList<>();
+ for (int i = 0; i < getLength();) {
+ int index = encoded.getIndex();
+ final Attribute attribute = new Attribute(encoded, false);
+ attribute.getTag().enforce(TAG_SEQUENCE);
+ list.add(attribute);
+ index = encoded.getIndex() - index;
+ i += index;
+ }
+ this.array = list.toArray(new Attribute[0]);
+ }
+
+ /**
+ * EFFECTS: Encode the SET OF into DER, keep order. Values will be encoded one-by-one.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(array)
+ .map(Encodable::encodeDER)
+ .flatMap(Arrays::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Attribute[] getArray() {
+ return array;
+ }
+}
diff --git a/src/main/model/csr/CertificationRequest.java b/src/main/model/csr/CertificationRequest.java
new file mode 100644
index 0000000..c08997c
--- /dev/null
+++ b/src/main/model/csr/CertificationRequest.java
@@ -0,0 +1,110 @@
+package model.csr;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents a PKCS#10 CSR.
+ * <pre>
+ * CertificationRequest ::= SEQUENCE {
+ * certificationRequestInfo CertificationRequestInfo,
+ * signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }},
+ * signature BIT STRING
+ * }
+ * </pre>
+ *
+ * A CSR is used to request a certificate from a CA, using a public key. The client encodes a CSR with
+ * its subject name, public key, and attributes, and sign that with their private key. The private key
+ * must match the public key encoded in the CSR. This is to prove to the CA that the client has the private
+ * key of the requested public key.
+ * After the CA receives the CSR, they can create a new certificate, with or without the requested subject
+ * and attributes. That is, the requested attributes only have informational purposes, and it is the CA that
+ * determines whether to use them.
+ * The data in the CSR are encoded in {@link CertificationRequestInfo}. This object contains the data an
+ * the signature.
+ */
+public class CertificationRequest extends ASN1Object {
+ /**
+ * All info of that CSR, excluding the signature.
+ * It will be signed, and the signature is in <pre>signature</pre>.
+ */
+ private final CertificationRequestInfo certificationRequestInfo;
+
+ /**
+ * The algorithm used for <pre>signature</pre>.
+ */
+ private final AlgorithmIdentifier signatureAlgorithm;
+
+ /**
+ * The signature.
+ */
+ private final BitString signature;
+
+ /**
+ * EFFECTS: Initialize the object with the given tag and parentTag, and info, signatureAlgorithm, and signature.
+ * REQUIRES: The signature must match the public key specified in info. The algorithm must match the signature. The
+ * fields must have correct tags as described in the class specification.
+ */
+ public CertificationRequest(Tag tag, Tag parentTag,
+ final CertificationRequestInfo certificationRequestInfo,
+ final AlgorithmIdentifier signatureAlgorithm,
+ final BitString signature) {
+ super(tag, parentTag);
+ this.certificationRequestInfo = certificationRequestInfo;
+ this.signatureAlgorithm = signatureAlgorithm;
+ this.signature = signature;
+ }
+
+ /**
+ * EFFECTS: Parse input DER CSR, without verifying the signature.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Any fields missing (info, algorithm, signature)
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public CertificationRequest(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.certificationRequestInfo = new CertificationRequestInfo(encoded, false);
+ this.certificationRequestInfo.getTag().enforce(TAG_SEQUENCE);
+
+ this.signatureAlgorithm = new AlgorithmIdentifier(encoded, false);
+ this.signatureAlgorithm.getTag().enforce(TAG_SEQUENCE);
+
+ this.signature = new BitString(encoded, false);
+ this.signature.getTag().enforce(BitString.TAG);
+ }
+
+ /**
+ * EFFECT: Encode that sequence into an ordered array of bytes, following the class specification.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(certificationRequestInfo.encodeDER()),
+ Arrays.asList(signatureAlgorithm.encodeDER()),
+ Arrays.asList(signature.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public CertificationRequestInfo getCertificationRequestInfo() {
+ return certificationRequestInfo;
+ }
+
+ public AlgorithmIdentifier getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public BitString getSignature() {
+ return signature;
+ }
+}
diff --git a/src/main/model/csr/CertificationRequestInfo.java b/src/main/model/csr/CertificationRequestInfo.java
new file mode 100644
index 0000000..425dba9
--- /dev/null
+++ b/src/main/model/csr/CertificationRequestInfo.java
@@ -0,0 +1,127 @@
+package model.csr;
+
+import model.asn1.ASN1Object;
+import model.asn1.Int;
+import model.asn1.Tag;
+import model.asn1.TagClass;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.SubjectPublicKeyInfo;
+import model.x501.Name;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents a RFC2986 / PKCS#10 CSR CertificationRequestInfo object.
+ * For more info on CRL, see {@link CertificationRequest}.
+ *
+ * <pre>
+ * DEFINITIONS IMPLICIT TAGS ::=
+ *
+ * CertificationRequestInfo ::= SEQUENCE {
+ * version INTEGER { v1(0) } (v1,...),
+ * subject Name,
+ * subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }},
+ * attributes [0] Attributes{{ CRIAttributes }}
+ * }
+ *
+ * </pre>
+ *
+ * It represents all information of a CSR (version, subject, public key, attributes).
+ * It will be signed, and the signature is in {@link CertificationRequest}.
+ */
+public class CertificationRequestInfo extends ASN1Object {
+ public static final int VERSION_V1 = 0;
+
+ /**
+ * Version of the CRL. Always {@link CertificationRequestInfo#VERSION_V1} (0).
+ */
+ private final Int version;
+
+ /**
+ * Subject of the requested certificate
+ */
+ private final Name subject;
+
+ /**
+ * The public key to request.
+ */
+ private final SubjectPublicKeyInfo subjectPKInfo;
+
+ private final Attributes attributes;
+
+ /**
+ * EFFECTS: Construct with the given version, subject, pubkey, attributes, and the given tags.
+ * REQUIRES: Version must be {@link CertificationRequestInfo#VERSION_V1}. The fields must have correct tags as
+ * described in class specification.
+ */
+ public CertificationRequestInfo(Tag tag, Tag parentTag,
+ final Int version,
+ final Name subject,
+ final SubjectPublicKeyInfo subjectPKInfo,
+ final Attributes attributes) {
+ super(tag, parentTag);
+ this.version = version;
+ this.subject = subject;
+ this.subjectPKInfo = subjectPKInfo;
+ this.attributes = attributes;
+ }
+
+ /**
+ * EFFECTS: Parse the object with the given DER input.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Any fields missing (version, subject, subjectPKInfo, attributes)
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public CertificationRequestInfo(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.version = new Int(encoded, false);
+ this.version.getTag().enforce(Int.TAG);
+ if (this.version.getLong() != VERSION_V1) {
+ throw new ParseException("Illegal version " + this.version.getLong());
+ }
+
+ this.subject = new Name(encoded, false);
+ this.subject.getTag().enforce(TAG_SEQUENCE);
+
+ this.subjectPKInfo = new SubjectPublicKeyInfo(encoded, false);
+ this.subjectPKInfo.getTag().enforce(TAG_SEQUENCE);
+
+ this.attributes = new Attributes(encoded, false);
+ this.attributes.getTag().enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 0));
+ }
+
+ /**
+ * EFFECTS: Encode the value of that object, in the same order and format as denoted in the ASN.1 specification.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(version.encodeDER()),
+ Arrays.asList(subject.encodeDER()),
+ Arrays.asList(subjectPKInfo.encodeDER()),
+ Arrays.asList(attributes.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Int getVersion() {
+ return version;
+ }
+
+ public Name getSubject() {
+ return subject;
+ }
+
+ public SubjectPublicKeyInfo getSubjectPKInfo() {
+ return subjectPKInfo;
+ }
+
+ public Attributes getAttributes() {
+ return attributes;
+ }
+}
diff --git a/src/main/model/csr/Values.java b/src/main/model/csr/Values.java
new file mode 100644
index 0000000..5c1e212
--- /dev/null
+++ b/src/main/model/csr/Values.java
@@ -0,0 +1,69 @@
+package model.csr;
+
+import model.asn1.ASN1Object;
+import model.asn1.Encodable;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * Represents a CSR attribute values list.
+ * <pre>
+ * Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE {
+ * type ATTRIBUTE.&id({IOSet}),
+ * values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type})
+ * }
+ * </pre>
+ * Values can be none or any length. Parsing and decoding the values are handled in specific types.
+ */
+public class Values extends ASN1Object {
+ private final ASN1Object[] array;
+
+ /**
+ * EFFECT: Initialize the list with the given tag, parentTag, and array. For tag and parentTag, consult
+ * {@link ASN1Object}.
+ * REQUIRES: All elements in the array shall be the same ASN.1 type.
+ */
+ public Values(Tag tag, Tag parentTag, ASN1Object[] array) {
+ super(tag, parentTag);
+ this.array = array;
+ }
+
+ /**
+ * EFFECT: Parse the list from input DER bytes. For details on parsing, refer to {@link ASN1Object}.
+ * Throws {@link ParseException} for invalid input.
+ * MODIFIES: this, encoded
+ */
+ public Values(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ final List<ASN1Object> list = new ArrayList<>();
+ for (int i = 0; i < getLength();) {
+ int index = encoded.getIndex();
+ final ASN1Object value = ASN1Object.parse(encoded, false);
+ list.add(value);
+ index = encoded.getIndex() - index;
+ i += index;
+ }
+ this.array = list.toArray(new ASN1Object[0]);
+ }
+
+ /**
+ * EFFECTS: Encode the SET OF into DER, keep order. Values will be encoded one-by-one.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(array)
+ .map(Encodable::encodeDER)
+ .flatMap(Arrays::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public ASN1Object[] getArray() {
+ return array;
+ }
+}
diff --git a/src/main/model/pki/AlgorithmIdentifier.java b/src/main/model/pki/AlgorithmIdentifier.java
new file mode 100644
index 0000000..421aa5a
--- /dev/null
+++ b/src/main/model/pki/AlgorithmIdentifier.java
@@ -0,0 +1,105 @@
+package model.pki;
+
+import model.asn1.ASN1Object;
+import model.asn1.ObjectIdentifier;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+/**
+ * Implements the following:
+ * <pre>
+ * AttributeTypeAndValue ::= SEQUENCE {
+ * type ATTRIBUTE.&id({SupportedAttributes}),
+ * value ATTRIBUTE.&Type({SupportedAttributes}{@type}) OPTIONAL,
+ * ... }
+ * </pre>
+ */
+public class AlgorithmIdentifier extends ASN1Object {
+ /**
+ * The type of that attribute. For example, <pre>1.2.840.113549.1.1.11</pre> is sha256WithRSAEncryption.
+ */
+ private final ObjectIdentifier type;
+
+ /**
+ * Additional parameters for that algorithm. Optional, and could be ASN.1 NULL or Java null (absent).
+ * According to RFC8017$A.2, it should be NULL for a number of algorithms:
+ * <pre>
+ * PKCS1Algorithms ALGORITHM-IDENTIFIER ::= {
+ * { OID rsaEncryption PARAMETERS NULL } |
+ * { OID md2WithRSAEncryption PARAMETERS NULL } |
+ * { OID md5WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha1WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha224WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha256WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha384WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha512WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha512-224WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha512-256WithRSAEncryption PARAMETERS NULL } |
+ * { OID id-RSAES-OAEP PARAMETERS RSAES-OAEP-params } |
+ * PKCS1PSourceAlgorithms |
+ * { OID id-RSASSA-PSS PARAMETERS RSASSA-PSS-params },
+ * ... -- Allows for future expansion --
+ * }
+ * </pre>
+ */
+ private final ASN1Object parameters;
+
+ /**
+ * EFFECT: Init the object with tag, parentTag, type, and parameters. For tag and parentTag, see {@link ASN1Object}.
+ * REQUIRES: The values must match the type. Type tag should be UNIVERSAL OID. Parameters nullable.
+ */
+ public AlgorithmIdentifier(Tag tag, Tag parentTag,
+ ObjectIdentifier type, ASN1Object parameters) {
+ super(tag, parentTag);
+ this.type = type;
+ this.parameters = parameters;
+ }
+
+ /**
+ * EFFECTS: Parse input DER. Parameters are not checked against the type.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public AlgorithmIdentifier(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ int i = encoded.getIndex();
+ this.type = new ObjectIdentifier(encoded, false);
+ this.type.getTag().enforce(ObjectIdentifier.TAG);
+ i = encoded.getIndex() - i;
+
+ if (getLength() > i) {
+ this.parameters = ASN1Object.parse(encoded, false);
+ } else {
+ this.parameters = null;
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the fields into DER, in the order.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(type.encodeDER()),
+ parameters == null ? Collections.<Byte>emptyList() : Arrays.asList(parameters.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public ObjectIdentifier getType() {
+ return type;
+ }
+
+ public ASN1Object getParameters() {
+ return parameters;
+ }
+}
diff --git a/src/main/model/pki/SubjectPublicKeyInfo.java b/src/main/model/pki/SubjectPublicKeyInfo.java
new file mode 100644
index 0000000..ac72055
--- /dev/null
+++ b/src/main/model/pki/SubjectPublicKeyInfo.java
@@ -0,0 +1,83 @@
+package model.pki;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents the following ASN.1 structure:
+ * <pre>
+ * SubjectPublicKeyInfo ::= SEQUENCE {
+ * algorithm AlgorithmIdentifier{{SupportedAlgorithms}},
+ * subjectPublicKey BIT STRING,
+ * ... }
+ * </pre>
+ * It represents the public key of a subject, in a certificate.
+ */
+public class SubjectPublicKeyInfo extends ASN1Object {
+ /**
+ * The algorithm used.
+ */
+ private final AlgorithmIdentifier algorithm;
+
+ /**
+ * The public key.
+ */
+ private final BitString subjectPublicKey;
+
+ /**
+ * EFFECTS: Init with tags, algorithm, subjectPublicKey. For tags, see {@link ASN1Object}.
+ * REQUIRES: The public key should be a valid $algorithm key. Algorithm and publicKey should have default UNIVERSAL
+ * tags (SEQUENCE and BIT STRING).
+ */
+ public SubjectPublicKeyInfo(Tag tag, Tag parentTag,
+ final AlgorithmIdentifier algorithm,
+ final BitString subjectPublicKey) {
+ super(tag, parentTag);
+ this.algorithm = algorithm;
+ this.subjectPublicKey = subjectPublicKey;
+ }
+
+ /**
+ * EFFECTS: Parse input DER.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing (info, algorithm, signature)
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public SubjectPublicKeyInfo(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.algorithm = new AlgorithmIdentifier(encoded, false);
+ this.algorithm.getTag().enforce(TAG_SEQUENCE);
+
+ this.subjectPublicKey = new BitString(encoded, false);
+ this.subjectPublicKey.getTag().enforce(BitString.TAG);
+ }
+
+ /**
+ * EFFECTS: Encode the fields into DER, in the order.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(algorithm.encodeDER()),
+ Arrays.asList(subjectPublicKey.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public AlgorithmIdentifier getAlgorithm() {
+ return algorithm;
+ }
+
+ public BitString getSubjectPublicKey() {
+ return subjectPublicKey;
+ }
+}
diff --git a/src/main/model/pki/cert/Certificate.java b/src/main/model/pki/cert/Certificate.java
new file mode 100644
index 0000000..4e6c291
--- /dev/null
+++ b/src/main/model/pki/cert/Certificate.java
@@ -0,0 +1,127 @@
+package model.pki.cert;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents an X.509 signed certificate.
+ * <pre>
+ * Certificate ::= SIGNED{TBSCertificate}
+ *
+ * ENCRYPTED{ToBeEnciphered} ::= BIT STRING (CONSTRAINED BY {
+ * -- shall be the result of applying an encipherment procedure
+ * -- to the BER-encoded octets of a value of -- ToBeEnciphered } )
+ *
+ * HASH{ToBeHashed} ::= SEQUENCE {
+ * algorithmIdentifier AlgorithmIdentifier{{SupportedAlgorithms}},
+ * hashValue BIT STRING (CONSTRAINED BY {
+ * -- shall be the result of applying a hashing procedure to the DER-encoded
+ * -- octets of a value of -- ToBeHashed } ),
+ * ... }
+ *
+ * ENCRYPTED-HASH{ToBeSigned} ::= BIT STRING (CONSTRAINED BY {
+ * -- shall be the result of applying a hashing procedure to the DER-encoded (see 6.2)
+ * -- octets of a value of -- ToBeSigned -- and then applying an encipherment procedure
+ * -- to those octets -- } )
+ *
+ * SIGNATURE{ToBeSigned} ::= SEQUENCE {
+ * algorithmIdentifier AlgorithmIdentifier{{SupportedAlgorithms}},
+ * encrypted ENCRYPTED-HASH{ToBeSigned},
+ * ... }
+ *
+ * SIGNED{ToBeSigned} ::= SEQUENCE {
+ * toBeSigned ToBeSigned,
+ * COMPONENTS OF SIGNATURE{ToBeSigned},
+ * ... }
+ * </pre>
+ *
+ * A certificate creates a binding between the proposed subject name and the public key. It is only valid once a trusted
+ * CA signs it. Relying parties only need to trust a single trust anchor (the Root CA), and all of its issued certs are
+ * trusted. This is done through the cert tree: each certificate contains the Issued By field, indicating the DN of the
+ * upper level, all the way until the root CA, which is hard-coded in relying parties.
+ */
+public class Certificate extends ASN1Object {
+ /**
+ * All info of that cert, excluding the signature.
+ * It will be signed, and the signature is in <pre>signature</pre>.
+ */
+ private final TbsCertificate certificate;
+
+ /**
+ * The algorithm used for <pre>signature</pre>.
+ */
+ private final AlgorithmIdentifier signatureAlgorithm;
+
+ /**
+ * The signature.
+ */
+ private final BitString signature;
+
+ /**
+ * EFFECTS: Initialize the object with the given tag and parentTag, and info, signatureAlgorithm, and signature.
+ * REQUIRES: The algorithm must match the signature. The fields must have correct tags as described in the class
+ * specification (SEQUENCE, SEQUENCE, BIT STRING).
+ */
+ public Certificate(Tag tag, Tag parentTag,
+ final TbsCertificate certificate,
+ final AlgorithmIdentifier signatureAlgorithm,
+ final BitString signature) {
+ super(tag, parentTag);
+ this.certificate = certificate;
+ this.signatureAlgorithm = signatureAlgorithm;
+ this.signature = signature;
+ }
+
+ /**
+ * EFFECTS: Parse input DER, without verifying the signature.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public Certificate(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.certificate = new TbsCertificate(encoded, false);
+ this.certificate.getTag().enforce(TAG_SEQUENCE);
+
+ this.signatureAlgorithm = new AlgorithmIdentifier(encoded, false);
+ this.signatureAlgorithm.getTag().enforce(TAG_SEQUENCE);
+
+ this.signature = new BitString(encoded, false);
+ this.signature.getTag().enforce(BitString.TAG);
+ }
+
+ /**
+ * EFFECT: Encode that sequence into an ordered array of bytes, following the class specification.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(certificate.encodeDER()),
+ Arrays.asList(signatureAlgorithm.encodeDER()),
+ Arrays.asList(signature.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public TbsCertificate getCertificate() {
+ return certificate;
+ }
+
+ public AlgorithmIdentifier getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public BitString getSignature() {
+ return signature;
+ }
+}
diff --git a/src/main/model/pki/cert/Extension.java b/src/main/model/pki/cert/Extension.java
new file mode 100644
index 0000000..0c104a4
--- /dev/null
+++ b/src/main/model/pki/cert/Extension.java
@@ -0,0 +1,113 @@
+package model.pki.cert;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+/**
+ * A X.509v3 certificate extension entry.
+ * <pre>
+ * Extension ::= SEQUENCE {
+ * extnId EXTENSION.&id({ExtensionSet}),
+ * critical BOOLEAN DEFAULT FALSE,
+ * extnValue OCTET STRING
+ * (CONTAINING EXTENSION.&ExtnType({ExtensionSet}{@extnId})
+ * ENCODED BY der),
+ * ... }
+ * </pre>
+ * Extensions only exist in v3 certificates. They allow the CA and the relying party to add additional verification
+ * stages to the certificate to constraint its use or to supply additional information. For example, the CA may put a
+ * CDP (CRL Distribution Point) into the extensions.
+ */
+public class Extension extends ASN1Object {
+ /**
+ * The ID of the type of that extension.
+ */
+ private final ObjectIdentifier extnId;
+
+ /**
+ * Marking an extension critical means that the relying-party
+ * must reject that certificate if the type is unrecognized.
+ * If the type is recognized but cannot be fully parsed, the
+ * behaviour is undefined.
+ * Marking an extension critical reduces compatibility.
+ */
+ private final Bool critical;
+
+ /**
+ * The DER-encoded ASN.1 content of that extension.
+ */
+ private final OctetString extnValue;
+
+ /**
+ * EFFECTS: Init with tags, extnId, critical, and extnValue. For tags, see {@link ASN1Object}.
+ * extnValue is not checked against extnId.
+ * REQUIRES: Tags of extnId, critical, extnValue should be OID, BOOLEAN, OCTET STRING. The value should be a DER
+ * bytes octet string. If critical is unspecified (which defaults to false), put null.
+ */
+ public Extension(Tag tag, Tag parentTag,
+ final ObjectIdentifier extnId,
+ final Bool critical,
+ final OctetString extnValue) {
+ super(tag, parentTag);
+ this.extnId = extnId;
+ this.critical = critical;
+ this.extnValue = extnValue;
+ }
+
+ /**
+ * EFFECTS: Parse input DER.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * Note that critical is optional, and if it does not exist, it will be left as null, and it should be treated as
+ * false.
+ * MODIFIES: this, encoded
+ */
+ public Extension(BytesReader encoded, boolean hasParentType) throws ParseException {
+ super(encoded, hasParentType);
+ this.extnId = new ObjectIdentifier(encoded, false);
+ this.extnId.getTag().enforce(ObjectIdentifier.TAG);
+
+ if (encoded.detectTag(Bool.TAG)) {
+ critical = new Bool(encoded, false);
+ } else {
+ critical = null;
+ }
+
+ this.extnValue = new OctetString(encoded, false);
+ this.extnValue.getTag().enforce(OctetString.TAG);
+ }
+
+ /**
+ * EFFECTS: Encode the DER.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(extnId.encodeDER()),
+ critical == null ? Collections.<Byte>emptyList() :
+ Arrays.asList(critical.encodeDER()),
+ Arrays.asList(extnValue.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public ObjectIdentifier getExtnId() {
+ return extnId;
+ }
+
+ public Bool getCritical() {
+ return critical;
+ }
+
+ public OctetString getExtnValue() {
+ return extnValue;
+ }
+}
diff --git a/src/main/model/pki/cert/Extensions.java b/src/main/model/pki/cert/Extensions.java
new file mode 100644
index 0000000..780fa2c
--- /dev/null
+++ b/src/main/model/pki/cert/Extensions.java
@@ -0,0 +1,67 @@
+package model.pki.cert;
+
+import model.asn1.ASN1Object;
+import model.asn1.Encodable;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.x501.RelativeDistinguishedName;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * Represents an X.509 certificate extensions list:
+ * <pre>
+ * Extensions ::= SEQUENCE OF Extension
+ * </pre>
+ */
+public class Extensions extends ASN1Object {
+ private final Extension[] extensions;
+
+ /**
+ * EFFECT: Initialize with the given tags and extensions. For tag and parentTag, consult
+ * {@link ASN1Object}.
+ * REQUIRES: Extensions should have SEQUENCE tag.
+ */
+ public Extensions(Tag tag, Tag parentTag, Extension[] extensions) {
+ super(tag, parentTag);
+ this.extensions = extensions;
+ }
+
+ /**
+ * EFFECT: Parse the Name from input DER bytes. For details on parsing, refer to {@link ASN1Object}.
+ * Throws {@link ParseException} for invalid input.
+ * MODIFIES: this, encoded
+ */
+ public Extensions(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ final List<Extension> list = new ArrayList<>();
+ for (int i = 0; i < getLength();) {
+ int index = encoded.getIndex();
+ final Extension ext = new Extension(encoded, false);
+ ext.getTag().enforce(TAG_SEQUENCE);
+ list.add(ext);
+ index = encoded.getIndex() - index;
+ i += index;
+ }
+ this.extensions = list.toArray(new Extension[0]);
+ }
+
+ /**
+ * EFFECTS: Encode the SEQUENCE OF into DER, keep order. RDNs will be encoded one-by-one.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(extensions)
+ .map(Encodable::encodeDER)
+ .flatMap(Arrays::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Extension[] getExtensions() {
+ return extensions;
+ }
+}
diff --git a/src/main/model/pki/cert/TbsCertificate.java b/src/main/model/pki/cert/TbsCertificate.java
new file mode 100644
index 0000000..1175456
--- /dev/null
+++ b/src/main/model/pki/cert/TbsCertificate.java
@@ -0,0 +1,263 @@
+package model.pki.cert;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+import model.pki.SubjectPublicKeyInfo;
+import model.x501.Name;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+/**
+ * Represents a X.509 certificate
+ *
+ * <pre>
+ * Certificate ::= SIGNED{TBSCertificate}
+ * TBSCertificate ::= SEQUENCE {
+ * version [0] Version DEFAULT v1,
+ * serialNumber CertificateSerialNumber,
+ * signature AlgorithmIdentifier{{SupportedAlgorithms}},
+ * issuer Name,
+ * validity Validity,
+ * subject Name,
+ * subjectPublicKeyInfo SubjectPublicKeyInfo,
+ * issuerUniqueIdentifier [1] IMPLICIT UniqueIdentifier OPTIONAL,
+ * ...,
+ * [[2: -- if present, version shall be v2 or v3
+ * subjectUniqueIdentifier [2] IMPLICIT UniqueIdentifier OPTIONAL]],
+ * [[3: -- if present, version shall be v2 or v3
+ * extensions [3] Extensions OPTIONAL]]
+ * -- If present, version shall be v3]]
+ * }
+ *
+ * Version ::= INTEGER {v1(0), v2(1), v3(2)}
+ * CertificateSerialNumber ::= INTEGER
+ *
+ * uniqueIdentifier ATTRIBUTE ::= {
+ * WITH SYNTAX UniqueIdentifier
+ * EQUALITY MATCHING RULE bitStringMatch
+ * LDAP-SYNTAX bitString.&id
+ * LDAP-NAME {"x500UniqueIdentifier"}
+ * ID id-at-uniqueIdentifier }
+ * UniqueIdentifier ::= BIT STRING
+ * </pre>
+ *
+ * NOTE that subjectUniqueIdentifier and issuerUniqueIdentifier are not supported.
+ */
+public class TbsCertificate extends ASN1Object {
+ // Version ::= INTEGER {v1(0), v2(1), v3(2)}
+ public static final int VERSION_V1 = 0;
+ public static final int VERSION_V2 = 1;
+ public static final int VERSION_V3 = 2;
+
+ /**
+ * The X.509 cert version. subjectUniqueIdentifier is v2 only, and extensions is v3 only.
+ * <pre>
+ * [0] Version DEFAULT v1
+ * </pre>
+ */
+ private final Int version;
+
+ /**
+ * The serial number of that certificate that is unique across the CA.
+ * <pre>
+ * serialNumber CertificateSerialNumber
+ * CertificateSerialNumber ::= INTEGER
+ * </pre>
+ */
+ private final Int serialNumber;
+
+ private final AlgorithmIdentifier signature;
+
+ /**
+ * The subject and issuer distinguished names.
+ * <pre>
+ * issuer Name,
+ * subject Name
+ * </pre>
+ */
+ private final Name issuer;
+
+ /**
+ * The validity period of that certificate.
+ * Validity ::= SEQUENCE { notBefore Time, notAfter Time, ... }
+ */
+ private final Validity validity;
+
+ /**
+ * See the comments on issuer.
+ */
+ private final Name subject;
+
+ private final SubjectPublicKeyInfo subjectPublicKeyInfo;
+
+ /**
+ * [3] Optional.
+ */
+ private final Extensions extensions;
+
+ /**
+ * EFFECTS: Init with the given parameters. For tag and parentTag, see {@link ASN1Object}.
+ * REQUIRES:
+ * - Version must be V1, V2, or V3.
+ * - {issuer,subject}UniqueIdentifier could be null.
+ * - If {issuer,subject}UniqueIdentifier presents, version must be V2 or V3.
+ * - Extensions could be null.
+ * - If extensions presents, version must be V3.
+ * - The signature should be valid.
+ * - Field and Desired Tags:
+ * version CONTEXT SPECIFIC 0 (EXPLICIT), INTEGER, OPTIONAL DEFAULT v1
+ * serialNumber INTEGER
+ * signature SEQUENCE
+ * issuer SEQUENCE
+ * validity SEQUENCE
+ * subject SEQUENCE
+ * subjectPublicKeyInfo SEQUENCE
+ * extensions CONTEXT SPECIFIC 3 (EXPLICIT), SEQUENCE, OPTIONAL
+ */
+ public TbsCertificate(Tag tag, Tag parentTag,
+ final Int version,
+ final Int serialNumber,
+ final AlgorithmIdentifier signature,
+ final Name issuer,
+ final Validity validity,
+ final Name subject,
+ final SubjectPublicKeyInfo subjectPublicKeyInfo,
+ final Extensions extensions) {
+ super(tag, parentTag);
+ this.version = version;
+ this.serialNumber = serialNumber;
+ this.signature = signature;
+ this.issuer = issuer;
+ this.validity = validity;
+ this.subject = subject;
+ this.subjectPublicKeyInfo = subjectPublicKeyInfo;
+ this.extensions = extensions;
+ }
+
+ /**
+ * EFFECTS: Parse input DER.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect parent / inner tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - extensions are specified, but the version is v1 or v2
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public TbsCertificate(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ int i = encoded.getIndex();
+ if (encoded.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, true, 0))) {
+ this.version = new Int(encoded, true);
+ } else {
+ this.version = null;
+ }
+ this.serialNumber = new Int(encoded, false);
+ this.signature = new AlgorithmIdentifier(encoded, false);
+ this.issuer = new Name(encoded, false);
+ this.validity = new Validity(encoded, false);
+ this.subject = new Name(encoded, false);
+ this.subjectPublicKeyInfo = new SubjectPublicKeyInfo(encoded, false);
+ if (encoded.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, true, 3))) {
+ this.extensions = new Extensions(encoded, true);
+ } else {
+ // Enforce the extensions tag - nothing else should be here.
+ if (Integer.compareUnsigned(getLength(), (encoded.getIndex() - i)) != 0) {
+ new Tag(encoded).enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 3));
+ }
+ this.extensions = null;
+ }
+ enforceInput();
+ enforceVersion();
+ }
+
+ /**
+ * EFFECTS: Throw {@link ParseException} if any field have illegal tags.
+ */
+ private void enforceInput() throws ParseException {
+ if (this.version != null) {
+ this.version.getTag().enforce(Int.TAG);
+ this.version.getParentTag().enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 0));
+ }
+ this.serialNumber.getTag().enforce(Int.TAG);
+ this.signature.getTag().enforce(TAG_SEQUENCE);
+ this.issuer.getTag().enforce(TAG_SEQUENCE);
+ this.validity.getTag().enforce(TAG_SEQUENCE);
+ this.subject.getTag().enforce(TAG_SEQUENCE);
+ this.subjectPublicKeyInfo.getTag().enforce(TAG_SEQUENCE);
+ if (extensions != null) {
+ this.extensions.getTag().enforce(TAG_SEQUENCE);
+ this.extensions.getParentTag().enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 3));
+ }
+ }
+
+ /**
+ * EFFECTS: Throw {@link ParseException} if the version is incorrect.
+ */
+ private void enforceVersion() throws ParseException {
+ if (version != null
+ && (version.getLong() != VERSION_V1
+ && version.getLong() != VERSION_V2
+ && version.getLong() != VERSION_V3)) {
+ throw new ParseException("Illegal certificate version: " + version.getLong());
+ }
+ if (extensions != null && (version == null || version.getLong() != VERSION_V3)) {
+ throw new ParseException("Extensions present. The version must be v3 or above.");
+ }
+ }
+
+ /**
+ * EFFECTS: Encode into ordered DER.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(version == null ? Collections.<Byte>emptyList() : Arrays.asList(version.encodeDER()),
+ Arrays.asList(serialNumber.encodeDER()),
+ Arrays.asList(signature.encodeDER()),
+ Arrays.asList(issuer.encodeDER()),
+ Arrays.asList(validity.encodeDER()),
+ Arrays.asList(subject.encodeDER()),
+ Arrays.asList(subjectPublicKeyInfo.encodeDER()),
+ extensions == null ? Collections.<Byte>emptyList()
+ : Arrays.asList(extensions.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Int getVersion() {
+ return version;
+ }
+
+ public Int getSerialNumber() {
+ return serialNumber;
+ }
+
+ public AlgorithmIdentifier getSignature() {
+ return signature;
+ }
+
+ public Name getIssuer() {
+ return issuer;
+ }
+
+ public Validity getValidity() {
+ return validity;
+ }
+
+ public Name getSubject() {
+ return subject;
+ }
+
+ public SubjectPublicKeyInfo getSubjectPublicKeyInfo() {
+ return subjectPublicKeyInfo;
+ }
+
+ public Extensions getExtensions() {
+ return extensions;
+ }
+}
diff --git a/src/main/model/pki/cert/Validity.java b/src/main/model/pki/cert/Validity.java
new file mode 100644
index 0000000..76279ed
--- /dev/null
+++ b/src/main/model/pki/cert/Validity.java
@@ -0,0 +1,95 @@
+package model.pki.cert;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents the following ASN.1 structure:
+ * <pre>
+ * Validity ::= SEQUENCE {
+ * notBefore Time,
+ * notAfter Time,
+ * ...
+ * }
+ *
+ * Time ::= CHOICE {
+ * utcTime UTCTime,
+ * generalizedTime GeneralizedTime
+ * }
+ * </pre>
+ * It describes the validity period of the certificate.
+ */
+public class Validity extends ASN1Object {
+ /**
+ * The certificate is not valid before that time.
+ */
+ private final ASN1Time notBefore;
+
+ /**
+ * The certificate is not valid after that time.
+ */
+ private final ASN1Time notAfter;
+
+ /**
+ * EFFECTS: Init with the given tag, parentTag, notBefore, and notAfter. For more info on tag and parentTag, see
+ * {@link ASN1Object}.
+ * REQUIRES: notBefore and notAfter are either UTCTime or GeneralizedTime.
+ */
+ public Validity(Tag tag, Tag parentTag,
+ ASN1Time notBefore, ASN1Time notAfter) {
+ super(tag, parentTag);
+ this.notBefore = notBefore;
+ this.notAfter = notAfter;
+ }
+
+ /**
+ * EFFECTS: Parse input DER.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing (info, algorithm, signature)
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public Validity(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ if (encoded.detectTag(GeneralizedTime.TAG)) {
+ this.notBefore = new GeneralizedTime(encoded, false);
+ this.notBefore.getTag().enforce(GeneralizedTime.TAG);
+ } else {
+ this.notBefore = new UtcTime(encoded, false);
+ this.notBefore.getTag().enforce(UtcTime.TAG);
+ }
+ if (encoded.detectTag(GeneralizedTime.TAG)) {
+ this.notAfter = new GeneralizedTime(encoded, false);
+ this.notAfter.getTag().enforce(GeneralizedTime.TAG);
+ } else {
+ this.notAfter = new UtcTime(encoded, false);
+ this.notAfter.getTag().enforce(UtcTime.TAG);
+ }
+ }
+
+ /**
+ * EFFECTS: Encode into ordered DER.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(notBefore.encodeDER()),
+ Arrays.asList(notAfter.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public ASN1Time getNotBefore() {
+ return notBefore;
+ }
+
+ public ASN1Time getNotAfter() {
+ return notAfter;
+ }
+}
diff --git a/src/main/model/pki/crl/CertificateList.java b/src/main/model/pki/crl/CertificateList.java
new file mode 100644
index 0000000..5142101
--- /dev/null
+++ b/src/main/model/pki/crl/CertificateList.java
@@ -0,0 +1,76 @@
+package model.pki.crl;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+import model.pki.cert.TbsCertificate;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents a signed X.509 CRL.
+ * <pre>
+ * CertificateList ::= SIGNED{CertificateListContent}
+ * </pre>
+ */
+public class CertificateList extends ASN1Object {
+ /**
+ * All info of that CRL, excluding the signature.
+ * It will be signed, and the signature is in <pre>signature</pre>.
+ */
+ private final CertificateListContent crl;
+
+ /**
+ * The algorithm used for <pre>signature</pre>.
+ */
+ private final AlgorithmIdentifier signatureAlgorithm;
+
+ /**
+ * The signature.
+ */
+ private final BitString signature;
+
+ /**
+ * EFFECTS: Initialize the object with the given tag and parentTag, and list, signatureAlgorithm, and signature.
+ * REQUIRES: The algorithm must match the signature. The fields must have correct tags as described in the class
+ * specification (SEQUENCE, SEQUENCE, BIT STRING).
+ */
+ public CertificateList(Tag tag, Tag parentTag,
+ final CertificateListContent crl,
+ final AlgorithmIdentifier signatureAlgorithm,
+ final BitString signature) {
+ super(tag, parentTag);
+ this.crl = crl;
+ this.signatureAlgorithm = signatureAlgorithm;
+ this.signature = signature;
+ }
+
+ /**
+ * EFFECT: Encode that sequence into an ordered array of bytes, following the class specification.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(crl.encodeDER()),
+ Arrays.asList(signatureAlgorithm.encodeDER()),
+ Arrays.asList(signature.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public CertificateListContent getCrl() {
+ return crl;
+ }
+
+ public AlgorithmIdentifier getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public BitString getSignature() {
+ return signature;
+ }
+}
diff --git a/src/main/model/pki/crl/CertificateListContent.java b/src/main/model/pki/crl/CertificateListContent.java
new file mode 100644
index 0000000..6f75d71
--- /dev/null
+++ b/src/main/model/pki/crl/CertificateListContent.java
@@ -0,0 +1,106 @@
+package model.pki.crl;
+
+import model.asn1.*;
+import model.pki.AlgorithmIdentifier;
+import model.x501.Name;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Represents a CRL content:
+ *
+ * <pre>
+ * CertificateListContent ::= SEQUENCE {
+ * version Version OPTIONAL,
+ * -- if present, version shall be v2
+ * signature AlgorithmIdentifier{{SupportedAlgorithms}},
+ * issuer Name,
+ * thisUpdate Time,
+ * nextUpdate Time OPTIONAL,
+ * revokedCertificates SEQUENCE OF SEQUENCE {
+ * serialNumber CertificateSerialNumber,
+ * revocationDate Time,
+ * crlEntryExtensions Extensions OPTIONAL,
+ * ...} OPTIONAL,
+ * ...,
+ * ...,
+ * crlExtensions [0] Extensions OPTIONAL }
+ * </pre>
+ *
+ * A CRL is a signed object published by the CA that revokes any certificates signed by this CA before their
+ * expiration. Relying-parties should check the CRL from corresponding CDPs to see if the certificate to check is
+ * already revoked.
+ * Because the CA will only generate CRLs, this object won't be parsed.
+ */
+public class CertificateListContent extends ASN1Object {
+ private final Int version = new Int(Int.TAG, null, 1);
+ private final Name issuer;
+ private final AlgorithmIdentifier signature;
+ private final ASN1Time thisUpdate;
+ private final ASN1Time nextUpdate;
+ private final RevokedCertificate[] revokedCertificates;
+
+ /**
+ * EFFECTS: Init with tags and the given parameters. Version is always set to 1.
+ * REQUIRES: except for nextUpdate, all other fields are non-null; items in revokedCerts should be SEQUENCE.
+ */
+ public CertificateListContent(Tag tag, Tag parentTag,
+ Name issuer,
+ AlgorithmIdentifier signature,
+ ASN1Time thisUpdate,
+ ASN1Time nextUpdate,
+ RevokedCertificate[] revokedCertificates) {
+ super(tag, parentTag);
+ this.issuer = issuer;
+ this.signature = signature;
+ this.thisUpdate = thisUpdate;
+ this.nextUpdate = nextUpdate;
+ this.revokedCertificates = revokedCertificates;
+ }
+
+ @Override
+ public Byte[] encodeValueDER() {
+ final List<Byte> itemsEncoded = Arrays.stream(revokedCertificates)
+ .map(Encodable::encodeDER)
+ .flatMap(Arrays::stream)
+ .collect(Collectors.toList());
+ return Stream.of(Arrays.asList(version.encodeDER()),
+ Arrays.asList(issuer.encodeDER()),
+ Arrays.asList(signature.encodeDER()),
+ Arrays.asList(thisUpdate.encodeDER()),
+ nextUpdate == null ? Collections.<Byte>emptyList() : Arrays.asList(nextUpdate.encodeDER()),
+ Arrays.asList(new Tag(TagClass.UNIVERSAL, true, 0x30).encodeDER()),
+ Arrays.asList(new ASN1Length(itemsEncoded.size()).encodeDER()), itemsEncoded)
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Int getVersion() {
+ return version;
+ }
+
+ public Name getIssuer() {
+ return issuer;
+ }
+
+ public AlgorithmIdentifier getSignature() {
+ return signature;
+ }
+
+ public ASN1Time getThisUpdate() {
+ return thisUpdate;
+ }
+
+ public ASN1Time getNextUpdate() {
+ return nextUpdate;
+ }
+
+ public RevokedCertificate[] getRevokedCertificates() {
+ return revokedCertificates;
+ }
+}
diff --git a/src/main/model/pki/crl/Reason.java b/src/main/model/pki/crl/Reason.java
new file mode 100644
index 0000000..e47609e
--- /dev/null
+++ b/src/main/model/pki/crl/Reason.java
@@ -0,0 +1,27 @@
+package model.pki.crl;
+
+/**
+ * Identify the reason for revocation.
+ */
+public enum Reason {
+ UNSPECIFIED(0),
+ KEY_COMPROMISE(1),
+ CA_COMPROMISE(2),
+ AFFILIATION_CHANGED(3),
+ SUPERSEDED(4),
+ CESSATION_OF_OPERATION(5);
+
+ private final int val;
+
+ /**
+ * EFFECTS: Init with the specific val.
+ * REQUIRES: 0 <= val <= 0xFF
+ */
+ Reason(int val) {
+ this.val = val;
+ }
+
+ public int getVal() {
+ return val;
+ }
+}
diff --git a/src/main/model/pki/crl/RevokedCertificate.java b/src/main/model/pki/crl/RevokedCertificate.java
new file mode 100644
index 0000000..457ecb8
--- /dev/null
+++ b/src/main/model/pki/crl/RevokedCertificate.java
@@ -0,0 +1,72 @@
+package model.pki.crl;
+
+import model.asn1.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Indicates the revocation status of a certificate, given its serial number, revocation date, and reason.
+ * <pre>
+ * SEQUENCE {
+ * serialNumber CertificateSerialNumber,
+ * revocationDate Time,
+ * crlEntryExtensions Extensions OPTIONAL,
+ * ...}
+ * </pre>
+ */
+public class RevokedCertificate extends ASN1Object {
+ private final Int serialNumber;
+ private final ASN1Time revocationDate;
+ private final Reason reason;
+
+ /**
+ * EFFECT: Init with tags and parameters. See {@link ASN1Object} for tags.
+ * REQUIRES: revocationDate should be either UtcTime or GeneralTime.
+ */
+ public RevokedCertificate(Tag tag, Tag parentTag,
+ Int serialNumber,
+ ASN1Time revocationDate,
+ Reason reason) {
+ super(tag, parentTag);
+ this.serialNumber = serialNumber;
+ this.revocationDate = revocationDate;
+ this.reason = reason;
+ }
+
+ @Override
+ public Byte[] encodeValueDER() {
+ final Byte[] r = new OctetString(OctetString.TAG,
+ null,
+ new Byte[]{ 0x0A, 0x01, (byte) reason.getVal() })
+ .encodeDER();
+ final Byte[] oid = new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_CRL_REASON)
+ .encodeDER();
+ final Byte[] seqExt = Stream.of(Arrays.asList(TAG_SEQUENCE.encodeDER()),
+ Arrays.asList(new ASN1Length(r.length + oid.length).encodeDER()),
+ Arrays.asList(oid),
+ Arrays.asList(r))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ return Stream.of(Arrays.asList(serialNumber.encodeDER()),
+ Arrays.asList(revocationDate.encodeDER()),
+ Arrays.asList(TAG_SEQUENCE.encodeDER()),
+ Arrays.asList(new ASN1Length(seqExt.length).encodeDER()),
+ Arrays.asList(seqExt))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Int getSerialNumber() {
+ return serialNumber;
+ }
+
+ public ASN1Time getRevocationDate() {
+ return revocationDate;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+}
diff --git a/src/main/model/x501/AttributeTypeAndValue.java b/src/main/model/x501/AttributeTypeAndValue.java
new file mode 100644
index 0000000..d43d137
--- /dev/null
+++ b/src/main/model/x501/AttributeTypeAndValue.java
@@ -0,0 +1,90 @@
+package model.x501;
+
+import model.asn1.ASN1Object;
+import model.asn1.ObjectIdentifier;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.csr.Values;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Implements the following:
+ * <pre>
+ * AttributeTypeAndValue ::= SEQUENCE {
+ * type ATTRIBUTE.&id({SupportedAttributes}),
+ * value ATTRIBUTE.&Type({SupportedAttributes}{@type}),
+ * ... }
+ * </pre>
+ */
+public class AttributeTypeAndValue extends ASN1Object {
+ /**
+ * The type of that attribute. For example, <pre>2.5.4.10</pre> is OU.
+ * It determines the format of the value.
+ */
+ private final ObjectIdentifier type;
+
+ /**
+ * Value corresponding to type.
+ */
+ private final ASN1Object value;
+
+ /**
+ * EFFECT: Init the object with tag, parentTag, type, and values. For tag and parentTag, see {@link ASN1Object}.
+ * REQUIRES: The values must match the type. Type tag should be UNIVERSAL OID.
+ */
+ public AttributeTypeAndValue(Tag tag, Tag parentTag,
+ ObjectIdentifier type, ASN1Object value) {
+ super(tag, parentTag);
+ this.type = type;
+ this.value = value;
+ }
+
+ /**
+ * EFFECTS: Parse input DER. Value is not checked against the type.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public AttributeTypeAndValue(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.type = new ObjectIdentifier(encoded, false);
+ this.type.getTag().enforce(ObjectIdentifier.TAG);
+
+ this.value = ASN1Object.parse(encoded, false);
+ }
+
+ /**
+ * EFFECTS: Encode the fields into DER, in the order.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(type.encodeDER()),
+ Arrays.asList(value.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ /**
+ * EFFECTS: Return in TYPE=Value format. Type will be either x.x.x.x.x or human-readable strings like CN. Value is
+ * input-defined.
+ */
+ @Override
+ public String toString() {
+ return type.toString() + "=" + value.toString();
+ }
+
+ public ObjectIdentifier getType() {
+ return type;
+ }
+
+ public ASN1Object getValue() {
+ return value;
+ }
+}
diff --git a/src/main/model/x501/Name.java b/src/main/model/x501/Name.java
new file mode 100644
index 0000000..dd2acb6
--- /dev/null
+++ b/src/main/model/x501/Name.java
@@ -0,0 +1,79 @@
+package model.x501;
+
+import model.asn1.ASN1Object;
+import model.asn1.Encodable;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Represents an X.501 directory Name (a.k.a. RDNSequence).
+ * <pre>
+ * Name ::= CHOICE { -- only one possibility for now -- rdnSequence RDNSequence }
+ * RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
+ * DistinguishedName ::= RDNSequence
+ * </pre>
+ */
+public class Name extends ASN1Object {
+ private final RelativeDistinguishedName[] rdnSequence;
+
+ /**
+ * EFFECT: Initialize the Name with the given tags and rdnSequence. For tag and parentTag, consult
+ * {@link ASN1Object}.
+ * REQUIRES: Items should have SET tag.
+ */
+ public Name(Tag tag, Tag parentTag, RelativeDistinguishedName[] rdnSequence) {
+ super(tag, parentTag);
+ this.rdnSequence = rdnSequence;
+ }
+
+ /**
+ * EFFECT: Parse the Name from input DER bytes. For details on parsing, refer to {@link ASN1Object}.
+ * Throws {@link ParseException} for invalid input.
+ * MODIFIES: this, encoded
+ */
+ public Name(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ final List<RelativeDistinguishedName> list = new ArrayList<>();
+ for (int i = 0; i < getLength();) {
+ int index = encoded.getIndex();
+ final RelativeDistinguishedName name = new RelativeDistinguishedName(encoded, false);
+ name.getTag().enforce(TAG_SET);
+ list.add(name);
+ index = encoded.getIndex() - index;
+ i += index;
+ }
+ this.rdnSequence = list.toArray(new RelativeDistinguishedName[0]);
+ }
+
+ /**
+ * EFFECTS: Encode the SEQUENCE OF into DER, keep order. RDNs will be encoded one-by-one.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(rdnSequence)
+ .map(Encodable::encodeDER)
+ .flatMap(Arrays::stream)
+ .toArray(Byte[]::new);
+ }
+
+ /**
+ * EFFECT: Convert the name into directory string, like CN=yuuta,OU=users,DC=yuuta,DC=moe
+ */
+ @Override
+ public String toString() {
+ return Stream.of(rdnSequence)
+ .map(RelativeDistinguishedName::toString)
+ .collect(Collectors.joining(","));
+ }
+
+ public RelativeDistinguishedName[] getRdnSequence() {
+ return rdnSequence;
+ }
+}
diff --git a/src/main/model/x501/RelativeDistinguishedName.java b/src/main/model/x501/RelativeDistinguishedName.java
new file mode 100644
index 0000000..8edde09
--- /dev/null
+++ b/src/main/model/x501/RelativeDistinguishedName.java
@@ -0,0 +1,78 @@
+package model.x501;
+
+import model.asn1.ASN1Object;
+import model.asn1.Encodable;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Represents a DN item.
+ * <pre>
+ * RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
+ * </pre>
+ * For more information on DN, see {@link Name}.
+ */
+public class RelativeDistinguishedName extends ASN1Object {
+ private final AttributeTypeAndValue[] array;
+
+ /**
+ * EFFECT: Initialize the list with the given tag, parentTag, and array. For tag and parentTag, consult
+ * {@link ASN1Object}.
+ * REQUIRES: Array items should have UNIVERSAL SEQUENCE tag.
+ */
+ public RelativeDistinguishedName(Tag tag, Tag parentTag, AttributeTypeAndValue[] array) {
+ super(tag, parentTag);
+ this.array = array;
+ }
+
+ /**
+ * EFFECT: Parse the list from input DER bytes. For details on parsing, refer to {@link ASN1Object}.
+ * Throws {@link ParseException} for invalid input.
+ * MODIFIES: this, encoded
+ */
+ public RelativeDistinguishedName(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ final List<AttributeTypeAndValue> list = new ArrayList<>();
+ for (int i = 0; i < getLength();) {
+ int index = encoded.getIndex();
+ final AttributeTypeAndValue value = new AttributeTypeAndValue(encoded, false);
+ value.getTag().enforce(TAG_SEQUENCE);
+ list.add(value);
+ index = encoded.getIndex() - index;
+ i += index;
+ }
+ this.array = list.toArray(new AttributeTypeAndValue[0]);
+ }
+
+ /**
+ * EFFECTS: Encode the SET OF into DER, keep order. Values will be encoded one-by-one.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(array)
+ .map(Encodable::encodeDER)
+ .flatMap(Arrays::stream)
+ .toArray(Byte[]::new);
+ }
+
+ /**
+ * EFFECT: Encode into multi-valed RDN strings like CN=yuuta+CN=qwq
+ */
+ @Override
+ public String toString() {
+ return Stream.of(array)
+ .map(AttributeTypeAndValue::toString)
+ .collect(Collectors.joining("+"));
+ }
+
+ public AttributeTypeAndValue[] getArray() {
+ return array;
+ }
+}
diff --git a/src/main/ui/Utils.java b/src/main/ui/Utils.java
new file mode 100644
index 0000000..ccb244e
--- /dev/null
+++ b/src/main/ui/Utils.java
@@ -0,0 +1,105 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * Useful small methods for the whole program.
+ */
+public final class Utils {
+ /**
+ * EFFECTS: Convert the input primitive byte array into the boxed Byte array.
+ */
+ public static Byte[] byteToByte(byte[] array) {
+ Byte[] arr = new Byte[array.length];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = array[i];
+ }
+ return arr;
+ }
+
+ /**
+ * EFFECTS: Convert the input boxed Byte array into primitive byte array.
+ */
+ public static byte[] byteToByte(Byte[] array) {
+ byte[] arr = new byte[array.length];
+ for (int i = 0; i < arr.length; i++) {
+ arr[i] = array[i];
+ }
+ return arr;
+ }
+
+ /**
+ * EFFECTS: Pack the big-endian bytes into a 32bit integer.
+ * Throws {@link model.asn1.exceptions.ParseException} if the value is too large.
+ */
+ public static int bytesToInt(Byte[] array) throws ParseException {
+ try {
+ return new BigInteger(byteToByte(array)).intValueExact();
+ } catch (ArithmeticException ignored) {
+ throw new ParseException("Value is too large.");
+ }
+ }
+
+ /**
+ * EFFECTS: Pack the big-endian bytes into a 64bit integer.
+ * Throws {@link model.asn1.exceptions.ParseException} if the value is too large.
+ */
+ public static long bytesToLong(Byte[] array) throws ParseException {
+ try {
+ return new BigInteger(byteToByte(array)).longValueExact();
+ } catch (ArithmeticException ignored) {
+ throw new ParseException("Value is too large.");
+ }
+ }
+
+ /**
+ * EFFECTS: Unpack the multibyte 64bit integer to its shortest array of byte format.
+ */
+ public static Byte[] valToByte(long val) {
+ byte[] v = BigInteger.valueOf(val).toByteArray();
+ if (val != 0 && v[0] == 0) {
+ if (v.length == 1) {
+ return new Byte[0];
+ }
+ byte[] arr = new byte[v.length - 1];
+ System.arraycopy(v, 1, arr, 0, arr.length);
+ return byteToByte(arr);
+ }
+ return byteToByte(v);
+ }
+
+ /**
+ * EFFECTS: Parse the two-digit octet string into an unsigned byte, preserving leading zero and negative values.
+ * REQUIRES: The input octet must be a two-char string, with each char matching [0-9][A-F].
+ */
+ public static Byte parseByte(String octet) {
+ return (byte) Integer.parseInt(octet, 16);
+ }
+
+ /**
+ * EFFECTS: Decode the input PEM file, with optional check on tags.
+ * Throws {@link ParseException} if the desiredTag is specified but the input does not have the specific tag, or
+ * if the input does not have any tags at all (not a PEM).
+ */
+ public static Byte[] parsePEM(Byte[] input, String desiredTag) throws ParseException {
+ final String str = new String(byteToByte(input), StandardCharsets.UTF_8);
+ Pattern pattern =
+ Pattern.compile("^-----BEGIN " + desiredTag
+ + "-----$\n^(.*)$\n^-----END " + desiredTag + "-----$",
+ Pattern.DOTALL | Pattern.MULTILINE);
+ final Matcher matcher = pattern.matcher(str);
+ if (!matcher.matches()) {
+ throw new ParseException("Not a valid PEM");
+ }
+ final String b64 = matcher.group(1).replace("\n", "");
+ return byteToByte(Base64.getDecoder().decode(b64));
+ }
+}
diff --git a/src/test/model/MyModelTest.java b/src/test/model/MyModelTest.java
deleted file mode 100644
index 91b287a..0000000
--- a/src/test/model/MyModelTest.java
+++ /dev/null
@@ -1,5 +0,0 @@
-package model;
-
-class MyModelTest {
- // delete or rename this class!
-} \ No newline at end of file
diff --git a/src/test/model/TestConstants.java b/src/test/model/TestConstants.java
new file mode 100644
index 0000000..3356549
--- /dev/null
+++ b/src/test/model/TestConstants.java
@@ -0,0 +1,544 @@
+package model;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.csr.Attribute;
+import model.csr.Attributes;
+import model.csr.Values;
+import model.pki.AlgorithmIdentifier;
+import model.pki.SubjectPublicKeyInfo;
+import model.pki.cert.Extension;
+import model.pki.cert.Extensions;
+import model.pki.cert.TbsCertificate;
+import model.pki.cert.Validity;
+import model.pki.crl.CertificateListContent;
+import model.pki.crl.Reason;
+import model.pki.crl.RevokedCertificate;
+import model.x501.AttributeTypeAndValue;
+import model.x501.Name;
+import model.x501.RelativeDistinguishedName;
+import ui.Utils;
+
+import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static model.asn1.ASN1Object.TAG_SEQUENCE;
+import static model.asn1.ASN1Object.TAG_SET;
+import static model.asn1.ObjectIdentifier.OID_EXTENSION_REQUEST;
+import static model.asn1.ObjectIdentifier.TAG;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public final class TestConstants {
+ // An opaque value of extended key usage block.
+ public static final ASN1Object EXT_KEY_USAGE;
+
+ // Example SubjectAlternativeName request attribute.
+ public static final ASN1Object SAN;
+
+ public static final Values CSR_ATTR_VALUES_2;
+
+ public static final Attribute CSR_ATTR_2;
+
+ public static final Byte[] CSR_ATTR_VALUES_2_DER = new Byte[]{
+ 0x30, 81, // Attribute
+ // 1.2.840.1113549.1.9.14
+ 0x06, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, 0x01, 0x09, 0x0E,
+
+ 0x31, 68, // Values
+
+ 0x30, 0x1F, // SEQUENCE (AttributeValue)
+ 0x30, 0x1D, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage
+ 0x04, 0x16, // OCTET STRING
+ 0x30, 0x14, // SEQUENCE
+ // 1.3.6.1.5.5.7.3.1 serverAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
+ // 1.3.6.1.5.5.7.3.2 clientAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02,
+
+ 0x30, 0x21, // SEQUENCE (AttributeValue)
+ 0x30, 0x1F, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName
+ 0x04, 0x18, // OCTET STRING
+ 0x30, 0x16, // SEQUENCE
+ -126, 0x14, // [2]
+ 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe
+ 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65
+ };
+
+ public static final Attribute CSR_ATTR_1;
+
+ public static final Byte[] CSR_ATTR_1_DER = new Byte[] {
+ 0x30, 0x1C,
+ 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0D, 0x02, 0x03, 0x31, 0x0E, 0x16,
+ 0x0C, 0x31, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x39, 0x30, 0x34, 0x35, 0x2E, 0x32
+ };
+
+ public static final Attributes CSR_ATTRS_2;
+
+ public static final Byte[] L_MILANO_DER = new Byte[]{
+ 0x30, 0x0D, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0C,
+ 0x06, 0x4D, 0x69, 0x6C, 0x61, 0x6E, 0x6F
+ };
+ public static final RelativeDistinguishedName L_MILANO;
+ public static final Byte[] CN_TEST_ED25519_DER = new Byte[] {
+ 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C,
+ 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64,
+ 0x32, 0x35, 0x35, 0x31, 0x39,
+ };
+ public static final RelativeDistinguishedName L_MILANO_CN_TEST_ED25519;
+
+ // CN = yuuta
+ public static final RelativeDistinguishedName CN_YUUTA;
+
+ // OU = users
+ public static final RelativeDistinguishedName OU_USERS;
+
+ // C = CA
+ public static final RelativeDistinguishedName C_CA;
+
+ // CN = yuuta, OU = users, C = CA
+ public static final Name NAME_1;
+
+ // SN = Qwq
+ public static final RelativeDistinguishedName SN_QWQ;
+
+ // O = IT
+ public static final RelativeDistinguishedName O_IT;
+
+ // C = CN
+ public static final RelativeDistinguishedName C_CN;
+
+ // SN = Qwq, O = IT, C = CN
+ public static final Name NAME_2;
+
+ /**
+ * Certificate:
+ * Data:
+ * Version: 3 (0x2)
+ * Serial Number:
+ * 70:fa:0f:fa:a6:d7:f4:b4:93:05:5d:a9:d3:e4:42:a8:52:60:b3:f8
+ * Signature Algorithm: ecdsa-with-SHA256
+ * Issuer: CN = Yuuta Root CA, C = CA
+ * Validity
+ * Not Before: Jun 23 02:50:46 2023 GMT
+ * Not After : Jun 23 02:50:46 2048 GMT
+ * Subject: CN = Yuuta Root CA, C = CA
+ * Subject Public Key Info:
+ * Public Key Algorithm: id-ecPublicKey
+ * Public-Key: (256 bit)
+ * pub:
+ * ASN1 OID: prime256v1
+ * NIST CURVE: P-256
+ * X509v3 extensions:
+ * X509v3 Subject Key Identifier:
+ * 78:92:E0:6C:70:F5:A3:BE:02:EE:44:BA:A7:8C:DA:D6:B5:43:A7:93
+ * X509v3 Authority Key Identifier:
+ * 78:92:E0:6C:70:F5:A3:BE:02:EE:44:BA:A7:8C:DA:D6:B5:43:A7:93
+ * X509v3 Basic Constraints: critical
+ * CA:TRUE
+ * X509v3 Key Usage: critical
+ * Digital Signature, Certificate Sign, CRL Sign
+ * Signature Algorithm: ecdsa-with-SHA256
+ * Signature Value:
+ */
+ public static final String CERT_L1_ECC_PEM =
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIBrzCCAVWgAwIBAgIUcPoP+qbX9LSTBV2p0+RCqFJgs/gwCgYIKoZIzj0EAwIw\n" +
+ "JTEWMBQGA1UEAwwNWXV1dGEgUm9vdCBDQTELMAkGA1UEBhMCQ0EwHhcNMjMwNjIz\n" +
+ "MDI1MDQ2WhcNNDgwNjIzMDI1MDQ2WjAlMRYwFAYDVQQDDA1ZdXV0YSBSb290IENB\n" +
+ "MQswCQYDVQQGEwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB3ocbzdSHAm\n" +
+ "cWzdBFs/Xd4UMYs/MYAYKjPlGYYT1udIL5UVOlmN7QnkUxrzYbI1YW5mX1/PCuJl\n" +
+ "ZT0iKzBxLCSjYzBhMB0GA1UdDgQWBBR4kuBscPWjvgLuRLqnjNrWtUOnkzAfBgNV\n" +
+ "HSMEGDAWgBR4kuBscPWjvgLuRLqnjNrWtUOnkzAPBgNVHRMBAf8EBTADAQH/MA4G\n" +
+ "A1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNIADBFAiAJr6S6xkgxMitFmnS/0cIu\n" +
+ "E7W9Ykii5d2Fe9+Lu4nL9wIhAIwHnAvZ4YzwfY6P5K4SaBwzzKPiq2zdpaXBm0lX\n" +
+ "qcsz\n" +
+ "-----END CERTIFICATE-----";
+ /**
+ * Certificate:
+ * Data:
+ * Version: 3 (0x2)
+ * Serial Number:
+ * 3e:10:93:9d:e4:57:8d:39:87:fd:ff:42:7b:da:65:5b:1f:21:cb:07
+ * Signature Algorithm: ecdsa-with-SHA512
+ * Issuer: CN = Yuuta Root CA, C = CA
+ * Validity
+ * Not Before: Jun 24 00:15:22 2023 GMT
+ * Not After : Jun 21 00:15:22 2033 GMT
+ * Subject: DC = MOE, DC = YUUTA, DC = AD, CN = Yuuta Home Issuing CA
+ * Subject Public Key Info:
+ * Public Key Algorithm: rsaEncryption
+ * Public-Key: (4096 bit)
+ * Modulus:
+ * Exponent: 65537 (0x10001)
+ * X509v3 extensions:
+ * X509v3 Subject Key Identifier:
+ * B1:C2:A7:81:63:66:4B:72:0A:DD:FD:7D:20:29:BD:6B:49:09:61:C0
+ * X509v3 Authority Key Identifier:
+ * 78:92:E0:6C:70:F5:A3:BE:02:EE:44:BA:A7:8C:DA:D6:B5:43:A7:93
+ * X509v3 Basic Constraints: critical
+ * CA:TRUE, pathlen:0
+ * X509v3 Key Usage: critical
+ * Digital Signature, Certificate Sign, CRL Sign
+ * X509v3 CRL Distribution Points:
+ * Full Name:
+ * URI:http://home.yuuta.moe/pki/rootca.crl
+ * Authority Information Access:
+ * CA Issuers - URI:http://home.yuuta.moe/pki/rootca.crt
+ * Signature Algorithm: ecdsa-with-SHA512
+ * Signature Value:
+ */
+ public static final String CERT_L2_RSA_PEM =
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIEMjCCA9mgAwIBAgIUPhCTneRXjTmH/f9Ce9plWx8hywcwCgYIKoZIzj0EAwQw\n" +
+ "JTEWMBQGA1UEAwwNWXV1dGEgUm9vdCBDQTELMAkGA1UEBhMCQ0EwHhcNMjMwNjI0\n" +
+ "MDAxNTIyWhcNMzMwNjIxMDAxNTIyWjBgMRMwEQYKCZImiZPyLGQBGRYDTU9FMRUw\n" +
+ "EwYKCZImiZPyLGQBGRYFWVVVVEExEjAQBgoJkiaJk/IsZAEZFgJBRDEeMBwGA1UE\n" +
+ "AwwVWXV1dGEgSG9tZSBJc3N1aW5nIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A\n" +
+ "MIICCgKCAgEA2WTLz4X8B2XN/AiDJNsQQNMiBDjye/TfpbC3dLsUZr4BneH9EX+I\n" +
+ "lKkaoJaSIXU7iXoN43FeLX5iuoq+aj4f3x+qevWySD7xLLt8gpozN5MKBdRXtaem\n" +
+ "4js3Nm3YLUbjv115sddHN/3QTQXgBSUGdjyi8woL54IKdKYzB1g2Jn2Et383usMA\n" +
+ "yHd3gCbwszvE5jpOgBIHxZMgMnmVAQhbQNzoEDMIkwaXmSt4jwX03oigf0KAaD+a\n" +
+ "XIwQRl15iIDZnG6rRw6+eiIR8c+x1ot1/u5qncwNhRUtLbbX3QfBQ6D/XBSfrqmA\n" +
+ "zhddM/i2Qt5Iw44CcLSGujFeb9ybU7NLx02EjfQsSAUGQR4VuXyD+//FsLYkh7g3\n" +
+ "WmdBTWzIhVnYEU9ohTeXaZZNTp9T67czqnntFbaCdOxnwOrcmFt1v0skrHd5mHKe\n" +
+ "1W3OU6XOjM6vQwcwhPUUGxAXYBcqwQ84fzD26CZz5g8I8HpnpmJ+SNtFIg+SnPOs\n" +
+ "sslnsoeMZpDPESwORYgayXIWkglop1fYeD4/ictH4me70vOIHF9fWqI8ydHoNxuw\n" +
+ "uZjZDa0mQgsHTmr40NhDLP/q6MEnS2w/MwHuSd3YbhbjPWFbu0Zo7XreiRkXjRLa\n" +
+ "R22XkuH+FkEGB3ZxQVIkkWf1znaKQS+ZdPuTzpZph5BPL50gE58k+i0CAwEAAaOB\n" +
+ "4DCB3TAdBgNVHQ4EFgQUscKngWNmS3IK3f19ICm9a0kJYcAwHwYDVR0jBBgwFoAU\n" +
+ "eJLgbHD1o74C7kS6p4za1rVDp5MwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B\n" +
+ "Af8EBAMCAYYwNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2hvbWUueXV1dGEubW9l\n" +
+ "L3BraS9yb290Y2EuY3JsMEAGCCsGAQUFBwEBBDQwMjAwBggrBgEFBQcwAoYkaHR0\n" +
+ "cDovL2hvbWUueXV1dGEubW9lL3BraS9yb290Y2EuY3J0MAoGCCqGSM49BAMEA0cA\n" +
+ "MEQCIHShp7SwbQ2rQC7l8u4u9rSU6Zl4DRfyor4jiHGAjm0gAiAbOHk6q+3Vm3uq\n" +
+ "Jj92o1yDl09pFNIryojHMDRBpHl6yQ==\n" +
+ "-----END CERTIFICATE-----";
+
+ /**
+ * Certificate:
+ * Data:
+ * Version: 1 (0x0)
+ * Serial Number: 3580 (0xdfc)
+ * Signature Algorithm: sha1WithRSAEncryption
+ * Issuer: C = JP, ST = Tokyo, L = Chuo-ku, O = Frank4DD, OU = WebCert Support, CN = Frank4DD Web CA, emailAddress = support@frank4dd.com
+ * Validity
+ * Not Before: Aug 22 05:27:41 2012 GMT
+ * Not After : Aug 21 05:27:41 2017 GMT
+ * Subject: C = JP, ST = Tokyo, O = Frank4DD, CN = www.example.com
+ * Subject Public Key Info:
+ * Public Key Algorithm: rsaEncryption
+ * Public-Key: (2048 bit)
+ * Modulus:
+ * Exponent: 65537 (0x10001)
+ * Signature Algorithm: sha1WithRSAEncryption
+ * Signature Value:
+ */
+ public static final String CERT_V1_PEM =
+ "-----BEGIN CERTIFICATE-----\n" +
+ "MIIC2jCCAkMCAg38MA0GCSqGSIb3DQEBBQUAMIGbMQswCQYDVQQGEwJKUDEOMAwG\n" +
+ "A1UECBMFVG9reW8xEDAOBgNVBAcTB0NodW8ta3UxETAPBgNVBAoTCEZyYW5rNERE\n" +
+ "MRgwFgYDVQQLEw9XZWJDZXJ0IFN1cHBvcnQxGDAWBgNVBAMTD0ZyYW5rNEREIFdl\n" +
+ "YiBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBmcmFuazRkZC5jb20wHhcNMTIw\n" +
+ "ODIyMDUyNzQxWhcNMTcwODIxMDUyNzQxWjBKMQswCQYDVQQGEwJKUDEOMAwGA1UE\n" +
+ "CAwFVG9reW8xETAPBgNVBAoMCEZyYW5rNEREMRgwFgYDVQQDDA93d3cuZXhhbXBs\n" +
+ "ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0z9FeMynsC8+u\n" +
+ "dvX+LciZxnh5uRj4C9S6tNeeAlIGCfQYk0zUcNFCoCkTknNQd/YEiawDLNbxBqut\n" +
+ "bMDZ1aarys1a0lYmUeVLCIqvzBkPJTSQsCopQQ9V8WuT252zzNzs68dVGNdCJd5J\n" +
+ "NRQykpwexmnjPPv0mvj7i8XgG379TyW6P+WWV5okeUkXJ9eJS2ouDYdR2SM9BoVW\n" +
+ "+FgxDu6BmXhozW5EfsnajFp7HL8kQClI0QOc79yuKl3492rH6bzFsFn2lfwWy9ic\n" +
+ "7cP8EpCTeFp1tFaD+vxBhPZkeTQ1HKx6hQ5zeHIB5ySJJZ7af2W8r4eTGYzbdRW2\n" +
+ "4DDHCPhZAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAQMv+BFvGdMVzkQaQ3/+2noVz\n" +
+ "/uAKbzpEL8xTcxYyP3lkOeh4FoxiSWqy5pGFALdPONoDuYFpLhjJSZaEwuvjI/Tr\n" +
+ "rGhLV1pRG9frwDFshqD2Vaj4ENBCBh6UpeBop5+285zQ4SI7q4U9oSebUDJiuOx6\n" +
+ "+tZ9KynmrbJpTSi0+BM=\n" +
+ "-----END CERTIFICATE-----";
+
+ public static final Byte[] CERT_L1_ECC;
+ public static final Byte[] CERT_L2_RSA;
+ public static final Byte[] CERT_V1;
+
+ public static final ZonedDateTime NOW = ZonedDateTime.now(ZoneId.of("UTC"));
+ public static final TbsCertificate CERT_GENERATED;
+
+ public static final RevokedCertificate REVOKED_CESSATION;
+ public static final Byte[] REVOKED_CESSATION_DER;
+ public static final RevokedCertificate REVOKED_KEY_COMPROMISE;
+ public static final Byte[] REVOKED_KEY_COMPROMISE_DER;
+ public static final CertificateListContent CRL_CONTENT_1;
+ public static final Byte[] CRL_CONTENT_1_DER;
+ public static final CertificateListContent CRL_CONTENT_2;
+ public static final Byte[] CRL_CONTENT_2_DER;
+
+ static {
+ try {
+ EXT_KEY_USAGE = ASN1Object.parse(new BytesReader(new Byte[]{
+ 0x30, 0x1F, // SEQUENCE (AttributeValue)
+ 0x30, 0x1D, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage
+ 0x04, 0x16, // OCTET STRING
+ 0x30, 0x14, // SEQUENCE
+ // 1.3.6.1.5.5.7.3.1 serverAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
+ // 1.3.6.1.5.5.7.3.2 clientAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02
+ }), false);
+
+ SAN = ASN1Object.parse(new BytesReader(new Byte[]{
+ 0x30, 0x21, // SEQUENCE (AttributeValue)
+ 0x30, 0x1F, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName
+ 0x04, 0x18, // OCTET STRING
+ 0x30, 0x16, // SEQUENCE
+ -126, 0x14, // [2]
+ 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe
+ 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65
+ }), false);
+
+ CSR_ATTR_VALUES_2 = new Values(TAG_SET, null, new ASN1Object[]{
+ EXT_KEY_USAGE,
+ SAN
+ });
+
+ CSR_ATTR_2 = new Attribute(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_EXTENSION_REQUEST),
+ CSR_ATTR_VALUES_2);
+
+ CSR_ATTR_1 = new Attribute(
+ TAG_SEQUENCE, null,
+ new ObjectIdentifier(TAG, null, new Integer[]{ 1, 3, 6, 1, 4, 1, 311, 13, 2, 3 }),
+ new Values(TAG_SET, null, new ASN1Object[]{
+ new IA5String(IA5String.TAG, null, "10.0.19045.2")
+ })
+ );
+
+ CSR_ATTRS_2 = new Attributes(TAG_SET, null, new Attribute[]{
+ CSR_ATTR_2, CSR_ATTR_1
+ });
+
+ // L = Milano
+ L_MILANO = new RelativeDistinguishedName(TAG_SET, null,
+ new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_L),
+ new PrintableString(PrintableString.TAG, null, "Milano"))
+ });
+ // CN = Test ed25519+L = Milano
+ L_MILANO_CN_TEST_ED25519 = new RelativeDistinguishedName(TAG_SET, null,
+ new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_CN),
+ new UTF8String(UTF8String.TAG, null, "Test ed25519")),
+ new AttributeTypeAndValue(TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_L),
+ new UTF8String(UTF8String.TAG, null, "Milano"))
+ });
+
+ CN_YUUTA = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_CN),
+ new PrintableString(PrintableString.TAG, null, "yuuta"))
+ });
+ OU_USERS = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_OU),
+ new PrintableString(PrintableString.TAG, null, "users"))
+ });
+ C_CA = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_C),
+ new PrintableString(PrintableString.TAG, null, "CA"))
+ });
+ NAME_1 = new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{
+ CN_YUUTA, OU_USERS, C_CA
+ });
+
+ SN_QWQ = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SN),
+ new PrintableString(PrintableString.TAG, null, "Qwq"))
+ });
+ O_IT = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_O),
+ new PrintableString(PrintableString.TAG, null, "IT"))
+ });
+ C_CN = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_C),
+ new PrintableString(PrintableString.TAG, null, "CN"))
+ });
+ NAME_2 = new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{
+ SN_QWQ, O_IT, C_CN
+ });
+
+ CERT_L1_ECC = Utils.parsePEM(Utils.byteToByte(CERT_L1_ECC_PEM.getBytes(StandardCharsets.UTF_8)),
+ "CERTIFICATE");
+ CERT_L2_RSA = Utils.parsePEM(Utils.byteToByte(CERT_L2_RSA_PEM.getBytes(StandardCharsets.UTF_8)),
+ "CERTIFICATE");
+ CERT_V1 = Utils.parsePEM(Utils.byteToByte(CERT_V1_PEM.getBytes(StandardCharsets.UTF_8)),
+ "CERTIFICATE");
+
+ CERT_GENERATED = new TbsCertificate(ASN1Object.TAG_SEQUENCE, null,
+ new Int(Int.TAG, new Tag(TagClass.CONTEXT_SPECIFIC, false, 0), TbsCertificate.VERSION_V3),
+ new Int(Int.TAG, null, 100),
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{
+ new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_CN),
+ new PrintableString(PrintableString.TAG, null, "Test CA"))
+ }),
+ new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_C),
+ new PrintableString(PrintableString.TAG, null, "CA"))
+ })
+ }),
+ new Validity(ASN1Object.TAG_SEQUENCE, null,
+ new UtcTime(UtcTime.TAG, null, NOW),
+ new GeneralizedTime(GeneralizedTime.TAG, null, NOW.plusYears(1))),
+ new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{
+ new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_CN),
+ new PrintableString(PrintableString.TAG, null, "Yuuta Liang"))
+ }),
+ new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_C),
+ new PrintableString(PrintableString.TAG, null, "CA"))
+ })
+ }),
+ new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null,
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_EC_PUBLIC_KEY),
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_PRIME256_V1)),
+ new BitString(BitString.TAG, null, 0, new Byte[]{ 1, 2, 3 })),
+ new Extensions(ASN1Object.TAG_SEQUENCE,
+ new Tag(TagClass.CONTEXT_SPECIFIC, false, 3),
+ new Extension[]{
+ new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_BASIC_CONSTRAINTS),
+ new Bool(Bool.TAG, null, true),
+ new OctetString(OctetString.TAG, null,
+ new Byte[]{ 0x30, 0x06, 0x01, 0x01, -1, 0x02, 0x01, 0x00 })),
+ new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_KEY_USAGE),
+ new Bool(Bool.TAG, null, true),
+ new OctetString(OctetString.TAG, null,
+ new Byte[]{ 0x03, 0x02, 0x01, -122 }))
+ }));
+
+ REVOKED_CESSATION =
+ new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null,
+ new Int(Int.TAG, null, 123),
+ new UtcTime(UtcTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))),
+ Reason.CESSATION_OF_OPERATION);
+ REVOKED_KEY_COMPROMISE =
+ new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null,
+ new Int(Int.TAG, null, 2),
+ new UtcTime(UtcTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))),
+ Reason.KEY_COMPROMISE);
+ REVOKED_CESSATION_DER = combine((byte) 0x30,
+ REVOKED_CESSATION.getSerialNumber().encodeDER(),
+ REVOKED_CESSATION.getRevocationDate().encodeDER(),
+ combine((byte) 0x30,
+ combine((byte) 0x30,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_CRL_REASON).encodeDER(),
+ new OctetString(OctetString.TAG, null,
+ new Byte[]{ 0x0A, 0x01, (byte) Reason.CESSATION_OF_OPERATION.getVal() })
+ .encodeDER())));
+ REVOKED_KEY_COMPROMISE_DER = combine((byte) 0x30,
+ REVOKED_KEY_COMPROMISE.getSerialNumber().encodeDER(),
+ REVOKED_KEY_COMPROMISE.getRevocationDate().encodeDER(),
+ combine((byte) 0x30,
+ combine((byte) 0x30,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_CRL_REASON).encodeDER(),
+ new OctetString(OctetString.TAG, null,
+ new Byte[]{ 0x0A, 0x01, (byte) Reason.KEY_COMPROMISE.getVal() })
+ .encodeDER())));
+ CRL_CONTENT_1 =
+ new CertificateListContent(ASN1Object.TAG_SEQUENCE, null,
+ new Name(ASN1Object.TAG_SEQUENCE, null,
+ new RelativeDistinguishedName[]{
+ new RelativeDistinguishedName(ASN1Object.TAG_SET, null,
+ new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_CN),
+ new PrintableString(PrintableString.TAG, null,
+ "Test CA"))
+ })
+ }),
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new GeneralizedTime(GeneralizedTime.TAG, null,
+ ZonedDateTime.now(ZoneId.of("UTC"))),
+ null,
+ new RevokedCertificate[]{
+ TestConstants.REVOKED_CESSATION,
+ TestConstants.REVOKED_KEY_COMPROMISE
+ });
+ CRL_CONTENT_1_DER = combine((byte) 0x30, CRL_CONTENT_1.getVersion().encodeDER(),
+ CRL_CONTENT_1.getIssuer().encodeDER(),
+ CRL_CONTENT_1.getSignature().encodeDER(),
+ CRL_CONTENT_1.getThisUpdate().encodeDER(),
+ combine((byte) 0x30, REVOKED_CESSATION_DER, REVOKED_KEY_COMPROMISE_DER));
+ CRL_CONTENT_2 = new CertificateListContent(CRL_CONTENT_1.getTag(), CRL_CONTENT_1.getParentTag(),
+ CRL_CONTENT_1.getIssuer(),
+ CRL_CONTENT_1.getSignature(),
+ CRL_CONTENT_1.getThisUpdate(),
+ CRL_CONTENT_1.getThisUpdate(),
+ CRL_CONTENT_1.getRevokedCertificates());
+ CRL_CONTENT_2_DER = combine((byte) 0x30, CRL_CONTENT_2.getVersion().encodeDER(),
+ CRL_CONTENT_2.getIssuer().encodeDER(),
+ CRL_CONTENT_2.getSignature().encodeDER(),
+ CRL_CONTENT_2.getThisUpdate().encodeDER(),
+ CRL_CONTENT_2.getNextUpdate().encodeDER(),
+ combine((byte) 0x30, REVOKED_CESSATION_DER, REVOKED_KEY_COMPROMISE_DER));
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static Byte[] mutate(Byte[] in, int i, int from, int to) {
+ Byte[] b = new Byte[in.length];
+ System.arraycopy(in, 0, b, 0, in.length);
+ assertEquals(from, (int) in[i]);
+ b[i] = (byte) to;
+ return b;
+ }
+
+ public static Byte[] combine(Byte tag, Byte[]... vals) {
+ return Stream.of(Collections.singletonList(tag),
+ Arrays.asList(new ASN1Length(Arrays.stream(vals).mapToInt(b -> b.length).sum()).encodeDER()),
+ Arrays.stream(vals).flatMap(Arrays::stream).collect(Collectors.toList()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+}
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());
+ }
+}
diff --git a/src/test/model/csr/AttributeTest.java b/src/test/model/csr/AttributeTest.java
new file mode 100644
index 0000000..f4daa4c
--- /dev/null
+++ b/src/test/model/csr/AttributeTest.java
@@ -0,0 +1,53 @@
+package model.csr;
+
+import model.TestConstants;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import org.junit.jupiter.api.Test;
+
+import static model.asn1.ObjectIdentifier.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class AttributeTest {
+ @Test
+ void testConstructor() throws ParseException {
+ assertArrayEquals(OID_EXTENSION_REQUEST, TestConstants.CSR_ATTR_2.getType().getInts());
+ assertEquals(0x21, TestConstants.CSR_ATTR_2.getValues().getArray()[1].getLength());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ final Attribute parsed = new Attribute(new BytesReader(TestConstants.CSR_ATTR_VALUES_2_DER), false);
+
+ assertArrayEquals(OID_EXTENSION_REQUEST, parsed.getType().getInts());
+ assertEquals(2, parsed.getValues().getArray().length);
+ }
+
+ @Test
+ void testParseFail() {
+ // No type
+ assertThrows(ParseException.class, () -> new Attribute(new BytesReader(new Byte[]{
+ 0x30, 0x0
+ }), false));
+ // No value
+ assertThrows(ParseException.class, () -> new Attribute(new BytesReader(new Byte[]{
+ 0x30, 0x5, 0x6, 0x3, 0x55, 0x4, 0x6
+ }), false));
+ // Incorrect type tag (should be OID)
+ assertThrows(ParseException.class, () -> new Attribute(new BytesReader(new Byte[]{
+ 0x30, 0x9, 0x7, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54
+ }), false));
+ // Incorrect value tag (should be SET)
+ assertThrows(ParseException.class, () -> new Attribute(new BytesReader(new Byte[]{
+ 0x30, 13,
+ 0x06, 0x09, 0x1A, -122, 0x48, -122, -9, 0x0D, 0x01, 0x09, 0x0E,
+ 0x30, 0
+ }), false));
+ }
+
+ @Test
+ void testEncode() {
+ assertArrayEquals(TestConstants.CSR_ATTR_1_DER, TestConstants.CSR_ATTR_1.encodeDER());
+ assertArrayEquals(TestConstants.CSR_ATTR_VALUES_2_DER, TestConstants.CSR_ATTR_2.encodeDER());
+ }
+}
diff --git a/src/test/model/csr/AttributesTest.java b/src/test/model/csr/AttributesTest.java
new file mode 100644
index 0000000..86a0112
--- /dev/null
+++ b/src/test/model/csr/AttributesTest.java
@@ -0,0 +1,69 @@
+package model.csr;
+
+import model.TestConstants;
+import model.asn1.ObjectIdentifier;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class AttributesTest {
+ @Test
+ void testConstructor() {
+ assertEquals("10.0.19045.2",
+ TestConstants.CSR_ATTRS_2.getArray()[1].getValues().getArray()[0].toString());
+ assertArrayEquals(ObjectIdentifier.OID_EXTENSION_REQUEST,
+ TestConstants.CSR_ATTRS_2.getArray()[0].getType().getInts());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ final Attributes parsed = new Attributes(new BytesReader(new Byte[]{
+ -96, 30,
+ 0x30, 0x1C,
+ 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0D, 0x02, 0x03,
+ 0x31, 0x0E,
+ 0x16, 0x0C, 0x31, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x39, 0x30, 0x34, 0x35, 0x2E, 0x32
+ }), false);
+ assertEquals(1, parsed.getArray().length);
+ assertEquals("10.0.19045.2", parsed.getArray()[0].getValues().getArray()[0].toString());
+ }
+
+ @Test
+ void testParseFail() {
+ // Incorrect length
+ assertThrows(ParseException.class, () -> new Attributes(new BytesReader(new Byte[]{
+ -96, 31, // Incorrect
+ 0x30, 0x1C,
+ 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0D, 0x02, 0x03,
+ 0x31, 0x0E,
+ 0x16, 0x0C, 0x31, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x39, 0x30, 0x34, 0x35, 0x2E, 0x32
+ }), false));
+ // Incorrect child item tag
+ assertThrows(ParseException.class, () -> new Attributes(new BytesReader(new Byte[]{
+ -96, 30,
+ 0x31, 0x1C, // Incorrect
+ 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0D, 0x02, 0x03,
+ 0x31, 0x0E,
+ 0x16, 0x0C, 0x31, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x39, 0x30, 0x34, 0x35, 0x2E, 0x32
+ }), false));
+ }
+
+ @Test
+ void testEncode() {
+ Byte[] a2 = TestConstants.CSR_ATTR_2.encodeDER();
+ Byte[] a1 = TestConstants.CSR_ATTR_1.encodeDER();
+ assertArrayEquals(
+ Stream.of(Arrays.asList(new Byte[]{ 0x31, (byte)(a2.length + a1.length) }),
+ Arrays.asList(a2),
+ Arrays.asList(a1))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new),
+ TestConstants.CSR_ATTRS_2.encodeDER());
+ }
+}
diff --git a/src/test/model/csr/CertificationRequestInfoTest.java b/src/test/model/csr/CertificationRequestInfoTest.java
new file mode 100644
index 0000000..fe2633f
--- /dev/null
+++ b/src/test/model/csr/CertificationRequestInfoTest.java
@@ -0,0 +1,174 @@
+package model.csr;
+
+import model.TestConstants;
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+import model.pki.SubjectPublicKeyInfo;
+import org.junit.jupiter.api.Test;
+
+import static model.TestConstants.mutate;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CertificationRequestInfoTest {
+ static final Byte[] CSR_1 = new Byte[] {
+ 0x30, -126, 0x02, 0x03, // SEQUENCE CertificationRequestInfo
+ 0x02, 0x01, 0x00, // Version
+ 0x30, 0x1c, // SEQUENCE Subject
+ 0x31, 0x1a, // RDN
+ 0x30, 0x18, // AttributeTypeAndValue
+ 0x06, 0x03, 0x55, 0x04, 0x03, // OID (CN)
+ 0x0c, 0x11, 0x4d, 0x49, 0x4b, 0x55, 0x2e, 0x41, // PrintableString (MIKU.AD.YUUTA.MOE)
+ 0x44, 0x2e, 0x59, 0x55, 0x55, 0x54, 0x41, 0x2e,
+ 0x4d, 0x4f, 0x45,
+ 0x30, -127, -97, // SEQUENCE SubjectPublicKeyInfo
+ 0x30, 0x0d, // SEQUENCE AlgorithmIdentifier
+ 0x06, 0x09, 0x2a, -122, 0x48, -122, -9, 0x0d, // OID (rsaEncryption)
+ 0x01, 0x01, 0x01,
+ 0x05, 0x00, // Null (Parameter)
+ 0x03, -127, -115, 0x00, 0x30, -127, -119, 0x02, // BIT STRING (subjectPublicKey)
+ -127, -127, 0x00, -67, -1, 0x4e, 0x6d, -22,
+ 0x62, 0x6a, 0x11, -120, 0x77, 0x0a, -92, 0x32,
+ -124, -37, 0x22, 0x2f, 0x3d, 0x5d, 0x2a, 0x63,
+ -71, -109, 0x11, -50, -92, 0x4f, -119, 0x3b,
+ 0x14, 0x3b, -54, 0x3c, -106, -42, 0x11, 0x42,
+ 0x78, -110, 0x68, -100, -25, -25, -50, 0x75,
+ -101, 0x21, 0x41, -34, -31, -85, -13, 0x1e,
+ 0x51, -81, 0x25, 0x4f, -1, 0x56, 0x77, 0x5e,
+ -30, 0x27, -104, 0x34, 0x67, -28, -56, 0x55,
+ 0x6a, 0x3c, 0x6f, -38, -85, -63, 0x5f, 0x16,
+ 0x7a, -93, -19, -35, 0x7f, 0x35, 0x0f, -47,
+ -7, -22, -12, -24, -48, 0x25, 0x6d, -114,
+ 0x66, 0x1a, 0x53, -77, 0x67, 0x32, -69, -39,
+ 0x57, -42, -65, -13, 0x5f, 0x6f, 0x53, 0x6d,
+ 0x62, -95, 0x42, 0x12, 0x7b, 0x13, 0x4f, 0x1a,
+ -26, 0x00, -72, -32, 0x2b, -83, 0x3c, 0x35,
+ -103, 0x18, 0x51, 0x02, 0x03, 0x01, 0x00, 0x01,
+ -96, -126, 0x01, 0x3c, // SEQUENCE (attributes)
+ 0x30, 0x1c, 0x06, 0x0a, // SEQUENCE (attribute)
+ 0x2b, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0d,
+ 0x02, 0x03, 0x31, 0x0e, 0x16, 0x0c, 0x31, 0x30,
+ 0x2e, 0x30, 0x2e, 0x31, 0x39, 0x30, 0x34, 0x35,
+ 0x2e, 0x32,
+ 0x30, 0x42, 0x06, // SEQUENCE (attribute)
+ 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, -126, 0x37,
+ 0x15, 0x14, 0x31, 0x35, 0x30, 0x33, 0x02, 0x01,
+ 0x05, 0x0c, 0x11, 0x4d, 0x49, 0x4b, 0x55, 0x2e,
+ 0x41, 0x44, 0x2e, 0x59, 0x55, 0x55, 0x54, 0x41,
+ 0x2e, 0x4d, 0x4f, 0x45, 0x0c, 0x12, 0x4d, 0x49,
+ 0x4b, 0x55, 0x5c, 0x41, 0x64, 0x6d, 0x69, 0x6e,
+ 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72,
+ 0x0c, 0x07, 0x4d, 0x4d, 0x43, 0x2e, 0x45, 0x58,
+ 0x45,
+ 0x30, 0x66, // SEQUENCE (attribute)
+ 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, -126,
+ 0x37, 0x0d, 0x02, 0x02, 0x31, 0x58, 0x30, 0x56,
+ 0x02, 0x01, 0x00, 0x1e, 0x4e, 0x00, 0x4d, 0x00,
+ 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00,
+ 0x73, 0x00, 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00,
+ 0x20, 0x00, 0x53, 0x00, 0x6f, 0x00, 0x66, 0x00,
+ 0x74, 0x00, 0x77, 0x00, 0x61, 0x00, 0x72, 0x00,
+ 0x65, 0x00, 0x20, 0x00, 0x4b, 0x00, 0x65, 0x00,
+ 0x79, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00,
+ 0x6f, 0x00, 0x72, 0x00, 0x61, 0x00, 0x67, 0x00,
+ 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x72, 0x00,
+ 0x6f, 0x00, 0x76, 0x00, 0x69, 0x00, 0x64, 0x00,
+ 0x65, 0x00, 0x72, 0x03, 0x01, 0x00,
+ 0x30, 0x70, 0x06, 0x09, // SEQUENCE (attribute)
+ 0x2a, -122, 0x48, -122, -9, 0x0d, 0x01, 0x09,
+ 0x0e, 0x31, 0x63, 0x30, 0x61, 0x30, 0x0e, 0x06,
+ 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, -1, 0x04,
+ 0x04, 0x03, 0x02, 0x05, -96, 0x30, 0x13, 0x06,
+ 0x03, 0x55, 0x1d, 0x25, 0x04, 0x0c, 0x30, 0x0a,
+ 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07,
+ 0x03, 0x01, 0x30, 0x1b, 0x06, 0x09, 0x2b, 0x06,
+ 0x01, 0x04, 0x01, -126, 0x37, 0x15, 0x0a, 0x04,
+ 0x0e, 0x30, 0x0c, 0x30, 0x0a, 0x06, 0x08, 0x2b,
+ 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, 0x30,
+ 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16,
+ 0x04, 0x14, -15, 0x3e, -110, -16, 0x4d, 0x1b,
+ -47, 0x6e, 0x53, 0x7f, -102, 0x1d, 0x19, -75,
+ 0x5e, -22, 0x64, 0x7f, 0x1f, -110,
+ };
+
+ @Test
+ void testConstructor() {
+ final CertificationRequestInfo info = new CertificationRequestInfo(
+ ASN1Object.TAG_SEQUENCE, null,
+ new Int(Int.TAG, null, CertificationRequestInfo.VERSION_V1),
+ TestConstants.NAME_2,
+ new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null,
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new BitString(BitString.TAG, null,
+ 0, new Byte[]{ 1, 2, 3, 4, 5 })),
+ TestConstants.CSR_ATTRS_2);
+ assertEquals(CertificationRequestInfo.VERSION_V1, info.getVersion().getLong());
+ assertEquals(3, info.getSubject().getRdnSequence().length);
+ assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION,
+ info.getSubjectPKInfo().getAlgorithm().getType().getInts());
+ assertEquals(2, info.getAttributes().getArray().length);
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ final CertificationRequestInfo parsed =
+ new CertificationRequestInfo(new BytesReader(CSR_1), false);
+ assertEquals("CN=MIKU.AD.YUUTA.MOE", parsed.getSubject().toString());
+ assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION,
+ parsed.getSubjectPKInfo().getAlgorithm().getType().getInts());
+ assertEquals(4, parsed.getAttributes().getArray().length);
+ assertEquals(1, parsed.getAttributes().getArray()[3].getValues().getArray().length);
+ }
+
+ @Test
+ void testParseFail() {
+ // No version
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequestInfo(new BytesReader(new Byte[]{
+ 0x30, 0
+ }), false);
+ });
+ // Incorrect version tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 4, (byte) Int.TAG.getNumber(), 0x3)),
+ false);
+ });
+ // Incorrect version
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 6, CertificationRequestInfo.VERSION_V1, 1)),
+ false);
+ });
+ // No subject
+ assertThrows(ParseException.class, () -> {
+ Byte[] test = new Byte[5];
+ test[0] = 0x30;
+ test[1] = 3;
+ System.arraycopy(CSR_1, 4, test, 2, 3);
+ new CertificationRequestInfo(new BytesReader(test), false);
+ });
+ // Incorrect subject tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 7, 0x30, 0x31)),
+ false);
+ });
+ // Incorrect subject pk info tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 37, 0x30, 0x31)),
+ false);
+ });
+ // Incorrect attributes tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 199, -96, 0x31)),
+ false);
+ });
+ }
+
+ @Test
+ void testEncode() throws ParseException {
+ assertArrayEquals(CSR_1, new CertificationRequestInfo(new BytesReader(CSR_1), false).encodeDER());
+ }
+}
diff --git a/src/test/model/csr/CertificationRequestTest.java b/src/test/model/csr/CertificationRequestTest.java
new file mode 100644
index 0000000..962e90b
--- /dev/null
+++ b/src/test/model/csr/CertificationRequestTest.java
@@ -0,0 +1,114 @@
+package model.csr;
+
+import model.TestConstants;
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+import model.pki.SubjectPublicKeyInfo;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+import static model.TestConstants.mutate;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CertificationRequestTest {
+ private static final Byte[] CSR_1 = Stream.of(
+ // SEQUENCE (CertificationRequest)
+ Arrays.asList(new Byte[]{ 0x30, -126, 0x02, -102 }),
+ // SEQUENCE (CertificationRequestInfo)
+ Arrays.asList(CertificationRequestInfoTest.CSR_1),
+ // SEQUENCE (AlgorithmIdentifier)
+ Arrays.asList(new Byte[]{
+ 0x30, 0x0D, 0x06, 0x09, 0x2A, -122, 0x48, -122,
+ -9, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00
+ }),
+ // BIT STRING (Signature)
+ Arrays.asList(new Byte[]{
+ 0x03, -127, -127,
+ 0x00, 0x6F, 0x61, 0x5C, -25, 0x29, 0x48, 0x3F,
+ -78, 0x1B, -117, 0x2C, -93, -114, 0x7D, -77,
+ 0x62, 0x14, 0x21, 0x4B, -99, 0x74, -95, -93,
+ 0x16, 0x38, 0x31, 0x40, 0x5E, 0x72, -77, -55,
+ 0x6D, -69, 0x19, -108, 0x52, -95, 0x19, -121,
+ -81, -71, 0x74, -123, 0x6B, -27, -20, 0x4C,
+ -126, 0x42, -89, 0x66, 0x6A, 0x52, -34, 0x62,
+ 0x72, 0x40, 0x2C, -79, 0x78, -117, -100, -70,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x7E,
+ })).flatMap(Collection::stream).toArray(Byte[]::new);
+
+ @Test
+ void testConstructor() {
+ final CertificationRequest request = new CertificationRequest(
+ ASN1Object.TAG_SEQUENCE, null,
+ new CertificationRequestInfo(
+ ASN1Object.TAG_SEQUENCE, null,
+ new Int(Int.TAG, null, CertificationRequestInfo.VERSION_V1),
+ TestConstants.NAME_2,
+ new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null,
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new BitString(BitString.TAG, null,
+ 0, new Byte[]{ 1, 2, 3, 4, 5 })),
+ TestConstants.CSR_ATTRS_2),
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new BitString(BitString.TAG, null,
+ 0, new Byte[]{ 2, 4, 6, 8, 10 }));
+
+ assertEquals(CertificationRequestInfo.VERSION_V1,
+ request.getCertificationRequestInfo().getVersion().getLong());
+ assertEquals(3,
+ request.getCertificationRequestInfo().getSubject().getRdnSequence().length);
+ assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION,
+ request.getSignatureAlgorithm().getType().getInts());
+ assertArrayEquals(new Byte[]{ 2, 4, 6, 8, 10 },
+ request.getSignature().getConvertedVal());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ final CertificationRequest parsed =
+ new CertificationRequest(new BytesReader(CSR_1), false);
+ assertEquals("CN=MIKU.AD.YUUTA.MOE",
+ parsed.getCertificationRequestInfo().getSubject().toString());
+ assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION,
+ parsed.getSignatureAlgorithm().getType().getInts());
+ }
+
+ @Test
+ void testParseFail() throws ParseException {
+ // Incorrect info tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequest(new BytesReader(mutate(CSR_1, 4, 0x30, 0x31)), false);
+ });
+ // Incorrect algorithm info tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequest(new BytesReader(mutate(CSR_1, 523, 0x30, 0x31)), false);
+ });
+ // Incorrect signature tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequest(new BytesReader(mutate(CSR_1, 538, 0x3, 0x31)), false);
+ });
+ }
+
+ @Test
+ void testEncode() throws ParseException {
+ assertArrayEquals(CSR_1, new CertificationRequest(new BytesReader(CSR_1), false).encodeDER());
+ }
+}
diff --git a/src/test/model/csr/ValuesTest.java b/src/test/model/csr/ValuesTest.java
new file mode 100644
index 0000000..93229a4
--- /dev/null
+++ b/src/test/model/csr/ValuesTest.java
@@ -0,0 +1,133 @@
+package model.csr;
+
+import model.asn1.ASN1Object;
+import model.asn1.Null;
+import model.asn1.ObjectIdentifier;
+import model.asn1.PrintableString;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.x501.AttributeTypeAndValue;
+import model.x501.RelativeDistinguishedName;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static model.asn1.ASN1Object.TAG_SET;
+import static model.asn1.ObjectIdentifier.OID_C;
+import static model.asn1.ObjectIdentifier.OID_OU;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ValuesTest {
+ // An opaque value of extended key usage block.
+ private ASN1Object extKeyUsage;
+
+ // Example SubjectAlternativeName request attribute.
+ private ASN1Object san;
+
+ private Values values;
+
+ @BeforeEach
+ void setup() throws ParseException {
+ extKeyUsage = ASN1Object.parse(new BytesReader(new Byte[]{
+ 0x30, 0x1F, // SEQUENCE (AttributeValue)
+ 0x30, 0x1D, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage
+ 0x04, 0x16, // OCTET STRING
+ 0x30, 0x14, // SEQUENCE
+ // 1.3.6.1.5.5.7.3.1 serverAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
+ // 1.3.6.1.5.5.7.3.2 clientAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02
+ }), false);
+
+ san = ASN1Object.parse(new BytesReader(new Byte[]{
+ 0x30, 0x21, // SEQUENCE (AttributeValue)
+ 0x30, 0x1F, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName
+ 0x04, 0x18, // OCTET STRING
+ 0x30, 0x16, // SEQUENCE
+ -126, 0x14, // [2]
+ 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe
+ 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65
+ }), false);
+
+ values = new Values(TAG_SET, null, new ASN1Object[]{
+ extKeyUsage,
+ san
+ });
+ }
+
+ @Test
+ void testConstructor() {
+ assertEquals(0x1F, values.getArray()[0].getLength());
+ assertEquals(0x21, values.getArray()[1].getLength());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ assertEquals(0x1F, new Values(new BytesReader(new Byte[]{
+ 0x31, 0x21,
+ 0x30, 0x1F, // SEQUENCE (AttributeValue)
+ 0x30, 0x1D, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage
+ 0x04, 0x16, // OCTET STRING
+ 0x30, 0x14, // SEQUENCE
+ // 1.3.6.1.5.5.7.3.1 serverAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
+ // 1.3.6.1.5.5.7.3.2 clientAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02
+ }), false).getArray()[0].getLength());
+ assertEquals(0x21, new Values(new BytesReader(new Byte[]{
+ 0x31, 0x23,
+ 0x30, 0x21, // SEQUENCE (AttributeValue)
+ 0x30, 0x1F, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName
+ 0x04, 0x18, // OCTET STRING
+ 0x30, 0x16, // SEQUENCE
+ -126, 0x14, // [2]
+ 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe
+ 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65
+ }), false).getArray()[0].getLength());
+ }
+
+ @Test
+ void testParseFail() {
+ // Incorrect length
+ assertThrows(ParseException.class, () -> new Values(new BytesReader(new Byte[]{
+ 0x31, 0x29, // Incorrect length!
+ 0x30, 0x21, // SEQUENCE (AttributeValue)
+ 0x30, 0x1F, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName
+ 0x04, 0x18, // OCTET STRING
+ 0x30, 0x16, // SEQUENCE
+ -126, 0x14, // [2]
+ 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe
+ 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65
+ }), false));
+ }
+
+ @Test
+ void testEncode() {
+ assertArrayEquals(new Byte[]{
+ 0x31, 68,
+
+ 0x30, 0x1F, // SEQUENCE (AttributeValue)
+ 0x30, 0x1D, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage
+ 0x04, 0x16, // OCTET STRING
+ 0x30, 0x14, // SEQUENCE
+ // 1.3.6.1.5.5.7.3.1 serverAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01,
+ // 1.3.6.1.5.5.7.3.2 clientAuth
+ 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02,
+
+ 0x30, 0x21, // SEQUENCE (AttributeValue)
+ 0x30, 0x1F, // SEQUENCE
+ 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName
+ 0x04, 0x18, // OCTET STRING
+ 0x30, 0x16, // SEQUENCE
+ -126, 0x14, // [2]
+ 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe
+ 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65
+ }, values.encodeDER());
+ }
+}
diff --git a/src/test/model/pki/AlgorithmIdentifierTest.java b/src/test/model/pki/AlgorithmIdentifierTest.java
new file mode 100644
index 0000000..8bcc80e
--- /dev/null
+++ b/src/test/model/pki/AlgorithmIdentifierTest.java
@@ -0,0 +1,84 @@
+package model.pki;
+
+import model.asn1.ASN1Object;
+import model.asn1.Null;
+import model.asn1.ObjectIdentifier;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import org.junit.jupiter.api.Test;
+
+import static model.asn1.ObjectIdentifier.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class AlgorithmIdentifierTest {
+ @Test
+ void testConstructor() {
+ assertArrayEquals(OID_SHA256_WITH_RSA_ENCRYPTION,
+ new AlgorithmIdentifier(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null))
+ .getType().getInts());
+ assertEquals(Null.TAG.getNumber(),
+ new AlgorithmIdentifier(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null))
+ .getParameters().getTag().getNumber());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ AlgorithmIdentifier parsed = new AlgorithmIdentifier(new BytesReader(new Byte[]{
+ 0x30, 0x0d, // SEQUENCE AlgorithmIdentifier
+ 0x06, 0x09, 0x2a, -122, 0x48, -122, -9, 0x0d, // OID (rsaEncryption)
+ 0x01, 0x01, 0x01,
+ 0x05, 0x00, // Null (Parameter)
+ }), false);
+ assertArrayEquals(OID_RSA_ENCRYPTION, parsed.getType().getInts());
+ assertEquals(Null.TAG.getNumber(), parsed.getParameters().getTag().getNumber());
+
+ parsed = new AlgorithmIdentifier(new BytesReader(new Byte[]{
+ 0x30, 0x0D, // SEQUENCE AlgorithmIdentifier
+ 0x06, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, // OID (sha256WithRsaEncryption)
+ 0x01, 0x01, 0x0B,
+ 0x05, 0x00 // Null (Parameter)
+ }), false);
+ assertArrayEquals(OID_SHA256_WITH_RSA_ENCRYPTION, parsed.getType().getInts());
+ assertEquals(Null.TAG.getNumber(), parsed.getParameters().getTag().getNumber());
+
+ parsed = new AlgorithmIdentifier(new BytesReader(new Byte[]{
+ 0x30, 0x0A, // SEQUENCE AlgorithmIdentifier
+ 0x06, 0x08, 0x2A, -122, 0x48, -50, 0x3D, 0x04, 0x03, 0x02 // OID (ecdsaWithSHA256)
+ }), false);
+ assertArrayEquals(OID_ECDSA_WITH_SHA256, parsed.getType().getInts());
+ assertNull(parsed.getParameters());
+ }
+
+ @Test
+ void testParseFail() {
+ // No type
+ assertThrows(ParseException.class, () -> new AlgorithmIdentifier(new BytesReader(new Byte[]{
+ 0x30, 0x0
+ }), false));
+ // Incorrect type tag (should be OID)
+ assertThrows(ParseException.class, () -> new AlgorithmIdentifier(new BytesReader(new Byte[]{
+ 0x30, 0x0B, // SEQUENCE AlgorithmIdentifier
+ 0x07, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, // Incorrect tag
+ 0x01, 0x01, 0x0B,
+ }), false));
+ }
+
+ @Test
+ void testEncode() throws ParseException {
+ assertArrayEquals(new Byte[]{
+ 0x30, 0x0D, // SEQUENCE AlgorithmIdentifier
+ 0x06, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, // OID (sha256WithRsaEncryption)
+ 0x01, 0x01, 0x0B,
+ 0x05, 0x00 // Null (Parameter)
+ }, new AlgorithmIdentifier(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)).encodeDER());
+ }
+}
diff --git a/src/test/model/pki/SubjectPublicKeyInfoTest.java b/src/test/model/pki/SubjectPublicKeyInfoTest.java
new file mode 100644
index 0000000..f1cfff0
--- /dev/null
+++ b/src/test/model/pki/SubjectPublicKeyInfoTest.java
@@ -0,0 +1,115 @@
+package model.pki;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Null;
+import model.asn1.ObjectIdentifier;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.csr.CertificationRequestInfo;
+import model.csr.CertificationRequestInfoTest;
+import org.junit.jupiter.api.Test;
+
+import static model.TestConstants.mutate;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class SubjectPublicKeyInfoTest {
+ private static final Byte[] RSA = new Byte[] {
+ 0x30, -127, -97, // SEQUENCE SubjectPublicKeyInfo
+ 0x30, 0x0d, // SEQUENCE AlgorithmIdentifier
+ 0x06, 0x09, 0x2a, -122, 0x48, -122, -9, 0x0d, // OID (rsaEncryption)
+ 0x01, 0x01, 0x01,
+ 0x05, 0x00, // Null (Parameter)
+ 0x03, -127, -115, 0x00, 0x30, -127, -119, 0x02, // BIT STRING (subjectPublicKey)
+ -127, -127, 0x00, -67, -1, 0x4e, 0x6d, -22,
+ 0x62, 0x6a, 0x11, -120, 0x77, 0x0a, -92, 0x32,
+ -124, -37, 0x22, 0x2f, 0x3d, 0x5d, 0x2a, 0x63,
+ -71, -109, 0x11, -50, -92, 0x4f, -119, 0x3b,
+ 0x14, 0x3b, -54, 0x3c, -106, -42, 0x11, 0x42,
+ 0x78, -110, 0x68, -100, -25, -25, -50, 0x75,
+ -101, 0x21, 0x41, -34, -31, -85, -13, 0x1e,
+ 0x51, -81, 0x25, 0x4f, -1, 0x56, 0x77, 0x5e,
+ -30, 0x27, -104, 0x34, 0x67, -28, -56, 0x55,
+ 0x6a, 0x3c, 0x6f, -38, -85, -63, 0x5f, 0x16,
+ 0x7a, -93, -19, -35, 0x7f, 0x35, 0x0f, -47,
+ -7, -22, -12, -24, -48, 0x25, 0x6d, -114,
+ 0x66, 0x1a, 0x53, -77, 0x67, 0x32, -69, -39,
+ 0x57, -42, -65, -13, 0x5f, 0x6f, 0x53, 0x6d,
+ 0x62, -95, 0x42, 0x12, 0x7b, 0x13, 0x4f, 0x1a,
+ -26, 0x00, -72, -32, 0x2b, -83, 0x3c, 0x35,
+ -103, 0x18, 0x51, 0x02, 0x03, 0x01, 0x00, 0x01,
+ };
+
+ private static final Byte[] ECC = new Byte[] {
+ 0x30, 0x59, // SEQUENCE SubjectPublicKeyInfo
+ 0x30, 0x13, // SEQUENCE AlgorithmIdentifier
+ 0x06, 0x07, 0x2A, -122, 0x48, -50, 0x3D, 0x02, 0x01, // OID (ecPublicKey)
+ 0x06, 0x08, 0x2A, -122, 0x48, -50, 0x3D, 0x03, 0x01, // OID Parameter (prime256v1)
+ 0x07,
+ 0x03, 0x42, // BIT STRING
+ 0x00, 0x04, 0x1D, -24, 0x71, -68, -35, 0x48, 0x70,
+ 0x26, 0x71, 0x6C, -35, 0x04, 0x5B, 0x3F, 0x5D, -34,
+ 0x14, 0x31, -117, 0x3F, 0x31, -128, 0x18, 0x2A, 0x33,
+ -27, 0x19, -122, 0x13, -42, -25, 0x48, 0x2F, -107,
+ 0x15, 0x3A, 0x59, -115, -19, 0x09, -28, 0x53, 0x1A,
+ -13, 0x61, -78, 0x35, 0x61, 0x6E, 0x66, 0x5F, 0x5F,
+ -49, 0x0A, -30, 0x65, 0x65, 0x3D, 0x22, 0x2B, 0x30,
+ 0x71, 0x2C, 0x24
+ };
+
+ @Test
+ void testConstructor() {
+ assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION,
+ new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null,
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new BitString(BitString.TAG, null,
+ 0, new Byte[]{ 1, 2, 3}))
+ .getAlgorithm().getType().getInts());
+ assertArrayEquals(new Byte[]{ 1, 2, 3 },
+ new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null,
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new BitString(BitString.TAG, null,
+ 0, new Byte[]{ 1, 2, 3}))
+ .getSubjectPublicKey().getConvertedVal());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ SubjectPublicKeyInfo parsed = new SubjectPublicKeyInfo(new BytesReader(RSA), false);
+ assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, parsed.getAlgorithm().getType().getInts());
+ assertEquals(Null.TAG.getNumber(), parsed.getAlgorithm().getParameters().getTag().getNumber());
+ assertEquals(140, parsed.getSubjectPublicKey().getConvertedVal().length);
+
+ parsed = new SubjectPublicKeyInfo(new BytesReader(ECC), false);
+ assertArrayEquals(ObjectIdentifier.OID_EC_PUBLIC_KEY, parsed.getAlgorithm().getType().getInts());
+ assertEquals(ObjectIdentifier.TAG.getNumber(), parsed.getAlgorithm().getParameters().getTag().getNumber());
+ assertArrayEquals(ObjectIdentifier.OID_PRIME256_V1,
+ ((ObjectIdentifier) parsed.getAlgorithm().getParameters()).getInts());
+ assertEquals(65, parsed.getSubjectPublicKey().getConvertedVal().length);
+ }
+
+ @Test
+ void testParseFail() {
+ // No algorithm
+ assertThrows(ParseException.class, () -> {
+ new SubjectPublicKeyInfo(new BytesReader(new Byte[]{
+ 0x30, 0
+ }), false);
+ });
+ // Incorrect algorithm ID tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequestInfo(new BytesReader(mutate(RSA, 3, 0x30, 0x31)), false);
+ });
+ // Incorrect public key tag
+ assertThrows(ParseException.class, () -> {
+ new CertificationRequestInfo(new BytesReader(mutate(RSA, 18, BitString.TAG.getNumber(), 0x31)),
+ false);
+ });
+ }
+}
diff --git a/src/test/model/pki/cert/CertificateTest.java b/src/test/model/pki/cert/CertificateTest.java
new file mode 100644
index 0000000..70564fc
--- /dev/null
+++ b/src/test/model/pki/cert/CertificateTest.java
@@ -0,0 +1,81 @@
+package model.pki.cert;
+
+import model.TestConstants;
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.csr.CertificationRequest;
+import model.csr.CertificationRequestInfo;
+import model.csr.CertificationRequestInfoTest;
+import model.pki.AlgorithmIdentifier;
+import model.pki.SubjectPublicKeyInfo;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+import static model.TestConstants.mutate;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CertificateTest {
+ @Test
+ void testConstructor() {
+ final Certificate certificate = new Certificate(ASN1Object.TAG_SEQUENCE, null,
+ TestConstants.CERT_GENERATED,
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new BitString(BitString.TAG, null, 0, new Byte[]{ 1, 2, 3 }));
+
+ assertEquals(TbsCertificate.VERSION_V3,
+ certificate.getCertificate().getVersion().getLong());
+ assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION,
+ certificate.getSignatureAlgorithm().getType().getInts());
+ assertArrayEquals(new Byte[]{ 1, 2, 3 },
+ certificate.getSignature().getConvertedVal());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ Certificate parsed = new Certificate(new BytesReader(TestConstants.CERT_L2_RSA), false);
+ assertEquals(TbsCertificate.VERSION_V3,
+ parsed.getCertificate().getVersion().getLong());
+ assertArrayEquals(ObjectIdentifier.OID_ECDSA_WITH_SHA512, parsed.getSignatureAlgorithm().getType().getInts());
+ assertNull(parsed.getSignatureAlgorithm().getParameters());
+ assertEquals(70, parsed.getSignature().getVal().length);
+
+ parsed = new Certificate(new BytesReader(TestConstants.CERT_L1_ECC), false);
+ assertEquals(TbsCertificate.VERSION_V3,
+ parsed.getCertificate().getVersion().getLong());
+ assertArrayEquals(ObjectIdentifier.OID_ECDSA_WITH_SHA256, parsed.getSignatureAlgorithm().getType().getInts());
+ assertNull(parsed.getSignatureAlgorithm().getParameters());
+ assertEquals(71, parsed.getSignature().getVal().length);
+ }
+
+ @Test
+ void testParseFail() {
+ // Incorrect certificate tag
+ assertThrows(ParseException.class, () ->
+ new Certificate(new BytesReader(mutate(TestConstants.CERT_L1_ECC, 4, 0x30, 0x31)), false)
+ );
+ // Incorrect signatureAlgorithm tag
+ assertThrows(ParseException.class, () ->
+ new Certificate(new BytesReader(mutate(TestConstants.CERT_L1_ECC, 349, 0x30, 0x31)), false)
+ );
+ // Incorrect signature tag
+ assertThrows(ParseException.class, () ->
+ new Certificate(new BytesReader(mutate(TestConstants.CERT_L1_ECC, 361, 0x3, 0x5)), false)
+ );
+ }
+
+ @Test
+ void testEncode() throws ParseException {
+ assertArrayEquals(TestConstants.CERT_V1,
+ new Certificate(new BytesReader(TestConstants.CERT_V1), false).encodeDER());
+ assertArrayEquals(TestConstants.CERT_L1_ECC,
+ new Certificate(new BytesReader(TestConstants.CERT_L1_ECC), false).encodeDER());
+ assertArrayEquals(TestConstants.CERT_L2_RSA,
+ new Certificate(new BytesReader(TestConstants.CERT_L2_RSA), false).encodeDER());
+ }
+}
diff --git a/src/test/model/pki/cert/ExtensionTest.java b/src/test/model/pki/cert/ExtensionTest.java
new file mode 100644
index 0000000..06561ba
--- /dev/null
+++ b/src/test/model/pki/cert/ExtensionTest.java
@@ -0,0 +1,118 @@
+package model.pki.cert;
+
+import model.asn1.ASN1Object;
+import model.asn1.Bool;
+import model.asn1.ObjectIdentifier;
+import model.asn1.OctetString;
+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 ExtensionTest {
+ static final Byte[] EXT_SUBJECT_KEY_ID = new Byte[] {
+ 0x30, 0x1D, // SEQUENCE Extension
+ 0x06, 0x03, 0x55, 0x1D, 0x0E, // OID subjectKeyIdentifier
+ 0x04, 0x16, // OCTET STRING
+ 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66,
+ 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29,
+ -67, 0x6B, 0x49, 0x09, 0x61, -64
+ };
+
+ static final Byte[] EXT_KEY_USAGE = new Byte[] {
+ 0x30, 0x0E, // SEQUENCE Extension
+ 0x06, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage
+ 0x01, 0x01, -1, // BOOLEAN critical
+ 0x04, 0x04, // OCTET STRING
+ 0x03, 0x02, 0x01, -122
+ };
+
+ @Test
+ void testConstructor() throws ParseException {
+ final Extension ext = new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_BASIC_CONSTRAINTS),
+ new Bool(Bool.TAG, null, true),
+ new OctetString(OctetString.TAG, null, new Byte[]{ 0x30, 0x03, 0x01, 0x01, -1 }));
+ assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS, ext.getExtnId().getInts());
+ assertTrue(ext.getCritical().getValue());
+ assertArrayEquals(new Byte[]{ 0x30, 0x03, 0x01, 0x01, -1 }, ext.getExtnValue().getBytes());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ Extension parsed = new Extension(new BytesReader(EXT_SUBJECT_KEY_ID), false);
+ assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER, parsed.getExtnId().getInts());
+ assertNull(parsed.getCritical());
+ assertArrayEquals(new Byte[] {
+ 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66,
+ 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29,
+ -67, 0x6B, 0x49, 0x09, 0x61, -64
+ }, parsed.getExtnValue().getBytes());
+
+ parsed = new Extension(new BytesReader(EXT_KEY_USAGE), false);
+ assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE, parsed.getExtnId().getInts());
+ assertTrue(parsed.getCritical().getValue());
+ assertArrayEquals(new Byte[] {
+ 0x03, 0x02, 0x01, -122
+ }, parsed.getExtnValue().getBytes());
+ }
+
+ @Test
+ void testParseFail() throws ParseException {
+ // Too short (no ID)
+ assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{
+ 0x30, 0x00
+ }), false));
+ // Wrong ID tag
+ assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{
+ 0x30, 0x0E, // SEQUENCE Extension
+ 0x07, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage
+ 0x01, 0x01, -1, // BOOLEAN critical
+ 0x04, 0x04, // OCTET STRING
+ 0x03, 0x02, 0x01, -122
+ }), false));
+ // Wrong critical tag (neither bool nor sequence)
+ assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{
+ 0x30, 0x0E, // SEQUENCE Extension
+ 0x06, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage
+ 0x05, 0x01, -1, // BOOLEAN critical
+ 0x04, 0x04, // OCTET STRING
+ 0x03, 0x02, 0x01, -122
+ }), false));
+ // Critical and wrong value tag
+ assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{
+ 0x30, 0x0E, // SEQUENCE Extension
+ 0x06, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage
+ 0x01, 0x01, -1, // BOOLEAN critical
+ 0x09, 0x04, // OCTET STRING
+ 0x03, 0x02, 0x01, -122
+ }), false));
+
+ // No critical and wrong value tag
+ assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{
+ 0x30, 0x0B, // SEQUENCE Extension
+ 0x06, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage
+ 0x09, 0x04, // OCTET STRING
+ 0x03, 0x02, 0x01, -122
+ }), false));
+ }
+
+ @Test
+ void testEncode() {
+ assertArrayEquals(EXT_SUBJECT_KEY_ID, new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER),
+ null,
+ new OctetString(OctetString.TAG, null, new Byte[] {
+ 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66,
+ 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29,
+ -67, 0x6B, 0x49, 0x09, 0x61, -64
+ })).encodeDER());
+ assertArrayEquals(EXT_KEY_USAGE, new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_KEY_USAGE),
+ new Bool(Bool.TAG, null, true),
+ new OctetString(OctetString.TAG, null, new Byte[] {
+ 0x03, 0x02, 0x01, -122
+ })).encodeDER());
+ }
+}
diff --git a/src/test/model/pki/cert/ExtensionsTest.java b/src/test/model/pki/cert/ExtensionsTest.java
new file mode 100644
index 0000000..e50b3e6
--- /dev/null
+++ b/src/test/model/pki/cert/ExtensionsTest.java
@@ -0,0 +1,112 @@
+package model.pki.cert;
+
+import model.asn1.ASN1Object;
+import model.asn1.Bool;
+import model.asn1.ObjectIdentifier;
+import model.asn1.OctetString;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ExtensionsTest {
+ @Test
+ void testConstructor() {
+ final Extension ext1 = new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_BASIC_CONSTRAINTS),
+ new Bool(Bool.TAG, null, true),
+ new OctetString(OctetString.TAG, null, new Byte[]{0x30, 0x03, 0x01, 0x01, -1}));
+ final Extension ext2 = new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER),
+ null,
+ new OctetString(OctetString.TAG, null, new Byte[]{
+ 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66,
+ 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29,
+ -67, 0x6B, 0x49, 0x09, 0x61, -64
+ }));
+ final Extensions extensions = new Extensions(ASN1Object.TAG_SEQUENCE, null, new Extension[]{
+ ext1, ext2
+ });
+
+ assertEquals(2, extensions.getExtensions().length);
+ assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS, extensions.getExtensions()[0].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER, extensions.getExtensions()[1].getExtnId().getInts());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ final Extensions parsed = new Extensions(new BytesReader(
+ Stream.of(Arrays.asList(new Byte[]{0x30,
+ (byte) (ExtensionTest.EXT_KEY_USAGE.length + ExtensionTest.EXT_SUBJECT_KEY_ID.length)}),
+ Arrays.asList(ExtensionTest.EXT_KEY_USAGE),
+ Arrays.asList(ExtensionTest.EXT_SUBJECT_KEY_ID))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new)), false);
+ assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE, parsed.getExtensions()[0].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER, parsed.getExtensions()[1].getExtnId().getInts());
+ }
+
+ @Test
+ void testParseFail() {
+ assertThrows(ParseException.class, () -> {
+ new Extensions(new BytesReader(new Byte[]{0x30, 0x1}), false);
+ });
+ assertThrows(ParseException.class, () -> {
+ Byte[] bytes =
+ Stream.of(Arrays.asList(new Byte[]{0x30,
+ (byte) (ExtensionTest.EXT_KEY_USAGE.length + ExtensionTest.EXT_SUBJECT_KEY_ID.length)}),
+ Arrays.asList(ExtensionTest.EXT_KEY_USAGE),
+ Arrays.asList(ExtensionTest.EXT_SUBJECT_KEY_ID))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ assertEquals((byte) 0x30, bytes[2]);
+ bytes[2] = 0x31;
+ new Extensions(new BytesReader(bytes), false);
+ });
+ assertThrows(ParseException.class, () -> {
+ Byte[] bytes =
+ Stream.of(Arrays.asList(new Byte[]{0x30,
+ (byte) (ExtensionTest.EXT_KEY_USAGE.length + ExtensionTest.EXT_SUBJECT_KEY_ID.length)}),
+ Arrays.asList(ExtensionTest.EXT_KEY_USAGE),
+ Arrays.asList(ExtensionTest.EXT_SUBJECT_KEY_ID))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ assertEquals((byte) 0x30, bytes[2 + ExtensionTest.EXT_KEY_USAGE.length]);
+ bytes[2 + ExtensionTest.EXT_KEY_USAGE.length] = 0x31;
+ new Extensions(new BytesReader(bytes), false);
+ });
+ }
+
+ @Test
+ void testEncode() {
+ assertArrayEquals(
+ Stream.of(Arrays.asList(new Byte[]{0x30,
+ (byte) (ExtensionTest.EXT_KEY_USAGE.length +
+ ExtensionTest.EXT_SUBJECT_KEY_ID.length)}),
+ Arrays.asList(ExtensionTest.EXT_SUBJECT_KEY_ID),
+ Arrays.asList(ExtensionTest.EXT_KEY_USAGE))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new),
+ new Extensions(ASN1Object.TAG_SEQUENCE, null, new Extension[]{
+ new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER),
+ null,
+ new OctetString(OctetString.TAG, null, new Byte[]{
+ 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66,
+ 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29,
+ -67, 0x6B, 0x49, 0x09, 0x61, -64
+ })),
+ new Extension(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_KEY_USAGE),
+ new Bool(Bool.TAG, null, true),
+ new OctetString(OctetString.TAG, null, new Byte[]{
+ 0x03, 0x02, 0x01, -122
+ }))
+ }).encodeDER());
+ }
+}
diff --git a/src/test/model/pki/cert/TbsCertificateTest.java b/src/test/model/pki/cert/TbsCertificateTest.java
new file mode 100644
index 0000000..ae92ace
--- /dev/null
+++ b/src/test/model/pki/cert/TbsCertificateTest.java
@@ -0,0 +1,184 @@
+package model.pki.cert;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.TestConstants;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigInteger;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+
+import static model.TestConstants.mutate;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TbsCertificateTest {
+ @Test
+ void testConstructor() {
+ assertEquals(TbsCertificate.VERSION_V3, TestConstants.CERT_GENERATED.getVersion().getLong());
+ assertEquals(100, TestConstants.CERT_GENERATED.getSerialNumber().getLong());
+ assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, TestConstants.CERT_GENERATED.getSignature().getType().getInts());
+ assertEquals("CN=Test CA,C=CA", TestConstants.CERT_GENERATED.getIssuer().toString());
+ assertEquals(TestConstants.NOW, TestConstants.CERT_GENERATED.getValidity().getNotBefore().getTimestamp());
+ assertEquals(TestConstants.NOW.plusYears(1),
+ TestConstants.CERT_GENERATED.getValidity().getNotAfter().getTimestamp());
+ assertEquals("CN=Yuuta Liang,C=CA", TestConstants.CERT_GENERATED.getSubject().toString());
+ assertArrayEquals(ObjectIdentifier.OID_EC_PUBLIC_KEY,
+ TestConstants.CERT_GENERATED.getSubjectPublicKeyInfo().getAlgorithm().getType().getInts());
+ assertEquals(2, TestConstants.CERT_GENERATED.getExtensions().getExtensions().length);
+ assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS,
+ TestConstants.CERT_GENERATED.getExtensions().getExtensions()[0].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE,
+ TestConstants.CERT_GENERATED.getExtensions().getExtensions()[1].getExtnId().getInts());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ TbsCertificate parsed = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_L1_ECC)),
+ false);
+ assertEquals(TbsCertificate.VERSION_V3, parsed.getVersion().getLong());
+ assertEquals(0, parsed.getSerialNumber().getValue()
+ .compareTo(new BigInteger("644983544608556543477205958886697401602227090424")));
+ assertArrayEquals(ObjectIdentifier.OID_ECDSA_WITH_SHA256,
+ parsed.getSignature().getType().getInts());
+ assertNull(parsed.getSignature().getParameters());
+ assertEquals("CN=Yuuta Root CA,C=CA", parsed.getIssuer().toString());
+ assertEquals(ZonedDateTime.of(2023, 6, 23,
+ 2, 50, 46, 0, ZoneId.of("UTC")),
+ parsed.getValidity().getNotBefore().getTimestamp());
+ assertEquals(ZonedDateTime.of(2048, 6, 23,
+ 2, 50, 46, 0, ZoneId.of("UTC")),
+ parsed.getValidity().getNotAfter().getTimestamp());
+ assertEquals("CN=Yuuta Root CA,C=CA", parsed.getSubject().toString());
+ assertArrayEquals(ObjectIdentifier.OID_EC_PUBLIC_KEY,
+ parsed.getSubjectPublicKeyInfo().getAlgorithm().getType().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_PRIME256_V1,
+ ((ObjectIdentifier) parsed.getSubjectPublicKeyInfo().getAlgorithm().getParameters()).getInts());
+ assertEquals(4, parsed.getExtensions().getExtensions().length);
+ assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER,
+ parsed.getExtensions().getExtensions()[0].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_AUTHORITY_KEY_IDENTIFIER,
+ parsed.getExtensions().getExtensions()[1].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS,
+ parsed.getExtensions().getExtensions()[2].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE,
+ parsed.getExtensions().getExtensions()[3].getExtnId().getInts());
+
+ parsed = new TbsCertificate(
+ new BytesReader(trimToTbs(TestConstants.CERT_L2_RSA)),
+ false);
+ assertEquals(TbsCertificate.VERSION_V3, parsed.getVersion().getLong());
+ assertEquals(0, parsed.getSerialNumber().getValue()
+ .compareTo(new BigInteger("354327098948136693059815576591331472151989570311")));
+ assertArrayEquals(ObjectIdentifier.OID_ECDSA_WITH_SHA512,
+ parsed.getSignature().getType().getInts());
+ assertNull(parsed.getSignature().getParameters());
+ assertEquals("CN=Yuuta Root CA,C=CA", parsed.getIssuer().toString());
+ assertEquals(ZonedDateTime.of(2023, 6, 24,
+ 0, 15, 22, 0, ZoneId.of("UTC")),
+ parsed.getValidity().getNotBefore().getTimestamp());
+ assertEquals(ZonedDateTime.of(2033, 6, 21,
+ 0, 15, 22, 0, ZoneId.of("UTC")),
+ parsed.getValidity().getNotAfter().getTimestamp());
+ assertEquals("DC=MOE,DC=YUUTA,DC=AD,CN=Yuuta Home Issuing CA", parsed.getSubject().toString());
+ assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION,
+ parsed.getSubjectPublicKeyInfo().getAlgorithm().getType().getInts());
+ assertEquals(Null.TAG.getNumber(),
+ parsed.getSubjectPublicKeyInfo().getAlgorithm().getParameters().getTag().getNumber());
+ assertEquals(526, parsed.getSubjectPublicKeyInfo().getSubjectPublicKey().getVal().length);
+ assertEquals(6, parsed.getExtensions().getExtensions().length);
+ assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER,
+ parsed.getExtensions().getExtensions()[0].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_AUTHORITY_KEY_IDENTIFIER,
+ parsed.getExtensions().getExtensions()[1].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS,
+ parsed.getExtensions().getExtensions()[2].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE,
+ parsed.getExtensions().getExtensions()[3].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_CRL_DISTRIBUTION_POINTS,
+ parsed.getExtensions().getExtensions()[4].getExtnId().getInts());
+ assertArrayEquals(ObjectIdentifier.OID_AUTHORITY_INFO_ACCESS,
+ parsed.getExtensions().getExtensions()[5].getExtnId().getInts());
+
+ parsed = new TbsCertificate(
+ new BytesReader(Arrays.stream(TestConstants.CERT_V1).skip(4).toArray(Byte[]::new)),
+ false);
+ assertNull(parsed.getVersion());
+ assertNull(parsed.getExtensions());
+ }
+
+ @Test
+ void testParseFail() throws ParseException {
+ final Byte[] in = trimToTbs(TestConstants.CERT_L2_RSA);
+ // Wrong version parent tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 4, -96, 2)), false));
+ // Wrong version inner tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 6, 0x2, 3)), false));
+ // Wrong serial number tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 9, 0x2, 3)), false));
+ // Wrong signature tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 31, 0x30, 3)), false));
+ // Wrong issuer tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 43, 0x30, 0x31)), false));
+ // Wrong validity tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 82, 0x30, 0x31)), false));
+ // Wrong subject tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 114, 0x30, 0x31)), false));
+ // Wrong subject public key info tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 212, 0x30, 0x31)), false));
+ // Wrong extensions parent tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 762, -93, 0x31)), false));
+ // Wrong extensions inner tag
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 765, 0x30, 0x31)), false));
+ // Extensions exist, but wrong version
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 8, 0x2, TbsCertificate.VERSION_V2)),
+ false));
+ // Totally wrong version
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(mutate(in, 8, 0x2, TbsCertificate.VERSION_V3 + 1)),
+ false));
+
+ // Extensions exist, but no version
+ final TbsCertificate certV1 = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_V1)), false);
+ final TbsCertificate certV3 = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_L2_RSA)), false);
+ Byte[] wrongCert = new TbsCertificate(ASN1Object.TAG_SEQUENCE, null,
+ null,
+ certV1.getSerialNumber(),
+ certV1.getSignature(),
+ certV1.getIssuer(),
+ certV1.getValidity(),
+ certV1.getSubject(),
+ certV1.getSubjectPublicKeyInfo(),
+ certV3.getExtensions())
+ .encodeDER();
+ assertThrows(ParseException.class, () ->
+ new TbsCertificate(new BytesReader(wrongCert), false));
+ }
+
+ @Test
+ void testEncode() throws ParseException {
+ Byte[] in = trimToTbs(TestConstants.CERT_L1_ECC);
+ assertArrayEquals(Arrays.copyOfRange(in, 0, 345), new TbsCertificate(new BytesReader(in), false).encodeDER());
+ in = trimToTbs(TestConstants.CERT_L2_RSA);
+ assertArrayEquals(Arrays.copyOfRange(in, 0, 989), new TbsCertificate(new BytesReader(in), false).encodeDER());
+ in = trimToTbs(TestConstants.CERT_V1);
+ assertArrayEquals(Arrays.copyOfRange(in, 0, 583), new TbsCertificate(new BytesReader(in), false).encodeDER());
+ }
+
+ private static Byte[] trimToTbs(Byte[] in) {
+ return Arrays.stream(in).skip(4).toArray(Byte[]::new);
+ }
+}
diff --git a/src/test/model/pki/cert/ValidityTest.java b/src/test/model/pki/cert/ValidityTest.java
new file mode 100644
index 0000000..eba5092
--- /dev/null
+++ b/src/test/model/pki/cert/ValidityTest.java
@@ -0,0 +1,118 @@
+package model.pki.cert;
+
+import jdk.jshell.EvalException;
+import 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.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+import static model.TestConstants.combine;
+import static model.TestConstants.mutate;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class ValidityTest {
+ private ZonedDateTime now;
+
+ @BeforeEach
+ void setup() {
+ now = ZonedDateTime.now(ZoneId.of("UTC")).withNano(0);
+ }
+
+ @Test
+ void testConstructor() {
+ final ASN1Time time = new GeneralizedTime(GeneralizedTime.TAG, null, now);
+ assertEquals(time.getTimestamp(), new Validity(ASN1Object.TAG_SEQUENCE, null, time, time)
+ .getNotBefore().getTimestamp());
+ assertEquals(time.getTimestamp(), new Validity(ASN1Object.TAG_SEQUENCE, null, time, time)
+ .getNotAfter().getTimestamp());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ final ASN1Time utc = new UtcTime(UtcTime.TAG, null, now);
+ final ASN1Time gen = new GeneralizedTime(GeneralizedTime.TAG, null, now);
+ final Byte[] utcBytes = utc.encodeDER();
+ final Byte[] genBytes = gen.encodeDER();
+
+ // UTC, Generalized
+ Validity parsed = new Validity(new BytesReader(combine((byte) ASN1Object.TAG_SEQUENCE.getNumber(), utcBytes,
+ genBytes)), false);
+ assertEquals(UtcTime.TAG.getNumber(), parsed.getNotBefore().getTag().getNumber());
+ assertEquals(now, parsed.getNotBefore().getTimestamp());
+ assertEquals(GeneralizedTime.TAG.getNumber(), parsed.getNotAfter().getTag().getNumber());
+ assertEquals(now, parsed.getNotAfter().getTimestamp());
+
+ // UTC, UTC
+ parsed = new Validity(new BytesReader(combine((byte) ASN1Object.TAG_SEQUENCE.getNumber(), utcBytes, utcBytes)),
+ false);
+ assertEquals(UtcTime.TAG.getNumber(), parsed.getNotBefore().getTag().getNumber());
+ assertEquals(now, parsed.getNotBefore().getTimestamp());
+ assertEquals(UtcTime.TAG.getNumber(), parsed.getNotAfter().getTag().getNumber());
+ assertEquals(now, parsed.getNotAfter().getTimestamp());
+
+ // Generalized, Generalized
+ parsed = new Validity(new BytesReader(combine((byte) ASN1Object.TAG_SEQUENCE.getNumber(), genBytes, genBytes)),
+ false);
+ assertEquals(GeneralizedTime.TAG.getNumber(), parsed.getNotBefore().getTag().getNumber());
+ assertEquals(now, parsed.getNotBefore().getTimestamp());
+ assertEquals(GeneralizedTime.TAG.getNumber(), parsed.getNotAfter().getTag().getNumber());
+ assertEquals(now, parsed.getNotAfter().getTimestamp());
+
+ // Generalized, UTC
+ parsed = new Validity(new BytesReader(combine((byte) ASN1Object.TAG_SEQUENCE.getNumber(), genBytes, utcBytes)),
+ false);
+ assertEquals(GeneralizedTime.TAG.getNumber(), parsed.getNotBefore().getTag().getNumber());
+ assertEquals(now, parsed.getNotBefore().getTimestamp());
+ assertEquals(UtcTime.TAG.getNumber(), parsed.getNotAfter().getTag().getNumber());
+ assertEquals(now, parsed.getNotAfter().getTimestamp());
+ }
+
+ @Test
+ void testParseFail() {
+ final ASN1Time utc = new UtcTime(UtcTime.TAG, null, now);
+ final Byte[] utcBytes = utc.encodeDER();
+
+ // Too short
+ assertThrows(ParseException.class, () ->
+ new Validity(new BytesReader(new Byte[] {
+ 0x30, 0x0
+ }), false));
+ assertThrows(ParseException.class, () -> {
+ new Validity(new BytesReader(combine((byte) 0x30, utcBytes)), false);
+ });
+
+ // Illegal notBefore tag
+ assertThrows(ParseException.class, () -> {
+ new Validity(new BytesReader(mutate(combine((byte) 0x30, utcBytes, utcBytes), 2,
+ UtcTime.TAG.getNumber(), 0x2)), false);
+ });
+ // Illegal notAfter tag
+ assertThrows(ParseException.class, () -> {
+ new Validity(new BytesReader(mutate(combine((byte) 0x30, utcBytes, utcBytes), utcBytes.length + 2,
+ UtcTime.TAG.getNumber(), 0x2)), false);
+ });
+ }
+
+ @Test
+ void testEncode() {
+ final ASN1Time utc = new UtcTime(UtcTime.TAG, null, now);
+ final ASN1Time gen = new GeneralizedTime(GeneralizedTime.TAG, null, now);
+ final Byte[] utcBytes = utc.encodeDER();
+ final Byte[] genBytes = gen.encodeDER();
+
+ assertArrayEquals(Stream.of(Arrays.asList(new Byte[]{ 0x30, (byte) (utcBytes.length + genBytes.length) }),
+ Arrays.asList(utcBytes),
+ Arrays.asList(genBytes))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new),
+ new Validity(ASN1Object.TAG_SEQUENCE, null, utc, gen)
+ .encodeDER());
+ }
+}
diff --git a/src/test/model/pki/crl/CertificateListContentTest.java b/src/test/model/pki/crl/CertificateListContentTest.java
new file mode 100644
index 0000000..ec18629
--- /dev/null
+++ b/src/test/model/pki/crl/CertificateListContentTest.java
@@ -0,0 +1,37 @@
+package model.pki.crl;
+
+import model.TestConstants;
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.pki.AlgorithmIdentifier;
+import model.x501.AttributeTypeAndValue;
+import model.x501.Name;
+import model.x501.RelativeDistinguishedName;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import static model.TestConstants.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CertificateListContentTest {
+ @Test
+ void testConstructor() {
+ assertEquals(1, CRL_CONTENT_1.getVersion().getLong());
+ assertEquals("CN=Test CA", CRL_CONTENT_1.getIssuer().toString());
+ assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION,
+ CRL_CONTENT_1.getSignature().getType().getInts());
+ assertEquals(GeneralizedTime.TAG.getNumber(),
+ CRL_CONTENT_1.getThisUpdate().getTag().getNumber());
+ assertNull(CRL_CONTENT_1.getNextUpdate());
+ assertEquals(2, CRL_CONTENT_1.getRevokedCertificates().length);
+ }
+
+ @Test
+ void testEncode() {
+ assertArrayEquals(CRL_CONTENT_1_DER, CRL_CONTENT_1.encodeDER());
+ assertArrayEquals(CRL_CONTENT_2_DER, CRL_CONTENT_2.encodeDER());
+ }
+}
diff --git a/src/test/model/pki/crl/CertificateListTest.java b/src/test/model/pki/crl/CertificateListTest.java
new file mode 100644
index 0000000..0f4f06c
--- /dev/null
+++ b/src/test/model/pki/crl/CertificateListTest.java
@@ -0,0 +1,52 @@
+package model.pki.crl;
+
+import model.TestConstants;
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Null;
+import model.asn1.ObjectIdentifier;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+import model.pki.cert.Certificate;
+import model.pki.cert.TbsCertificate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static model.TestConstants.combine;
+import static model.TestConstants.mutate;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CertificateListTest {
+ private CertificateList crl;
+
+ @BeforeEach
+ void setup() {
+ crl = new CertificateList(ASN1Object.TAG_SEQUENCE, null,
+ TestConstants.CRL_CONTENT_1,
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)),
+ new BitString(BitString.TAG, null, 0, new Byte[]{ 1, 2, 3 }));
+ }
+
+ @Test
+ void testConstructor() {
+ assertEquals(2, crl.getCrl().getRevokedCertificates().length);
+ assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION,
+ crl.getSignatureAlgorithm().getType().getInts());
+ assertArrayEquals(new Byte[]{ 1, 2, 3 },
+ crl.getSignature().getConvertedVal());
+ }
+
+ @Test
+ void testEncode() {
+ assertArrayEquals(combine((byte) 0x30,
+ TestConstants.CRL_CONTENT_1_DER,
+ new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_RSA_ENCRYPTION),
+ new Null(Null.TAG, null)).encodeDER(),
+ new BitString(BitString.TAG, null, 0, new Byte[]{ 1, 2, 3 }).encodeDER()),
+ crl.encodeDER());
+ }
+}
diff --git a/src/test/model/pki/crl/RevokedCertificateTest.java b/src/test/model/pki/crl/RevokedCertificateTest.java
new file mode 100644
index 0000000..659e421
--- /dev/null
+++ b/src/test/model/pki/crl/RevokedCertificateTest.java
@@ -0,0 +1,25 @@
+package model.pki.crl;
+
+import model.asn1.*;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import static model.TestConstants.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class RevokedCertificateTest {
+ @Test
+ void testConstructor() {
+ assertEquals(123, REVOKED_CESSATION.getSerialNumber().getLong());
+ assertEquals(UtcTime.TAG.getNumber(), REVOKED_CESSATION.getRevocationDate().getTag().getNumber());
+ assertEquals(Reason.CESSATION_OF_OPERATION, REVOKED_CESSATION.getReason());
+ }
+
+ @Test
+ void testEncode() {
+ assertArrayEquals(REVOKED_KEY_COMPROMISE_DER, REVOKED_KEY_COMPROMISE.encodeDER());
+ }
+}
diff --git a/src/test/model/x501/AttributeTypeAndValueTest.java b/src/test/model/x501/AttributeTypeAndValueTest.java
new file mode 100644
index 0000000..ea9c17e
--- /dev/null
+++ b/src/test/model/x501/AttributeTypeAndValueTest.java
@@ -0,0 +1,90 @@
+package model.x501;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import org.junit.jupiter.api.Test;
+
+import static model.asn1.ObjectIdentifier.*;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class AttributeTypeAndValueTest {
+ @Test
+ void testConstructor() throws ParseException {
+ assertArrayEquals(OID_OU,
+ new AttributeTypeAndValue(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_OU),
+ new Null(Null.TAG, null))
+ .getType().getInts());
+ assertEquals("123",
+ new AttributeTypeAndValue(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_OU),
+ new PrintableString(PrintableString.TAG, null, "123"))
+ .getValue().toString());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ // C = IT
+ assertArrayEquals(OID_C, new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54
+ }), false).getType().getInts());
+ assertEquals("IT", ((PrintableString) new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54
+ }), false).getValue()).getString());
+
+ // CN = Test ed25519
+ assertArrayEquals(OID_CN, new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C,
+ 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64,
+ 0x32, 0x35, 0x35, 0x31, 0x39
+ }), false).getType().getInts());
+ assertEquals("Test ed25519", ((ASN1String) new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C,
+ 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64,
+ 0x32, 0x35, 0x35, 0x31, 0x39
+ }), false).getValue()).getString());
+ }
+
+ @Test
+ void testParseFail() {
+ // No type
+ assertThrows(ParseException.class, () -> new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x0
+ }), false));
+ // No value
+ assertThrows(ParseException.class, () -> new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x5, 0x6, 0x3, 0x55, 0x4, 0x6
+ }), false));
+ // Incorrect type tag (should be OID)
+ assertThrows(ParseException.class, () -> new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x9, 0x7, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54
+ }), false));
+ }
+
+ @Test
+ void testEncode() throws ParseException {
+ assertArrayEquals(new Byte[]{
+ 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C,
+ 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64,
+ 0x32, 0x35, 0x35, 0x31, 0x39
+ }, new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_CN),
+ new UTF8String(UTF8String.TAG, null, "Test ed25519"))
+ .encodeDER());
+ }
+
+ @Test
+ void testToString() throws ParseException {
+ assertEquals("CN=Test ed25519", new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C,
+ 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64,
+ 0x32, 0x35, 0x35, 0x31, 0x39
+ }), false).toString());
+ assertEquals("C=IT", new AttributeTypeAndValue(new BytesReader(new Byte[]{
+ 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54
+ }), false).toString());
+ }
+}
diff --git a/src/test/model/x501/NameTest.java b/src/test/model/x501/NameTest.java
new file mode 100644
index 0000000..c649798
--- /dev/null
+++ b/src/test/model/x501/NameTest.java
@@ -0,0 +1,167 @@
+package model.x501;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.TestConstants;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class NameTest {
+ @Test
+ void testConstructor() {
+ assertEquals("users",
+ TestConstants.NAME_1.getRdnSequence()[1].getArray()[0].getValue().toString());
+ assertEquals("CN",
+ TestConstants.NAME_2.getRdnSequence()[2].getArray()[0].getValue().toString());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ assertEquals("CA", new Name(new BytesReader(new Byte[]{
+ 0x30, 45, // Name
+
+ 0x31, 14, // RDN[0]
+ 0x30, 12, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x03, // CN
+ 0x13, 5, 'y', 'u', 'u', 't', 'a', // yuuta
+
+ 0x31, 14, // RDN[1]
+ 0x30, 12, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0xB, // OU
+ 0x13, 5, 'u', 's', 'e', 'r', 's', // users
+
+ 0x31, 11, // RDN[2]
+ 0x30, 9, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x6, // C
+ 0x13, 2, 'C', 'A' // CA
+ }), false).getRdnSequence()[2].getArray()[0].getValue().toString());
+
+ assertEquals("SN=Qwq", new Name(new BytesReader(new Byte[]{
+ 0x30, 38, // Name
+
+ 0x31, 12, // RDN[0]
+ 0x30, 10, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x04, // CN
+ 0x13, 3, 'Q', 'w', 'q', // Qwq
+
+ 0x31, 9, // RDN[1]
+ 0x30, 7, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0xA, // O
+ 0x13, 2, 'I', 'T', // IT
+
+ 0x31, 11, // RDN[2]
+ 0x30, 9, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x6, // C
+ 0x13, 2, 'C', 'N' // CN
+ }), false).getRdnSequence()[0].toString());
+ }
+
+ @Test
+ void testParseFail() {
+ assertThrows(ParseException.class, () -> new Name(new BytesReader(new Byte[]{
+ 0x30, 38, // Name
+
+ // Wrong tag here
+ 0x30, 12, // RDN[0]
+ 0x30, 10, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x04, // CN
+ 0x13, 3, 'Q', 'w', 'q', // Qwq
+
+ 0x31, 9, // RDN[1]
+ 0x30, 7, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0xA, // O
+ 0x13, 2, 'I', 'T', // IT
+
+ 0x31, 11, // RDN[2]
+ 0x30, 9, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x6, // C
+ 0x13, 2, 'C', 'N' // CN
+ }), false));
+ assertThrows(ParseException.class, () -> new Name(new BytesReader(new Byte[]{
+ 0x30, 38, // Name
+
+ 0x31, 12, // RDN[0]
+ 0x30, 10, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x04, // CN
+ 0x13, 3, 'Q', 'w', 'q', // Qwq
+
+ 0x31, 9, // RDN[1]
+ // Wrong tag here
+ 0x31, 7, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0xA, // O
+ 0x13, 2, 'I', 'T', // IT
+
+ 0x31, 11, // RDN[2]
+ 0x30, 9, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x6, // C
+ 0x13, 2, 'C', 'N' // CN
+ }), false));
+ assertThrows(ParseException.class, () -> new Name(new BytesReader(new Byte[]{
+ 0x30, 38, // Name
+
+ 0x31, 12, // RDN[0]
+ 0x30, 10, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x04, // CN
+ 0x13, 3, 'Q', 'w', 'q', // Qwq
+
+ 0x31, 9, // RDN[1]
+ 0x30, 7, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0xA, // O
+ 0x13, 2, 'I', 'T', // IT
+
+ // Wrong tag here
+ 0x30, 11, // RDN[2]
+ 0x30, 9, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x6, // C
+ 0x13, 2, 'C', 'N' // CN
+ }), false));
+ }
+
+ @Test
+ void testEncode() {
+ assertArrayEquals(new Byte[]{
+ 0x30, 45, // Name
+
+ 0x31, 14, // RDN[0]
+ 0x30, 12, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x03, // CN
+ 0x13, 5, 'y', 'u', 'u', 't', 'a', // yuuta
+
+ 0x31, 14, // RDN[1]
+ 0x30, 12, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0xB, // OU
+ 0x13, 5, 'u', 's', 'e', 'r', 's', // users
+
+ 0x31, 11, // RDN[2]
+ 0x30, 9, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x6, // C
+ 0x13, 2, 'C', 'A' // CA
+ }, TestConstants.NAME_1.encodeDER());
+
+ assertArrayEquals(new Byte[]{
+ 0x30, 40, // Name
+
+ 0x31, 12, // RDN[0]
+ 0x30, 10, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x04, // CN
+ 0x13, 3, 'Q', 'w', 'q', // Qwq
+
+ 0x31, 11, // RDN[1]
+ 0x30, 9, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0xA, // O
+ 0x13, 2, 'I', 'T', // IT
+
+ 0x31, 11, // RDN[2]
+ 0x30, 9, // KV[0]
+ 0x6, 3, 0x55, 0x04, 0x6, // C
+ 0x13, 2, 'C', 'N' // CN
+ }, TestConstants.NAME_2.encodeDER());
+ }
+
+ @Test
+ void testToString() {
+ assertEquals("CN=yuuta,OU=users,C=CA", TestConstants.NAME_1.toString());
+ assertEquals("SN=Qwq,O=IT,C=CN", TestConstants.NAME_2.toString());
+ }
+}
diff --git a/src/test/model/x501/RelativeDistinguishedNameTest.java b/src/test/model/x501/RelativeDistinguishedNameTest.java
new file mode 100644
index 0000000..d066010
--- /dev/null
+++ b/src/test/model/x501/RelativeDistinguishedNameTest.java
@@ -0,0 +1,95 @@
+package model.x501;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.TestConstants;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+
+import static model.asn1.ASN1Object.TAG_SET;
+import static model.asn1.ObjectIdentifier.OID_C;
+import static model.asn1.ObjectIdentifier.OID_OU;
+import static org.junit.jupiter.api.Assertions.*;
+
+public class RelativeDistinguishedNameTest {
+ @Test
+ void testConstructor() throws ParseException {
+ assertArrayEquals(OID_OU,
+ new RelativeDistinguishedName(TAG_SET, null,
+ new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_OU),
+ new Null(Null.TAG, null))
+ }).getArray()[0].getType().getInts());
+ assertEquals("123",
+ new RelativeDistinguishedName(TAG_SET, null,
+ new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_OU),
+ new PrintableString(PrintableString.TAG, null, "123"))
+ }).getArray()[0].getValue().toString());
+ }
+
+ @Test
+ void testParse() throws ParseException {
+ assertEquals(1, TestConstants.L_MILANO.getArray().length);
+ assertEquals("Milano", TestConstants.L_MILANO.getArray()[0].getValue().toString());
+
+ assertEquals(2, TestConstants.L_MILANO_CN_TEST_ED25519.getArray().length);
+ assertEquals("Test ed25519",
+ TestConstants.L_MILANO_CN_TEST_ED25519.getArray()[0].getValue().toString());
+ }
+
+ @Test
+ void testParseFail() {
+ // Invalid child tag
+ assertThrows(ParseException.class, () ->
+ new RelativeDistinguishedName(new BytesReader(new Byte[] {
+ 0x31, 0x0F,
+ 0x31, 0x0D, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0C,
+ 0x06, 0x4D, 0x69, 0x6C, 0x61, 0x6E, 0x6F
+ }), false));
+ assertThrows(ParseException.class, () ->
+ new RelativeDistinguishedName(new BytesReader(new Byte[] {
+ 0x31, 0x23,
+ // CN
+ 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C,
+ 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64, 0x32,
+ 0x35, 0x35, 0x31, 0x39,
+ // L
+ 0x31, 0x0D, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0C,
+ 0x06, 0x4D, 0x69, 0x6C, 0x61, 0x6E, 0x6F
+ }), false));
+ }
+
+ @Test
+ void testEncode() throws ParseException {
+ assertArrayEquals(TestConstants.combine((byte) 0x31,
+ TestConstants.CN_TEST_ED25519_DER,
+ TestConstants.L_MILANO_DER),
+ TestConstants.L_MILANO_CN_TEST_ED25519.encodeDER());
+ assertArrayEquals(new Byte[]{
+ 0x31, 15,
+ 0x30, 13,
+ 0x06, 3, 0x55, 0x04, 0x06,
+ 0x13, 6, '1', '2', '3', '1', '2', '3'
+
+ }, new RelativeDistinguishedName(TAG_SET, null,
+ new AttributeTypeAndValue[]{
+ new AttributeTypeAndValue(
+ ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_C),
+ new PrintableString(PrintableString.TAG, null, "123123"))})
+ .encodeDER());
+ }
+
+ @Test
+ void testToString() {
+ assertEquals("L=Milano", TestConstants.L_MILANO.toString());
+ assertEquals("CN=Test ed25519+L=Milano", TestConstants.L_MILANO_CN_TEST_ED25519.toString());
+ }
+}