From d342a45d98c4795b3a3fe1aaef5236ad4a782b55 Mon Sep 17 00:00:00 2001 From: Yuuta Liang Date: Thu, 12 Oct 2023 12:10:33 +0800 Subject: Implement data structures from X.680, X.501, X.509, and PKCS#10, with X.690 encoding / decoding support The implementation took four days, and it is still a little bit rough. Updated version should arrive soon. Signed-off-by: Yuuta Liang --- src/main/model/asn1/ASN1Object.java | 201 ++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 src/main/model/asn1/ASN1Object.java (limited to 'src/main/model/asn1/ASN1Object.java') 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 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 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; + } +} -- cgit v1.2.3