diff options
author | Yuuta Liang <yuutaw@students.cs.ubc.ca> | 2023-10-12 12:10:33 +0800 |
---|---|---|
committer | Yuuta Liang <yuutaw@students.cs.ubc.ca> | 2023-10-12 12:10:33 +0800 |
commit | d342a45d98c4795b3a3fe1aaef5236ad4a782b55 (patch) | |
tree | f4ebc0ad962b138d9371413fcc71c97a559df506 /src/main/model | |
parent | e60c9c76243cfe0a408af98dc60bedb973e815db (diff) | |
download | jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar.gz jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar.bz2 jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.zip |
Implement data structures from X.680, X.501, X.509, and PKCS#10, with X.690 encoding / decoding support
The implementation took four days, and it is still a little bit rough. Updated version should arrive soon.
Signed-off-by: Yuuta Liang <yuutaw@students.cs.ubc.ca>
Diffstat (limited to 'src/main/model')
40 files changed, 3418 insertions, 5 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; + } +} |