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/MyModel.java | 5 - src/main/model/asn1/ASN1Length.java | 101 ++++ src/main/model/asn1/ASN1Object.java | 201 ++++++++ src/main/model/asn1/ASN1String.java | 85 ++++ src/main/model/asn1/ASN1Time.java | 66 +++ src/main/model/asn1/BitString.java | 95 ++++ src/main/model/asn1/Bool.java | 60 +++ src/main/model/asn1/Encodable.java | 9 + src/main/model/asn1/GeneralizedTime.java | 90 ++++ src/main/model/asn1/IA5String.java | 53 ++ src/main/model/asn1/Int.java | 81 +++ src/main/model/asn1/Null.java | 45 ++ src/main/model/asn1/ObjectIdentifier.java | 204 ++++++++ src/main/model/asn1/OctetString.java | 47 ++ src/main/model/asn1/PrintableString.java | 49 ++ src/main/model/asn1/Tag.java | 109 +++++ src/main/model/asn1/TagClass.java | 42 ++ src/main/model/asn1/UTF8String.java | 40 ++ src/main/model/asn1/UtcTime.java | 91 ++++ src/main/model/asn1/exceptions/ParseException.java | 10 + src/main/model/asn1/parsing/BytesReader.java | 105 ++++ src/main/model/csr/Attribute.java | 83 ++++ src/main/model/csr/Attributes.java | 65 +++ src/main/model/csr/CertificationRequest.java | 110 +++++ src/main/model/csr/CertificationRequestInfo.java | 127 +++++ src/main/model/csr/Values.java | 69 +++ src/main/model/pki/AlgorithmIdentifier.java | 105 ++++ src/main/model/pki/SubjectPublicKeyInfo.java | 83 ++++ src/main/model/pki/cert/Certificate.java | 127 +++++ src/main/model/pki/cert/Extension.java | 113 +++++ src/main/model/pki/cert/Extensions.java | 67 +++ src/main/model/pki/cert/TbsCertificate.java | 263 ++++++++++ src/main/model/pki/cert/Validity.java | 95 ++++ src/main/model/pki/crl/CertificateList.java | 76 +++ src/main/model/pki/crl/CertificateListContent.java | 106 ++++ src/main/model/pki/crl/Reason.java | 27 + src/main/model/pki/crl/RevokedCertificate.java | 72 +++ src/main/model/x501/AttributeTypeAndValue.java | 90 ++++ src/main/model/x501/Name.java | 79 +++ src/main/model/x501/RelativeDistinguishedName.java | 78 +++ src/main/ui/Utils.java | 105 ++++ src/test/model/MyModelTest.java | 5 - src/test/model/TestConstants.java | 544 +++++++++++++++++++++ src/test/model/asn1/ASN1LengthTest.java | 94 ++++ src/test/model/asn1/ASN1ObjectTest.java | 177 +++++++ src/test/model/asn1/BitStringTest.java | 75 +++ src/test/model/asn1/BoolTest.java | 45 ++ src/test/model/asn1/GeneralizedTimeTest.java | 119 +++++ src/test/model/asn1/IA5StringTest.java | 79 +++ src/test/model/asn1/IntTest.java | 96 ++++ src/test/model/asn1/NullTest.java | 38 ++ src/test/model/asn1/ObjectIdentifierTest.java | 89 ++++ src/test/model/asn1/OctetStringTest.java | 44 ++ src/test/model/asn1/PrintableStringTest.java | 71 +++ src/test/model/asn1/TagClassTest.java | 15 + src/test/model/asn1/TagTest.java | 107 ++++ src/test/model/asn1/UTF8StringTest.java | 64 +++ src/test/model/asn1/UtcTimeTest.java | 121 +++++ src/test/model/asn1/parsing/BytesReaderTest.java | 95 ++++ src/test/model/csr/AttributeTest.java | 53 ++ src/test/model/csr/AttributesTest.java | 69 +++ .../model/csr/CertificationRequestInfoTest.java | 174 +++++++ src/test/model/csr/CertificationRequestTest.java | 114 +++++ src/test/model/csr/ValuesTest.java | 133 +++++ src/test/model/pki/AlgorithmIdentifierTest.java | 84 ++++ src/test/model/pki/SubjectPublicKeyInfoTest.java | 115 +++++ src/test/model/pki/cert/CertificateTest.java | 81 +++ src/test/model/pki/cert/ExtensionTest.java | 118 +++++ src/test/model/pki/cert/ExtensionsTest.java | 112 +++++ src/test/model/pki/cert/TbsCertificateTest.java | 184 +++++++ src/test/model/pki/cert/ValidityTest.java | 118 +++++ .../model/pki/crl/CertificateListContentTest.java | 37 ++ src/test/model/pki/crl/CertificateListTest.java | 52 ++ src/test/model/pki/crl/RevokedCertificateTest.java | 25 + src/test/model/x501/AttributeTypeAndValueTest.java | 90 ++++ src/test/model/x501/NameTest.java | 167 +++++++ .../model/x501/RelativeDistinguishedNameTest.java | 95 ++++ 77 files changed, 7217 insertions(+), 10 deletions(-) delete mode 100644 src/main/model/MyModel.java create mode 100644 src/main/model/asn1/ASN1Length.java create mode 100644 src/main/model/asn1/ASN1Object.java create mode 100644 src/main/model/asn1/ASN1String.java create mode 100644 src/main/model/asn1/ASN1Time.java create mode 100644 src/main/model/asn1/BitString.java create mode 100644 src/main/model/asn1/Bool.java create mode 100644 src/main/model/asn1/Encodable.java create mode 100644 src/main/model/asn1/GeneralizedTime.java create mode 100644 src/main/model/asn1/IA5String.java create mode 100644 src/main/model/asn1/Int.java create mode 100644 src/main/model/asn1/Null.java create mode 100644 src/main/model/asn1/ObjectIdentifier.java create mode 100644 src/main/model/asn1/OctetString.java create mode 100644 src/main/model/asn1/PrintableString.java create mode 100644 src/main/model/asn1/Tag.java create mode 100644 src/main/model/asn1/TagClass.java create mode 100644 src/main/model/asn1/UTF8String.java create mode 100644 src/main/model/asn1/UtcTime.java create mode 100644 src/main/model/asn1/exceptions/ParseException.java create mode 100644 src/main/model/asn1/parsing/BytesReader.java create mode 100644 src/main/model/csr/Attribute.java create mode 100644 src/main/model/csr/Attributes.java create mode 100644 src/main/model/csr/CertificationRequest.java create mode 100644 src/main/model/csr/CertificationRequestInfo.java create mode 100644 src/main/model/csr/Values.java create mode 100644 src/main/model/pki/AlgorithmIdentifier.java create mode 100644 src/main/model/pki/SubjectPublicKeyInfo.java create mode 100644 src/main/model/pki/cert/Certificate.java create mode 100644 src/main/model/pki/cert/Extension.java create mode 100644 src/main/model/pki/cert/Extensions.java create mode 100644 src/main/model/pki/cert/TbsCertificate.java create mode 100644 src/main/model/pki/cert/Validity.java create mode 100644 src/main/model/pki/crl/CertificateList.java create mode 100644 src/main/model/pki/crl/CertificateListContent.java create mode 100644 src/main/model/pki/crl/Reason.java create mode 100644 src/main/model/pki/crl/RevokedCertificate.java create mode 100644 src/main/model/x501/AttributeTypeAndValue.java create mode 100644 src/main/model/x501/Name.java create mode 100644 src/main/model/x501/RelativeDistinguishedName.java create mode 100644 src/main/ui/Utils.java delete mode 100644 src/test/model/MyModelTest.java create mode 100644 src/test/model/TestConstants.java create mode 100644 src/test/model/asn1/ASN1LengthTest.java create mode 100644 src/test/model/asn1/ASN1ObjectTest.java create mode 100644 src/test/model/asn1/BitStringTest.java create mode 100644 src/test/model/asn1/BoolTest.java create mode 100644 src/test/model/asn1/GeneralizedTimeTest.java create mode 100644 src/test/model/asn1/IA5StringTest.java create mode 100644 src/test/model/asn1/IntTest.java create mode 100644 src/test/model/asn1/NullTest.java create mode 100644 src/test/model/asn1/ObjectIdentifierTest.java create mode 100644 src/test/model/asn1/OctetStringTest.java create mode 100644 src/test/model/asn1/PrintableStringTest.java create mode 100644 src/test/model/asn1/TagClassTest.java create mode 100644 src/test/model/asn1/TagTest.java create mode 100644 src/test/model/asn1/UTF8StringTest.java create mode 100644 src/test/model/asn1/UtcTimeTest.java create mode 100644 src/test/model/asn1/parsing/BytesReaderTest.java create mode 100644 src/test/model/csr/AttributeTest.java create mode 100644 src/test/model/csr/AttributesTest.java create mode 100644 src/test/model/csr/CertificationRequestInfoTest.java create mode 100644 src/test/model/csr/CertificationRequestTest.java create mode 100644 src/test/model/csr/ValuesTest.java create mode 100644 src/test/model/pki/AlgorithmIdentifierTest.java create mode 100644 src/test/model/pki/SubjectPublicKeyInfoTest.java create mode 100644 src/test/model/pki/cert/CertificateTest.java create mode 100644 src/test/model/pki/cert/ExtensionTest.java create mode 100644 src/test/model/pki/cert/ExtensionsTest.java create mode 100644 src/test/model/pki/cert/TbsCertificateTest.java create mode 100644 src/test/model/pki/cert/ValidityTest.java create mode 100644 src/test/model/pki/crl/CertificateListContentTest.java create mode 100644 src/test/model/pki/crl/CertificateListTest.java create mode 100644 src/test/model/pki/crl/RevokedCertificateTest.java create mode 100644 src/test/model/x501/AttributeTypeAndValueTest.java create mode 100644 src/test/model/x501/NameTest.java create mode 100644 src/test/model/x501/RelativeDistinguishedNameTest.java 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 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; + } +} 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 lowest part. + * For example, consider: + *
+ *     0b 00000001 10111001 01110111 (pad to the highest)
+ *     Will be encoded into
+ *     0b 01101110 01011101 11000000 (pad to the lowest)
+ *                            ^^^^^^
+ * 
+ * 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: + *
+ *     0b 00000110 01101110 01011101 11000000
+ *        ^  6   ^ ^ Original Number  ^^Pad^
+ * 
+ * + * 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 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 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 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 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 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: + *
+ *   Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE {
+ *       type   ATTRIBUTE.&id({IOSet}),
+ *       values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type})
+ *   }
+ * 
+ * + * Represents a key - values pair in the CSR attribute. + */ +public class Attribute extends ASN1Object { + /** + * The type of that attribute. For example,
2.5.29.14
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. + *
+ *     Attributes { ATTRIBUTE:IOSet } ::= SET OF Attribute{{ IOSet }}
+ * 
+ */ +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 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. + *
+ *    CertificationRequest ::= SEQUENCE {
+ *         certificationRequestInfo CertificationRequestInfo,
+ *         signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }},
+ *         signature          BIT STRING
+ *    }
+ * 
+ * + * 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
signature
. + */ + private final CertificationRequestInfo certificationRequestInfo; + + /** + * The algorithm used for
signature
. + */ + 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}. + * + *
+ *    DEFINITIONS IMPLICIT TAGS ::=
+ *
+ *    CertificationRequestInfo ::= SEQUENCE {
+ *         version       INTEGER { v1(0) } (v1,...),
+ *         subject       Name,
+ *         subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }},
+ *         attributes    [0] Attributes{{ CRIAttributes }}
+ *    }
+ *
+ * 
+ * + * 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. + *
+ *   Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE {
+ *       type   ATTRIBUTE.&id({IOSet}),
+ *       values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type})
+ *   }
+ * 
+ * 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 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: + *
+ *   AttributeTypeAndValue ::= SEQUENCE {
+ *       type ATTRIBUTE.&id({SupportedAttributes}),
+ *       value ATTRIBUTE.&Type({SupportedAttributes}{@type}) OPTIONAL,
+ *   ... }
+ * 
+ */ +public class AlgorithmIdentifier extends ASN1Object { + /** + * The type of that attribute. For example,
1.2.840.113549.1.1.11
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: + *
+     *     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 --
+     *    }
+     * 
+ */ + 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.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: + *
+ *     SubjectPublicKeyInfo ::= SEQUENCE {
+ *          algorithm AlgorithmIdentifier{{SupportedAlgorithms}},
+ *          subjectPublicKey BIT STRING,
+ *     ... }
+ * 
+ * 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. + *
+ *    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},
+ *    ... }
+ * 
+ * + * 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
signature
. + */ + private final TbsCertificate certificate; + + /** + * The algorithm used for
signature
. + */ + 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. + *
+ *     Extension ::= SEQUENCE {
+ *      extnId EXTENSION.&id({ExtensionSet}),
+ *      critical BOOLEAN DEFAULT FALSE,
+ *      extnValue OCTET STRING
+ *          (CONTAINING EXTENSION.&ExtnType({ExtensionSet}{@extnId})
+ *              ENCODED BY der),
+ *     ... }
+ * 
+ * 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.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: + *
+ *     Extensions ::= SEQUENCE OF Extension
+ * 
+ */ +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 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 + * + *
+ *  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
+ * 
+ * + * 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. + *
+     *   [0] Version DEFAULT v1
+     * 
+ */ + private final Int version; + + /** + * The serial number of that certificate that is unique across the CA. + *
+     *     serialNumber CertificateSerialNumber
+     *     CertificateSerialNumber ::= INTEGER
+     * 
+ */ + private final Int serialNumber; + + private final AlgorithmIdentifier signature; + + /** + * The subject and issuer distinguished names. + *
+     *     issuer Name,
+     *     subject Name
+     * 
+ */ + 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.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.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: + *
+ *     Validity ::= SEQUENCE {
+ *         notBefore Time,
+ *         notAfter Time,
+ *         ...
+ *     }
+ *
+ *     Time ::= CHOICE {
+ *         utcTime UTCTime,
+ *         generalizedTime GeneralizedTime
+ *     }
+ * 
+ * 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. + *
+ *      CertificateList ::= SIGNED{CertificateListContent}
+ * 
+ */ +public class CertificateList extends ASN1Object { + /** + * All info of that CRL, excluding the signature. + * It will be signed, and the signature is in
signature
. + */ + private final CertificateListContent crl; + + /** + * The algorithm used for
signature
. + */ + 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: + * + *
+ *     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 }
+ * 
+ * + * 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 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.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. + *
+ *     SEQUENCE {
+ *        serialNumber CertificateSerialNumber,
+ *        revocationDate Time,
+ *        crlEntryExtensions Extensions OPTIONAL,
+ *     ...}
+ * 
+ */ +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: + *
+ *   AttributeTypeAndValue ::= SEQUENCE {
+ *       type ATTRIBUTE.&id({SupportedAttributes}),
+ *       value ATTRIBUTE.&Type({SupportedAttributes}{@type}),
+ *   ... }
+ * 
+ */ +public class AttributeTypeAndValue extends ASN1Object { + /** + * The type of that attribute. For example,
2.5.4.10
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). + *
+ *     Name ::= CHOICE { -- only one possibility for now -- rdnSequence RDNSequence }
+ *     RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
+ *     DistinguishedName ::= RDNSequence
+ * 
+ */ +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 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. + *
+ *      RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
+ * 
+ * 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 list = new ArrayList<>(); + for (int i = 0; i < getLength();) { + int index = encoded.getIndex(); + final AttributeTypeAndValue value = new AttributeTypeAndValue(encoded, false); + value.getTag().enforce(TAG_SEQUENCE); + list.add(value); + index = encoded.getIndex() - index; + i += index; + } + this.array = list.toArray(new AttributeTypeAndValue[0]); + } + + /** + * EFFECTS: Encode the SET OF into DER, keep order. Values will be encoded one-by-one. + */ + @Override + public Byte[] encodeValueDER() { + return Stream.of(array) + .map(Encodable::encodeDER) + .flatMap(Arrays::stream) + .toArray(Byte[]::new); + } + + /** + * EFFECT: Encode into multi-valed RDN strings like CN=yuuta+CN=qwq + */ + @Override + public String toString() { + return Stream.of(array) + .map(AttributeTypeAndValue::toString) + .collect(Collectors.joining("+")); + } + + public AttributeTypeAndValue[] getArray() { + return array; + } +} diff --git a/src/main/ui/Utils.java b/src/main/ui/Utils.java new file mode 100644 index 0000000..ccb244e --- /dev/null +++ b/src/main/ui/Utils.java @@ -0,0 +1,105 @@ +package ui; + +import model.asn1.exceptions.ParseException; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +/** + * Useful small methods for the whole program. + */ +public final class Utils { + /** + * EFFECTS: Convert the input primitive byte array into the boxed Byte array. + */ + public static Byte[] byteToByte(byte[] array) { + Byte[] arr = new Byte[array.length]; + for (int i = 0; i < arr.length; i++) { + arr[i] = array[i]; + } + return arr; + } + + /** + * EFFECTS: Convert the input boxed Byte array into primitive byte array. + */ + public static byte[] byteToByte(Byte[] array) { + byte[] arr = new byte[array.length]; + for (int i = 0; i < arr.length; i++) { + arr[i] = array[i]; + } + return arr; + } + + /** + * EFFECTS: Pack the big-endian bytes into a 32bit integer. + * Throws {@link model.asn1.exceptions.ParseException} if the value is too large. + */ + public static int bytesToInt(Byte[] array) throws ParseException { + try { + return new BigInteger(byteToByte(array)).intValueExact(); + } catch (ArithmeticException ignored) { + throw new ParseException("Value is too large."); + } + } + + /** + * EFFECTS: Pack the big-endian bytes into a 64bit integer. + * Throws {@link model.asn1.exceptions.ParseException} if the value is too large. + */ + public static long bytesToLong(Byte[] array) throws ParseException { + try { + return new BigInteger(byteToByte(array)).longValueExact(); + } catch (ArithmeticException ignored) { + throw new ParseException("Value is too large."); + } + } + + /** + * EFFECTS: Unpack the multibyte 64bit integer to its shortest array of byte format. + */ + public static Byte[] valToByte(long val) { + byte[] v = BigInteger.valueOf(val).toByteArray(); + if (val != 0 && v[0] == 0) { + if (v.length == 1) { + return new Byte[0]; + } + byte[] arr = new byte[v.length - 1]; + System.arraycopy(v, 1, arr, 0, arr.length); + return byteToByte(arr); + } + return byteToByte(v); + } + + /** + * EFFECTS: Parse the two-digit octet string into an unsigned byte, preserving leading zero and negative values. + * REQUIRES: The input octet must be a two-char string, with each char matching [0-9][A-F]. + */ + public static Byte parseByte(String octet) { + return (byte) Integer.parseInt(octet, 16); + } + + /** + * EFFECTS: Decode the input PEM file, with optional check on tags. + * Throws {@link ParseException} if the desiredTag is specified but the input does not have the specific tag, or + * if the input does not have any tags at all (not a PEM). + */ + public static Byte[] parsePEM(Byte[] input, String desiredTag) throws ParseException { + final String str = new String(byteToByte(input), StandardCharsets.UTF_8); + Pattern pattern = + Pattern.compile("^-----BEGIN " + desiredTag + + "-----$\n^(.*)$\n^-----END " + desiredTag + "-----$", + Pattern.DOTALL | Pattern.MULTILINE); + final Matcher matcher = pattern.matcher(str); + if (!matcher.matches()) { + throw new ParseException("Not a valid PEM"); + } + final String b64 = matcher.group(1).replace("\n", ""); + return byteToByte(Base64.getDecoder().decode(b64)); + } +} diff --git a/src/test/model/MyModelTest.java b/src/test/model/MyModelTest.java deleted file mode 100644 index 91b287a..0000000 --- a/src/test/model/MyModelTest.java +++ /dev/null @@ -1,5 +0,0 @@ -package model; - -class MyModelTest { - // delete or rename this class! -} \ No newline at end of file diff --git a/src/test/model/TestConstants.java b/src/test/model/TestConstants.java new file mode 100644 index 0000000..3356549 --- /dev/null +++ b/src/test/model/TestConstants.java @@ -0,0 +1,544 @@ +package model; + +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.csr.Attribute; +import model.csr.Attributes; +import model.csr.Values; +import model.pki.AlgorithmIdentifier; +import model.pki.SubjectPublicKeyInfo; +import model.pki.cert.Extension; +import model.pki.cert.Extensions; +import model.pki.cert.TbsCertificate; +import model.pki.cert.Validity; +import model.pki.crl.CertificateListContent; +import model.pki.crl.Reason; +import model.pki.crl.RevokedCertificate; +import model.x501.AttributeTypeAndValue; +import model.x501.Name; +import model.x501.RelativeDistinguishedName; +import ui.Utils; + +import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static model.asn1.ASN1Object.TAG_SEQUENCE; +import static model.asn1.ASN1Object.TAG_SET; +import static model.asn1.ObjectIdentifier.OID_EXTENSION_REQUEST; +import static model.asn1.ObjectIdentifier.TAG; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public final class TestConstants { + // An opaque value of extended key usage block. + public static final ASN1Object EXT_KEY_USAGE; + + // Example SubjectAlternativeName request attribute. + public static final ASN1Object SAN; + + public static final Values CSR_ATTR_VALUES_2; + + public static final Attribute CSR_ATTR_2; + + public static final Byte[] CSR_ATTR_VALUES_2_DER = new Byte[]{ + 0x30, 81, // Attribute + // 1.2.840.1113549.1.9.14 + 0x06, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, 0x01, 0x09, 0x0E, + + 0x31, 68, // Values + + 0x30, 0x1F, // SEQUENCE (AttributeValue) + 0x30, 0x1D, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage + 0x04, 0x16, // OCTET STRING + 0x30, 0x14, // SEQUENCE + // 1.3.6.1.5.5.7.3.1 serverAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, + // 1.3.6.1.5.5.7.3.2 clientAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02, + + 0x30, 0x21, // SEQUENCE (AttributeValue) + 0x30, 0x1F, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName + 0x04, 0x18, // OCTET STRING + 0x30, 0x16, // SEQUENCE + -126, 0x14, // [2] + 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe + 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65 + }; + + public static final Attribute CSR_ATTR_1; + + public static final Byte[] CSR_ATTR_1_DER = new Byte[] { + 0x30, 0x1C, + 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0D, 0x02, 0x03, 0x31, 0x0E, 0x16, + 0x0C, 0x31, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x39, 0x30, 0x34, 0x35, 0x2E, 0x32 + }; + + public static final Attributes CSR_ATTRS_2; + + public static final Byte[] L_MILANO_DER = new Byte[]{ + 0x30, 0x0D, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0C, + 0x06, 0x4D, 0x69, 0x6C, 0x61, 0x6E, 0x6F + }; + public static final RelativeDistinguishedName L_MILANO; + public static final Byte[] CN_TEST_ED25519_DER = new Byte[] { + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, + 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64, + 0x32, 0x35, 0x35, 0x31, 0x39, + }; + public static final RelativeDistinguishedName L_MILANO_CN_TEST_ED25519; + + // CN = yuuta + public static final RelativeDistinguishedName CN_YUUTA; + + // OU = users + public static final RelativeDistinguishedName OU_USERS; + + // C = CA + public static final RelativeDistinguishedName C_CA; + + // CN = yuuta, OU = users, C = CA + public static final Name NAME_1; + + // SN = Qwq + public static final RelativeDistinguishedName SN_QWQ; + + // O = IT + public static final RelativeDistinguishedName O_IT; + + // C = CN + public static final RelativeDistinguishedName C_CN; + + // SN = Qwq, O = IT, C = CN + public static final Name NAME_2; + + /** + * Certificate: + * Data: + * Version: 3 (0x2) + * Serial Number: + * 70:fa:0f:fa:a6:d7:f4:b4:93:05:5d:a9:d3:e4:42:a8:52:60:b3:f8 + * Signature Algorithm: ecdsa-with-SHA256 + * Issuer: CN = Yuuta Root CA, C = CA + * Validity + * Not Before: Jun 23 02:50:46 2023 GMT + * Not After : Jun 23 02:50:46 2048 GMT + * Subject: CN = Yuuta Root CA, C = CA + * Subject Public Key Info: + * Public Key Algorithm: id-ecPublicKey + * Public-Key: (256 bit) + * pub: + * ASN1 OID: prime256v1 + * NIST CURVE: P-256 + * X509v3 extensions: + * X509v3 Subject Key Identifier: + * 78:92:E0:6C:70:F5:A3:BE:02:EE:44:BA:A7:8C:DA:D6:B5:43:A7:93 + * X509v3 Authority Key Identifier: + * 78:92:E0:6C:70:F5:A3:BE:02:EE:44:BA:A7:8C:DA:D6:B5:43:A7:93 + * X509v3 Basic Constraints: critical + * CA:TRUE + * X509v3 Key Usage: critical + * Digital Signature, Certificate Sign, CRL Sign + * Signature Algorithm: ecdsa-with-SHA256 + * Signature Value: + */ + public static final String CERT_L1_ECC_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIBrzCCAVWgAwIBAgIUcPoP+qbX9LSTBV2p0+RCqFJgs/gwCgYIKoZIzj0EAwIw\n" + + "JTEWMBQGA1UEAwwNWXV1dGEgUm9vdCBDQTELMAkGA1UEBhMCQ0EwHhcNMjMwNjIz\n" + + "MDI1MDQ2WhcNNDgwNjIzMDI1MDQ2WjAlMRYwFAYDVQQDDA1ZdXV0YSBSb290IENB\n" + + "MQswCQYDVQQGEwJDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABB3ocbzdSHAm\n" + + "cWzdBFs/Xd4UMYs/MYAYKjPlGYYT1udIL5UVOlmN7QnkUxrzYbI1YW5mX1/PCuJl\n" + + "ZT0iKzBxLCSjYzBhMB0GA1UdDgQWBBR4kuBscPWjvgLuRLqnjNrWtUOnkzAfBgNV\n" + + "HSMEGDAWgBR4kuBscPWjvgLuRLqnjNrWtUOnkzAPBgNVHRMBAf8EBTADAQH/MA4G\n" + + "A1UdDwEB/wQEAwIBhjAKBggqhkjOPQQDAgNIADBFAiAJr6S6xkgxMitFmnS/0cIu\n" + + "E7W9Ykii5d2Fe9+Lu4nL9wIhAIwHnAvZ4YzwfY6P5K4SaBwzzKPiq2zdpaXBm0lX\n" + + "qcsz\n" + + "-----END CERTIFICATE-----"; + /** + * Certificate: + * Data: + * Version: 3 (0x2) + * Serial Number: + * 3e:10:93:9d:e4:57:8d:39:87:fd:ff:42:7b:da:65:5b:1f:21:cb:07 + * Signature Algorithm: ecdsa-with-SHA512 + * Issuer: CN = Yuuta Root CA, C = CA + * Validity + * Not Before: Jun 24 00:15:22 2023 GMT + * Not After : Jun 21 00:15:22 2033 GMT + * Subject: DC = MOE, DC = YUUTA, DC = AD, CN = Yuuta Home Issuing CA + * Subject Public Key Info: + * Public Key Algorithm: rsaEncryption + * Public-Key: (4096 bit) + * Modulus: + * Exponent: 65537 (0x10001) + * X509v3 extensions: + * X509v3 Subject Key Identifier: + * B1:C2:A7:81:63:66:4B:72:0A:DD:FD:7D:20:29:BD:6B:49:09:61:C0 + * X509v3 Authority Key Identifier: + * 78:92:E0:6C:70:F5:A3:BE:02:EE:44:BA:A7:8C:DA:D6:B5:43:A7:93 + * X509v3 Basic Constraints: critical + * CA:TRUE, pathlen:0 + * X509v3 Key Usage: critical + * Digital Signature, Certificate Sign, CRL Sign + * X509v3 CRL Distribution Points: + * Full Name: + * URI:http://home.yuuta.moe/pki/rootca.crl + * Authority Information Access: + * CA Issuers - URI:http://home.yuuta.moe/pki/rootca.crt + * Signature Algorithm: ecdsa-with-SHA512 + * Signature Value: + */ + public static final String CERT_L2_RSA_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIEMjCCA9mgAwIBAgIUPhCTneRXjTmH/f9Ce9plWx8hywcwCgYIKoZIzj0EAwQw\n" + + "JTEWMBQGA1UEAwwNWXV1dGEgUm9vdCBDQTELMAkGA1UEBhMCQ0EwHhcNMjMwNjI0\n" + + "MDAxNTIyWhcNMzMwNjIxMDAxNTIyWjBgMRMwEQYKCZImiZPyLGQBGRYDTU9FMRUw\n" + + "EwYKCZImiZPyLGQBGRYFWVVVVEExEjAQBgoJkiaJk/IsZAEZFgJBRDEeMBwGA1UE\n" + + "AwwVWXV1dGEgSG9tZSBJc3N1aW5nIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A\n" + + "MIICCgKCAgEA2WTLz4X8B2XN/AiDJNsQQNMiBDjye/TfpbC3dLsUZr4BneH9EX+I\n" + + "lKkaoJaSIXU7iXoN43FeLX5iuoq+aj4f3x+qevWySD7xLLt8gpozN5MKBdRXtaem\n" + + "4js3Nm3YLUbjv115sddHN/3QTQXgBSUGdjyi8woL54IKdKYzB1g2Jn2Et383usMA\n" + + "yHd3gCbwszvE5jpOgBIHxZMgMnmVAQhbQNzoEDMIkwaXmSt4jwX03oigf0KAaD+a\n" + + "XIwQRl15iIDZnG6rRw6+eiIR8c+x1ot1/u5qncwNhRUtLbbX3QfBQ6D/XBSfrqmA\n" + + "zhddM/i2Qt5Iw44CcLSGujFeb9ybU7NLx02EjfQsSAUGQR4VuXyD+//FsLYkh7g3\n" + + "WmdBTWzIhVnYEU9ohTeXaZZNTp9T67czqnntFbaCdOxnwOrcmFt1v0skrHd5mHKe\n" + + "1W3OU6XOjM6vQwcwhPUUGxAXYBcqwQ84fzD26CZz5g8I8HpnpmJ+SNtFIg+SnPOs\n" + + "sslnsoeMZpDPESwORYgayXIWkglop1fYeD4/ictH4me70vOIHF9fWqI8ydHoNxuw\n" + + "uZjZDa0mQgsHTmr40NhDLP/q6MEnS2w/MwHuSd3YbhbjPWFbu0Zo7XreiRkXjRLa\n" + + "R22XkuH+FkEGB3ZxQVIkkWf1znaKQS+ZdPuTzpZph5BPL50gE58k+i0CAwEAAaOB\n" + + "4DCB3TAdBgNVHQ4EFgQUscKngWNmS3IK3f19ICm9a0kJYcAwHwYDVR0jBBgwFoAU\n" + + "eJLgbHD1o74C7kS6p4za1rVDp5MwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B\n" + + "Af8EBAMCAYYwNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2hvbWUueXV1dGEubW9l\n" + + "L3BraS9yb290Y2EuY3JsMEAGCCsGAQUFBwEBBDQwMjAwBggrBgEFBQcwAoYkaHR0\n" + + "cDovL2hvbWUueXV1dGEubW9lL3BraS9yb290Y2EuY3J0MAoGCCqGSM49BAMEA0cA\n" + + "MEQCIHShp7SwbQ2rQC7l8u4u9rSU6Zl4DRfyor4jiHGAjm0gAiAbOHk6q+3Vm3uq\n" + + "Jj92o1yDl09pFNIryojHMDRBpHl6yQ==\n" + + "-----END CERTIFICATE-----"; + + /** + * Certificate: + * Data: + * Version: 1 (0x0) + * Serial Number: 3580 (0xdfc) + * Signature Algorithm: sha1WithRSAEncryption + * Issuer: C = JP, ST = Tokyo, L = Chuo-ku, O = Frank4DD, OU = WebCert Support, CN = Frank4DD Web CA, emailAddress = support@frank4dd.com + * Validity + * Not Before: Aug 22 05:27:41 2012 GMT + * Not After : Aug 21 05:27:41 2017 GMT + * Subject: C = JP, ST = Tokyo, O = Frank4DD, CN = www.example.com + * Subject Public Key Info: + * Public Key Algorithm: rsaEncryption + * Public-Key: (2048 bit) + * Modulus: + * Exponent: 65537 (0x10001) + * Signature Algorithm: sha1WithRSAEncryption + * Signature Value: + */ + public static final String CERT_V1_PEM = + "-----BEGIN CERTIFICATE-----\n" + + "MIIC2jCCAkMCAg38MA0GCSqGSIb3DQEBBQUAMIGbMQswCQYDVQQGEwJKUDEOMAwG\n" + + "A1UECBMFVG9reW8xEDAOBgNVBAcTB0NodW8ta3UxETAPBgNVBAoTCEZyYW5rNERE\n" + + "MRgwFgYDVQQLEw9XZWJDZXJ0IFN1cHBvcnQxGDAWBgNVBAMTD0ZyYW5rNEREIFdl\n" + + "YiBDQTEjMCEGCSqGSIb3DQEJARYUc3VwcG9ydEBmcmFuazRkZC5jb20wHhcNMTIw\n" + + "ODIyMDUyNzQxWhcNMTcwODIxMDUyNzQxWjBKMQswCQYDVQQGEwJKUDEOMAwGA1UE\n" + + "CAwFVG9reW8xETAPBgNVBAoMCEZyYW5rNEREMRgwFgYDVQQDDA93d3cuZXhhbXBs\n" + + "ZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0z9FeMynsC8+u\n" + + "dvX+LciZxnh5uRj4C9S6tNeeAlIGCfQYk0zUcNFCoCkTknNQd/YEiawDLNbxBqut\n" + + "bMDZ1aarys1a0lYmUeVLCIqvzBkPJTSQsCopQQ9V8WuT252zzNzs68dVGNdCJd5J\n" + + "NRQykpwexmnjPPv0mvj7i8XgG379TyW6P+WWV5okeUkXJ9eJS2ouDYdR2SM9BoVW\n" + + "+FgxDu6BmXhozW5EfsnajFp7HL8kQClI0QOc79yuKl3492rH6bzFsFn2lfwWy9ic\n" + + "7cP8EpCTeFp1tFaD+vxBhPZkeTQ1HKx6hQ5zeHIB5ySJJZ7af2W8r4eTGYzbdRW2\n" + + "4DDHCPhZAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAQMv+BFvGdMVzkQaQ3/+2noVz\n" + + "/uAKbzpEL8xTcxYyP3lkOeh4FoxiSWqy5pGFALdPONoDuYFpLhjJSZaEwuvjI/Tr\n" + + "rGhLV1pRG9frwDFshqD2Vaj4ENBCBh6UpeBop5+285zQ4SI7q4U9oSebUDJiuOx6\n" + + "+tZ9KynmrbJpTSi0+BM=\n" + + "-----END CERTIFICATE-----"; + + public static final Byte[] CERT_L1_ECC; + public static final Byte[] CERT_L2_RSA; + public static final Byte[] CERT_V1; + + public static final ZonedDateTime NOW = ZonedDateTime.now(ZoneId.of("UTC")); + public static final TbsCertificate CERT_GENERATED; + + public static final RevokedCertificate REVOKED_CESSATION; + public static final Byte[] REVOKED_CESSATION_DER; + public static final RevokedCertificate REVOKED_KEY_COMPROMISE; + public static final Byte[] REVOKED_KEY_COMPROMISE_DER; + public static final CertificateListContent CRL_CONTENT_1; + public static final Byte[] CRL_CONTENT_1_DER; + public static final CertificateListContent CRL_CONTENT_2; + public static final Byte[] CRL_CONTENT_2_DER; + + static { + try { + EXT_KEY_USAGE = ASN1Object.parse(new BytesReader(new Byte[]{ + 0x30, 0x1F, // SEQUENCE (AttributeValue) + 0x30, 0x1D, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage + 0x04, 0x16, // OCTET STRING + 0x30, 0x14, // SEQUENCE + // 1.3.6.1.5.5.7.3.1 serverAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, + // 1.3.6.1.5.5.7.3.2 clientAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02 + }), false); + + SAN = ASN1Object.parse(new BytesReader(new Byte[]{ + 0x30, 0x21, // SEQUENCE (AttributeValue) + 0x30, 0x1F, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName + 0x04, 0x18, // OCTET STRING + 0x30, 0x16, // SEQUENCE + -126, 0x14, // [2] + 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe + 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65 + }), false); + + CSR_ATTR_VALUES_2 = new Values(TAG_SET, null, new ASN1Object[]{ + EXT_KEY_USAGE, + SAN + }); + + CSR_ATTR_2 = new Attribute( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_EXTENSION_REQUEST), + CSR_ATTR_VALUES_2); + + CSR_ATTR_1 = new Attribute( + TAG_SEQUENCE, null, + new ObjectIdentifier(TAG, null, new Integer[]{ 1, 3, 6, 1, 4, 1, 311, 13, 2, 3 }), + new Values(TAG_SET, null, new ASN1Object[]{ + new IA5String(IA5String.TAG, null, "10.0.19045.2") + }) + ); + + CSR_ATTRS_2 = new Attributes(TAG_SET, null, new Attribute[]{ + CSR_ATTR_2, CSR_ATTR_1 + }); + + // L = Milano + L_MILANO = new RelativeDistinguishedName(TAG_SET, null, + new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_L), + new PrintableString(PrintableString.TAG, null, "Milano")) + }); + // CN = Test ed25519+L = Milano + L_MILANO_CN_TEST_ED25519 = new RelativeDistinguishedName(TAG_SET, null, + new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_CN), + new UTF8String(UTF8String.TAG, null, "Test ed25519")), + new AttributeTypeAndValue(TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_L), + new UTF8String(UTF8String.TAG, null, "Milano")) + }); + + CN_YUUTA = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_CN), + new PrintableString(PrintableString.TAG, null, "yuuta")) + }); + OU_USERS = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_OU), + new PrintableString(PrintableString.TAG, null, "users")) + }); + C_CA = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_C), + new PrintableString(PrintableString.TAG, null, "CA")) + }); + NAME_1 = new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{ + CN_YUUTA, OU_USERS, C_CA + }); + + SN_QWQ = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SN), + new PrintableString(PrintableString.TAG, null, "Qwq")) + }); + O_IT = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_O), + new PrintableString(PrintableString.TAG, null, "IT")) + }); + C_CN = new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_C), + new PrintableString(PrintableString.TAG, null, "CN")) + }); + NAME_2 = new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{ + SN_QWQ, O_IT, C_CN + }); + + CERT_L1_ECC = Utils.parsePEM(Utils.byteToByte(CERT_L1_ECC_PEM.getBytes(StandardCharsets.UTF_8)), + "CERTIFICATE"); + CERT_L2_RSA = Utils.parsePEM(Utils.byteToByte(CERT_L2_RSA_PEM.getBytes(StandardCharsets.UTF_8)), + "CERTIFICATE"); + CERT_V1 = Utils.parsePEM(Utils.byteToByte(CERT_V1_PEM.getBytes(StandardCharsets.UTF_8)), + "CERTIFICATE"); + + CERT_GENERATED = new TbsCertificate(ASN1Object.TAG_SEQUENCE, null, + new Int(Int.TAG, new Tag(TagClass.CONTEXT_SPECIFIC, false, 0), TbsCertificate.VERSION_V3), + new Int(Int.TAG, null, 100), + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{ + new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_CN), + new PrintableString(PrintableString.TAG, null, "Test CA")) + }), + new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_C), + new PrintableString(PrintableString.TAG, null, "CA")) + }) + }), + new Validity(ASN1Object.TAG_SEQUENCE, null, + new UtcTime(UtcTime.TAG, null, NOW), + new GeneralizedTime(GeneralizedTime.TAG, null, NOW.plusYears(1))), + new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{ + new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_CN), + new PrintableString(PrintableString.TAG, null, "Yuuta Liang")) + }), + new RelativeDistinguishedName(ASN1Object.TAG_SET, null, new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_C), + new PrintableString(PrintableString.TAG, null, "CA")) + }) + }), + new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null, + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_EC_PUBLIC_KEY), + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_PRIME256_V1)), + new BitString(BitString.TAG, null, 0, new Byte[]{ 1, 2, 3 })), + new Extensions(ASN1Object.TAG_SEQUENCE, + new Tag(TagClass.CONTEXT_SPECIFIC, false, 3), + new Extension[]{ + new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_BASIC_CONSTRAINTS), + new Bool(Bool.TAG, null, true), + new OctetString(OctetString.TAG, null, + new Byte[]{ 0x30, 0x06, 0x01, 0x01, -1, 0x02, 0x01, 0x00 })), + new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_KEY_USAGE), + new Bool(Bool.TAG, null, true), + new OctetString(OctetString.TAG, null, + new Byte[]{ 0x03, 0x02, 0x01, -122 })) + })); + + REVOKED_CESSATION = + new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null, + new Int(Int.TAG, null, 123), + new UtcTime(UtcTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))), + Reason.CESSATION_OF_OPERATION); + REVOKED_KEY_COMPROMISE = + new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null, + new Int(Int.TAG, null, 2), + new UtcTime(UtcTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))), + Reason.KEY_COMPROMISE); + REVOKED_CESSATION_DER = combine((byte) 0x30, + REVOKED_CESSATION.getSerialNumber().encodeDER(), + REVOKED_CESSATION.getRevocationDate().encodeDER(), + combine((byte) 0x30, + combine((byte) 0x30, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_CRL_REASON).encodeDER(), + new OctetString(OctetString.TAG, null, + new Byte[]{ 0x0A, 0x01, (byte) Reason.CESSATION_OF_OPERATION.getVal() }) + .encodeDER()))); + REVOKED_KEY_COMPROMISE_DER = combine((byte) 0x30, + REVOKED_KEY_COMPROMISE.getSerialNumber().encodeDER(), + REVOKED_KEY_COMPROMISE.getRevocationDate().encodeDER(), + combine((byte) 0x30, + combine((byte) 0x30, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_CRL_REASON).encodeDER(), + new OctetString(OctetString.TAG, null, + new Byte[]{ 0x0A, 0x01, (byte) Reason.KEY_COMPROMISE.getVal() }) + .encodeDER()))); + CRL_CONTENT_1 = + new CertificateListContent(ASN1Object.TAG_SEQUENCE, null, + new Name(ASN1Object.TAG_SEQUENCE, null, + new RelativeDistinguishedName[]{ + new RelativeDistinguishedName(ASN1Object.TAG_SET, null, + new AttributeTypeAndValue[]{ + new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_CN), + new PrintableString(PrintableString.TAG, null, + "Test CA")) + }) + }), + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new GeneralizedTime(GeneralizedTime.TAG, null, + ZonedDateTime.now(ZoneId.of("UTC"))), + null, + new RevokedCertificate[]{ + TestConstants.REVOKED_CESSATION, + TestConstants.REVOKED_KEY_COMPROMISE + }); + CRL_CONTENT_1_DER = combine((byte) 0x30, CRL_CONTENT_1.getVersion().encodeDER(), + CRL_CONTENT_1.getIssuer().encodeDER(), + CRL_CONTENT_1.getSignature().encodeDER(), + CRL_CONTENT_1.getThisUpdate().encodeDER(), + combine((byte) 0x30, REVOKED_CESSATION_DER, REVOKED_KEY_COMPROMISE_DER)); + CRL_CONTENT_2 = new CertificateListContent(CRL_CONTENT_1.getTag(), CRL_CONTENT_1.getParentTag(), + CRL_CONTENT_1.getIssuer(), + CRL_CONTENT_1.getSignature(), + CRL_CONTENT_1.getThisUpdate(), + CRL_CONTENT_1.getThisUpdate(), + CRL_CONTENT_1.getRevokedCertificates()); + CRL_CONTENT_2_DER = combine((byte) 0x30, CRL_CONTENT_2.getVersion().encodeDER(), + CRL_CONTENT_2.getIssuer().encodeDER(), + CRL_CONTENT_2.getSignature().encodeDER(), + CRL_CONTENT_2.getThisUpdate().encodeDER(), + CRL_CONTENT_2.getNextUpdate().encodeDER(), + combine((byte) 0x30, REVOKED_CESSATION_DER, REVOKED_KEY_COMPROMISE_DER)); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + public static Byte[] mutate(Byte[] in, int i, int from, int to) { + Byte[] b = new Byte[in.length]; + System.arraycopy(in, 0, b, 0, in.length); + assertEquals(from, (int) in[i]); + b[i] = (byte) to; + return b; + } + + public static Byte[] combine(Byte tag, Byte[]... vals) { + return Stream.of(Collections.singletonList(tag), + Arrays.asList(new ASN1Length(Arrays.stream(vals).mapToInt(b -> b.length).sum()).encodeDER()), + Arrays.stream(vals).flatMap(Arrays::stream).collect(Collectors.toList())) + .flatMap(Collection::stream) + .toArray(Byte[]::new); + } +} diff --git a/src/test/model/asn1/ASN1LengthTest.java b/src/test/model/asn1/ASN1LengthTest.java new file mode 100644 index 0000000..44aed8e --- /dev/null +++ b/src/test/model/asn1/ASN1LengthTest.java @@ -0,0 +1,94 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.asn1.parsing.BytesReaderTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ASN1LengthTest { + @Test + void testConstructor() { + assertEquals(0, new ASN1Length(0).getLength()); + assertEquals(1, new ASN1Length(1).getLength()); + } + + @Test + void testParse() throws Exception { // TODO: Exception + BytesReader reader = new BytesReader(new Byte[]{ 0x0 }); + assertEquals(0, new ASN1Length(reader).getLength()); + assertEquals(1, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ 0x20 }); + assertEquals(0x20, new ASN1Length(reader).getLength()); + assertEquals(1, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -127, 0x30 }); + assertEquals(0x30, new ASN1Length(reader).getLength()); + assertEquals(2, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -126, -22, 0x60 }); + assertEquals(60000, new ASN1Length(reader).getLength()); + assertEquals(3, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -127, 127 }); + assertEquals(127, new ASN1Length(reader).getLength()); + assertEquals(2, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -124, 1, 1, 1, 1 }); + assertEquals(16843009, new ASN1Length(reader).getLength()); + assertEquals(5, reader.getIndex()); + + reader = new BytesReader(new Byte[]{ -127, -97 }); + assertEquals(159, new ASN1Length(reader).getLength()); + assertEquals(2, reader.getIndex()); + } + + @Test + void testParseFail() throws ParseException { + // First byte 0b10000000 + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -128 })) + ); + + // First byte 0b11111111 + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -1 })) + ); + + // Multibyte, requested 2 bytes but have none. + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -126 })) + ); + + // Multibyte, requested 2 bytes but have one. + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -126, 0x1 })) + ); + + // But this one should work (0b01111111) + new ASN1Length(new BytesReader(new Byte[]{ -127, 127 })); + + // Multibyte, too long. + assertThrows(ParseException.class, () -> + new ASN1Length(new BytesReader(new Byte[]{ -124, -1, -1, -1, -1 })) + ); + // But this one should work, except for it is too large + new ASN1Length(new BytesReader(new Byte[]{ -125, -1, -1, -1 })); + } + + @Test + void testEncode() { + // Short form + assertArrayEquals(new Byte[]{ 0x0 }, new ASN1Length(0).encodeDER()); + assertArrayEquals(new Byte[]{ 0x1 }, new ASN1Length(1).encodeDER()); + assertArrayEquals(new Byte[]{ 127 }, new ASN1Length(127).encodeDER()); + + // Long form + // 0b10000001, 0b10000000 + assertArrayEquals(new Byte[]{ -127, -128 }, new ASN1Length(128).encodeDER()); + // 0b10000010, 0b11111111, 0b11111111 + assertArrayEquals(new Byte[]{ -126, -1, -1 }, new ASN1Length(65535).encodeDER()); + } +} diff --git a/src/test/model/asn1/ASN1ObjectTest.java b/src/test/model/asn1/ASN1ObjectTest.java new file mode 100644 index 0000000..ea765e6 --- /dev/null +++ b/src/test/model/asn1/ASN1ObjectTest.java @@ -0,0 +1,177 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public class ASN1ObjectTest { + @Test + void testParseType() throws ParseException { + assertEquals(Bool.class, + ASN1Object.parse(new BytesReader(new Bool(Bool.TAG, null, true).encodeDER()), + false).getClass()); + assertEquals(Int.class, + ASN1Object.parse(new BytesReader(new Int(Int.TAG, null, 1).encodeDER()), + false).getClass()); + assertEquals(BitString.class, + ASN1Object.parse(new BytesReader(new BitString(BitString.TAG, + null, 0, new Byte[]{ 1 }).encodeDER()), + false).getClass()); + assertEquals(OctetString.class, + ASN1Object.parse(new BytesReader(new OctetString(OctetString.TAG, null, new Byte[]{ 1 }).encodeDER()), + false).getClass()); + assertEquals(Null.class, + ASN1Object.parse(new BytesReader(new Null(Null.TAG, null).encodeDER()), + false).getClass()); + assertEquals(ObjectIdentifier.class, + ASN1Object.parse(new BytesReader(new ObjectIdentifier(ObjectIdentifier.TAG, null, + new Integer[]{ 1, 2, 3 }).encodeDER()), + false).getClass()); + assertEquals(UTF8String.class, + ASN1Object.parse(new BytesReader(new UTF8String(UTF8String.TAG, null, + "qwq").encodeDER()), + false).getClass()); + assertEquals(PrintableString.class, + ASN1Object.parse(new BytesReader(new PrintableString(PrintableString.TAG, null, + "qwq").encodeDER()), + false).getClass()); + assertEquals(IA5String.class, + ASN1Object.parse(new BytesReader(new IA5String(IA5String.TAG, null, + "qwq").encodeDER()), + false).getClass()); + assertEquals(UtcTime.class, + ASN1Object.parse(new BytesReader(new UtcTime(UtcTime.TAG, null, + ZonedDateTime.now(ZoneId.of("UTC"))).encodeDER()), + false).getClass()); + assertEquals(GeneralizedTime.class, + ASN1Object.parse(new BytesReader(new GeneralizedTime(GeneralizedTime.TAG, null, + ZonedDateTime.now(ZoneId.of("UTC"))).encodeDER()), + false).getClass()); + assertEquals(ASN1Object.class, + ASN1Object.parse(new BytesReader(new Byte[]{ 0x30, 1, 0x0 }), false) + .getClass()); + } + + @Test + void testConstructor() { + assertEquals(0, new ASN1Object(Null.TAG, null).getLength()); + assertNull(new ASN1Object(Null.TAG, null).encodeValueDER()); + assertEquals(0x5, + new ASN1Object(new Tag(TagClass.UNIVERSAL, false, 0x5), + null).getTag().getNumber()); + assertEquals(0x6, + new ASN1Object(new Tag(TagClass.UNIVERSAL, false, 0x5), + new Tag(TagClass.UNIVERSAL, false, 0x6)).getParentTag().getNumber()); + } + @Test + void testParseSuccess() throws ParseException { + // No parent tag + assertEquals(0x5, + new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getTag().getNumber()); + assertEquals(TagClass.UNIVERSAL, + new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getTag().getCls()); + assertFalse(new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getTag().isConstructive()); + assertNull(new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getParentTag()); + + assertEquals(0, new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .encodeValueDER().length); + assertEquals(0, new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x0 }), false) + .getLength()); + + // With parent tag + // -95 is the 2's complement represent of 0b10100001 + assertEquals(0x5, + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getTag().getNumber()); + assertEquals(TagClass.UNIVERSAL, + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getTag().getCls()); + assertFalse(new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getTag().isConstructive()); + assertEquals(0x1, + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getParentTag().getNumber()); + assertEquals(TagClass.CONTEXT_SPECIFIC, + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getParentTag().getCls()); + assertTrue(new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true) + .getParentTag().isConstructive()); + + // Test index + BytesReader reader = new BytesReader(new Byte[]{ 0xE, 5, 1, 2, 3, 4, 5 }); + ASN1Object obj = new ASN1Object(reader, false); + // Contents should not be read. + assertEquals(2, reader.getIndex()); + // But is copied + assertArrayEquals(new Byte[]{ 1, 2, 3, 4, 5 }, obj.encodeValueDER()); + // If we parse an unknown type + reader = new BytesReader(new Byte[]{ 0xE, 5, 1, 2, 3, 4, 5 }); + obj = ASN1Object.parse(reader, false); + // Contents should be read now + assertEquals(7, reader.getIndex()); + } + + @Test + void testParseFail() { + // Value early EOF + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ 0x5, 0x1 }), false)); + // Tag early EOF + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 2 }), true)); + // Length not found + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ 0x5 }), false)); + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 2, 0x5 }), true)); + // Parent tag is not CONTEXT_SPECIFIC + // UNIVERSAL + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ 33, 2, 0x5, 0x0 }), true)); + // APPLICATION + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ 97, 2, 0x5, 0x0 }), true)); + // PRIVATE + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -31, 2, 0x5, 0x0 }), true)); + // Parent tag is not constructive + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -127, 2, 0x5, 0x0 }), true)); + // Parent tag length incorrect + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 0, 0x5, 0x0 }), true)); + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 1, 0x5, 0x0 }), true)); + assertThrows(ParseException.class, () -> + new ASN1Object(new BytesReader(new Byte[]{ -95, 3, 0x5, 0x0 }), true)); + } + + @Test + void testEncode() { + // No parent tag + assertArrayEquals(new Byte[] { + 0x5, 0x0 + }, new Null(Null.TAG, null).encodeDER()); + // Custom tag + assertArrayEquals(new Byte[] { + 0x72, 0x0 + }, new Null(new Tag(TagClass.APPLICATION, true, 0x12), null) + .encodeDER()); + // With parent tag + assertArrayEquals(new Byte[] { + -95, 2, + 0x72, 0x0 + }, new Null(new Tag(TagClass.APPLICATION, true, 0x12), + new Tag(TagClass.CONTEXT_SPECIFIC, true, 0x1)) + .encodeDER()); + } +} diff --git a/src/test/model/asn1/BitStringTest.java b/src/test/model/asn1/BitStringTest.java new file mode 100644 index 0000000..c893b36 --- /dev/null +++ b/src/test/model/asn1/BitStringTest.java @@ -0,0 +1,75 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class BitStringTest { + @Test + void testConstructor() { + assertArrayEquals(new Byte[]{ 0x2, 0x3 }, + new BitString(BitString.TAG, null, 0, new Byte[]{ 0x2, 0x3 }) + .getVal()); + assertEquals(3, + new BitString(BitString.TAG, null, 3, new Byte[]{ 0x2, 0x8 }) + .getUnused()); + } + + @Test + void testConvert() { + // 00000010 00001000 + // 00000000 01000001 = 65 + assertArrayEquals(new Byte[]{ 65 }, + new BitString(BitString.TAG, null, 3, new Byte[]{ 0x2, 0x8 }) + .getConvertedVal()); + + assertArrayEquals(new Byte[]{ 0x2, 0x8 }, + new BitString(BitString.TAG, null, 0, new Byte[]{ 0x2, 0x8 }) + .getConvertedVal()); + } + + @Test + void testParse() throws ParseException { + assertArrayEquals(new Byte[]{ 0x6e, 0x5d, -64 }, + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }), false) + .getVal()); + assertEquals(6, + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }), false) + .getUnused()); + assertArrayEquals(new Byte[]{ 0x01, -71, 0x77 }, + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }), false) + .getConvertedVal()); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04 }), false)); + // 0b11100000 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -32 }), false)); + // 0b11000001 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -63 }), false)); + // Unused bits = 8 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x08, 0x6e, 0x5d, -64 }), false)); + // Unused bits = 6 -> 7 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x07, 0x6e, 0x5d, -64 }), false)); + // Illegal unused bits: 8 and -1 + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x08, 0x6e, 0x5d, -64 }), false)); + assertThrows(ParseException.class, () -> + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, -1, 0x6e, 0x5d, -64 }), false)); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }, + new BitString(new BytesReader(new Byte[]{ 0x03, 0x04, 0x06, 0x6e, 0x5d, -64 }), false) + .encodeDER()); + } +} diff --git a/src/test/model/asn1/BoolTest.java b/src/test/model/asn1/BoolTest.java new file mode 100644 index 0000000..fed3152 --- /dev/null +++ b/src/test/model/asn1/BoolTest.java @@ -0,0 +1,45 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class BoolTest { + @Test + void testConstructor() { + assertTrue(new Bool(Bool.TAG, null, true).getValue()); + assertFalse(new Bool(Bool.TAG, null, false).getValue()); + } + + @Test + void testParse() throws ParseException { + assertFalse(new Bool(new BytesReader(new Byte[]{ 0x1, 1, 0 }), false) + .getValue()); + assertTrue(new Bool(new BytesReader(new Byte[]{ 0x1, 1, -1 }), false) + .getValue()); + } + + @Test + void testParseFail() throws ParseException { + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 0 }), false)); + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 1 }), false)); + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 1, 1 }), false)); + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 1, -2 }), false)); + assertThrows(ParseException.class, () -> + new Bool(new BytesReader(new Byte[]{ 0x1, 2, -1, 2 }), false)); + } + + @Test + void testEncode() { + assertArrayEquals(new Byte[]{ 0 }, + new Bool(Bool.TAG, null, false).encodeValueDER()); + assertArrayEquals(new Byte[]{ -1 }, + new Bool(Bool.TAG, null, true).encodeValueDER()); + } +} diff --git a/src/test/model/asn1/GeneralizedTimeTest.java b/src/test/model/asn1/GeneralizedTimeTest.java new file mode 100644 index 0000000..4660de7 --- /dev/null +++ b/src/test/model/asn1/GeneralizedTimeTest.java @@ -0,0 +1,119 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public class GeneralizedTimeTest { + @Test + void testConstructor() throws ParseException { + final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); + assertEquals(now, new GeneralizedTime(GeneralizedTime.TAG, null, now).getTimestamp()); + final ASN1Time parsed = new GeneralizedTime(new BytesReader(new Byte[] { + 0x18, 15, + '1', '9', '1', '9', '0', '8', '1', '0', '1', '1', '4', '5', '1', '4', 'Z' + }), false); + assertEquals("19190810114514Z", + parsed.toString()); + assertEquals(ZonedDateTime.of(1919, 8, 10, 11, 45, 14, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + } + + @Test + void testParse() throws ParseException { + ASN1Time parsed = new GeneralizedTime(new BytesReader(new Byte[] { + 0x18, 15, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), false); + assertEquals(ZonedDateTime.of(2023, 9, 27, 11, 45, 14, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + + // No seconds + parsed = new GeneralizedTime(new BytesReader(new Byte[] { + 0x18, 13, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }), false); + assertEquals(ZonedDateTime.of(2023, 9, 27, 11, 45, 0, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + + // Length 0 + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 0 + }), false)); + // Early EOF + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15 + }), false)); + // No tailing Z + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 14, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4' + }), false)); + // Custom timezone + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 18, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z', '-', '0', '8' + }), false)); + // Invalid month / day / hour / minute / second + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '3', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '0', '3', '2', '1', '1', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '0', '3', '0', '2', '5', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '0', '3', '0', '2', '4', '6', '1', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new GeneralizedTime(new BytesReader(new Byte[]{ + 0x18, 15, + '2', '0', '2', '3', '1', '0', '3', '0', '2', '4', '6', '0', '6', '1', 'Z' + }), false)); + } + + @Test + void testEncode() throws ParseException { + assertEquals("20230927114514Z", new GeneralizedTime(new BytesReader(new Byte[] { + -95, 17, + 0x18, 15, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), true).toString()); + // No seconds + assertEquals("202309271145Z", new GeneralizedTime(new BytesReader(new Byte[] { + -95, 15, + 0x18, 13, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }), true).toString()); + + // To byte array + assertArrayEquals(new Byte[] { + 0x18, 13, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }, new GeneralizedTime(GeneralizedTime.TAG, null, ZonedDateTime.of(2023, 9, + 27, 11, 45, 0, 0, ZoneId.of("UTC"))) + .encodeDER()); + } +} diff --git a/src/test/model/asn1/IA5StringTest.java b/src/test/model/asn1/IA5StringTest.java new file mode 100644 index 0000000..dfaa1aa --- /dev/null +++ b/src/test/model/asn1/IA5StringTest.java @@ -0,0 +1,79 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class IA5StringTest { + private String stringToTest = ""; + + @BeforeEach + void setup() { + stringToTest = IntStream.range(0, 127) + .mapToObj(Character::toString) + .collect(Collectors.joining()); + } + + @Test + void testAcceptedString() throws ParseException { + assertEquals(stringToTest, + new IA5String(IA5String.TAG, null, stringToTest) + .getString()); + } + + @Test + void testIllegalStrings() { + assertThrows(ParseException.class, + () -> new IA5String(IA5String.TAG, null, + stringToTest + new String(new byte[]{ -128 }, StandardCharsets.UTF_8))); + assertThrows(ParseException.class, + () -> new IA5String(IA5String.TAG, null, + stringToTest + new String(new byte[]{ -1 }, StandardCharsets.UTF_8))); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals( + new Byte[] { 0x00, 0x01, 0x02 }, + new IA5String(IA5String.TAG, null, new String(new byte[]{ 0, 1, 2}, StandardCharsets.UTF_8)) + .encodeValueDER()); + assertArrayEquals( + new Byte[] { + 0x16, 0x02, // Tag - Length + 0x68, 0x69 // Value + }, new IA5String(IA5String.TAG, null, "hi").encodeDER()); + assertArrayEquals( + new Byte[] { + -85, 0x05, // Parent Tag - Length + 0x16, 0x03, // Inner Tag - Length + 0x68, 0x69, 0x69 // Value + }, new IA5String(IA5String.TAG, + new Tag(TagClass.CONTEXT_SPECIFIC, true, 11), + "hii").encodeDER()); + } + + @Test + void testParse() throws ParseException { + assertEquals("123", + new IA5String(new BytesReader(new Byte[]{ 0x16, 3, '1', '2', '3' }), false) + .getString()); + assertEquals("", + new IA5String(new BytesReader(new Byte[]{ 0x16, 0 }), false) + .getString()); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> + new IA5String(new BytesReader(new Byte[]{ 0x16, 3, '1', '2' }), false)); + assertThrows(ParseException.class, () -> + new IA5String(new BytesReader(new Byte[]{ 0x16, 2, '1', -128 }), false)); + } +} diff --git a/src/test/model/asn1/IntTest.java b/src/test/model/asn1/IntTest.java new file mode 100644 index 0000000..6e92eb0 --- /dev/null +++ b/src/test/model/asn1/IntTest.java @@ -0,0 +1,96 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class IntTest { + @Test + void testConstructor() { + assertEquals(0x2, new Int(Int.TAG, null, 255).getTag().getNumber()); + assertEquals(255, new Int(Int.TAG, null, 255).getLong()); + } + + @Test + void testEncode() { + // Single-byte + assertArrayEquals(new Byte[] { 0x0 }, new Int(Int.TAG, null, 0).encodeValueDER()); + assertArrayEquals(new Byte[] { 0x1 }, new Int(Int.TAG, null, 1).encodeValueDER()); + assertArrayEquals(new Byte[] { -1 }, new Int(Int.TAG, null, 255).encodeValueDER()); + + // Multiple bytes + assertArrayEquals(new Byte[] { 0x01, 0x00 }, new Int(Int.TAG, null, 256).encodeValueDER()); + assertArrayEquals(new Byte[] { -1, -1 }, new Int(Int.TAG, null, 65535).encodeValueDER()); + assertArrayEquals(new Byte[] { 0x01, 0x00, 0x00 }, new Int(Int.TAG, null, 65536).encodeValueDER()); + assertArrayEquals(new Byte[] { -1, -1, -1 }, new Int(Int.TAG, null, 16777215).encodeValueDER()); + assertArrayEquals(new Byte[] { 0x01, 0x00, 0x00, 0x00 }, new Int(Int.TAG, null, 16777216).encodeValueDER()); + assertArrayEquals(new Byte[] { -1, -1, -1, -1 }, new Int(Int.TAG, null, 4294967295L).encodeValueDER()); + assertArrayEquals(new Byte[] { -1, -1, -1, -1 }, new Int(Int.TAG, null, 4294967295L).encodeValueDER()); + + // 2 ^ 63 + 1 + assertArrayEquals(new Byte[] { -128, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }, + new Int(Int.TAG, null, Long.parseUnsignedLong("9223372036854775809")).encodeValueDER()); + + // Test no leading zeros + // Not 0x00, 0xFF + assertArrayEquals(new Byte[] { -1 }, new Int(Int.TAG, null, 255).encodeValueDER()); + + // Test no leading ones + // Not 0xFF, 0x80 + assertArrayEquals(new Byte[] { -128 }, new Int(Int.TAG, null, -128).encodeValueDER()); + + // Encode DER + assertArrayEquals(new Byte[] { 0x02, 2, 0x01, 0x00 }, new Int(Int.TAG, null, 256).encodeDER()); + } + + @Test + void testParse() throws ParseException { + // Single-byte + assertEquals(0, new Int(new BytesReader(new Byte[] { 0x2, 1, 0x0 }), false).getLong()); + assertEquals(1, new Int(new BytesReader(new Byte[] { 0x2, 1, 0x1 }), false).getLong()); + assertEquals(-1, new Int(new BytesReader(new Byte[] { 0x2, 1, -1 }), false).getLong()); + + // Multiple bytes + assertEquals(256, + new Int(new BytesReader(new Byte[] { 0x2, 2, 0x01, 0x00 }), false).getLong()); + assertEquals(-1, + new Int(new BytesReader(new Byte[] { 0x2, 2, -1, -1 }), false).getLong()); + assertEquals(65536, + new Int(new BytesReader(new Byte[] { 0x2, 3, 0x01, 0x00, 0x00 }), false).getLong()); + assertEquals(-1, + new Int(new BytesReader(new Byte[] { 0x2, 3, -1, -1, -1 }), false).getLong()); + assertEquals(16777216, + new Int(new BytesReader(new Byte[] { 0x2, 4, 0x01, 0x00, 0x00, 0x00 }), false).getLong()); + assertEquals(-1, + new Int(new BytesReader(new Byte[] { 0x2, 4, -1, -1, -1, -1 }), false).getLong()); + assertEquals(4294967296L, + new Int(new BytesReader(new Byte[] { 0x2, 5, 0x01, 0x00, 0x00, 0x00, 0x00 }),false).getLong()); + + // 2 ^ 63 + 1 + assertEquals(Long.parseUnsignedLong("9223372036854775809"), + new Int(new BytesReader(new Byte[] { 0x2, 9, 0x00, -128, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01 }), + false).getValue().longValue()); + + // Test no leading zeros + // Not 0x00, 0xFF + assertEquals(255, new Int(new BytesReader(new Byte[] { 0x02, 0x2, 0x0, -1 }), false).getLong()); + + // Test no leading ones + // Not 0xFF, 0x80 + assertArrayEquals(new Byte[] { -128 }, new Int(Int.TAG, null, -128).encodeValueDER()); + } + + @Test + void testParseFail() { + // Not enough bytes + assertThrows(ParseException.class, () -> + new Int(new BytesReader(new Byte[]{ 0x2, 0x7, -1, -1, -1, -1, -1, -1 }), + false)); + // Zero len + assertThrows(ParseException.class, () -> + new Int(new BytesReader(new Byte[]{ 0x2, 0x0, -1, -1, -1, -1, -1, -1 }), + false)); + } +} diff --git a/src/test/model/asn1/NullTest.java b/src/test/model/asn1/NullTest.java new file mode 100644 index 0000000..4ec2c64 --- /dev/null +++ b/src/test/model/asn1/NullTest.java @@ -0,0 +1,38 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class NullTest { + @Test + void testConstructor() { + assertEquals(Null.TAG, new Null(Null.TAG, null).getTag()); + assertEquals(0x01, + new Null(Null.TAG, new Tag(TagClass.CONTEXT_SPECIFIC, true, 0x01)).getParentTag().getNumber()); + } + + @Test + void testEncode() { + assertEquals(0, new Null(Null.TAG, null).encodeValueDER().length); + assertArrayEquals(new Byte[] { + 0x5, 0x0 // Tag - Length + }, new Null(Null.TAG, null).encodeDER()); + } + + @Test + void testParse() throws ParseException { + new Null(new BytesReader(new Byte[]{ 0x5, 0x0 }), false); + new Null(new BytesReader(new Byte[]{ -95, 2, 0x5, 0x0 }), true); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> + new Null(new BytesReader(new Byte[]{ 0x5, 0x2 }), false)); + assertThrows(ParseException.class, () -> + new Null(new BytesReader(new Byte[]{ 0x5, 0x2, 1, 1 }), false)); + } +} diff --git a/src/test/model/asn1/ObjectIdentifierTest.java b/src/test/model/asn1/ObjectIdentifierTest.java new file mode 100644 index 0000000..f6f1049 --- /dev/null +++ b/src/test/model/asn1/ObjectIdentifierTest.java @@ -0,0 +1,89 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ObjectIdentifierTest { + @Test + void testConstructor() { + assertArrayEquals(new Integer[]{ 1, 3, 6, 1, 4}, + new ObjectIdentifier(ObjectIdentifier.TAG, null, new Integer[]{ 1, 3, 6, 1, 4 }).getInts()); + assertArrayEquals(new Integer[]{ 1, 2, 3, 4, 5}, + new ObjectIdentifier(ObjectIdentifier.TAG, null, new Integer[]{ 1, 2, 3, 4, 5 }).getInts()); + } + + @Test + void testToString() { + assertEquals("1.3.6.1.4", + new ObjectIdentifier(ObjectIdentifier.TAG, null, new Integer[]{ 1, 3, 6, 1, 4 }).toString()); + assertEquals("1.2.3.4.5.6", + new ObjectIdentifier(ObjectIdentifier.TAG, null, new Integer[]{ 1, 2, 3, 4, 5, 6 }).toString()); + assertEquals("CN", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_CN).toString()); + assertEquals("SN", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SN).toString()); + assertEquals("C", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_C).toString()); + assertEquals("L", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_L).toString()); + assertEquals("O", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_O).toString()); + assertEquals("OU", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_OU).toString()); + assertEquals("DC", + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_DC).toString()); + } + + @Test + void testEncode() { + assertArrayEquals(new Byte[]{ 0x55, 0x04, 0x0A }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_O).encodeValueDER()); + assertArrayEquals(new Byte[]{ 0x67, -127, 0x0C, 0x01, 0x02, 0x01 }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + new Integer[]{ 2, 23, 140, 1, 2, 1 }).encodeValueDER()); + assertArrayEquals(new Byte[]{ 0x2B, 0x06, 0x01, 0x04, 0x01, -126, -33, 0x13, 0x01, 0x01, 0x01 }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + new Integer[]{ 1, 3, 6, 1, 4, 1, 44947, 1, 1, 1 }).encodeValueDER()); + assertArrayEquals(new Byte[]{ 0x2A, -122, 0x48, -50, 0x3D, 0x02, 0x01 }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + new Integer[]{ 1, 2, 840, 10045, 2, 1 }).encodeValueDER()); + assertArrayEquals(new Byte[]{ 0x2A, -122, 0x48, -122, -9, 0x0D, 0x01, 0x01, 0x0B }, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION).encodeValueDER()); + + } + + @Test + void testParse() throws ParseException { + assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 0x9, 0x2A, -122, 0x48, -122, -9, 0x0D, + 0x01, 0x01, 0x0B }),false).getInts()); + assertArrayEquals(new Integer[]{ 1, 2, 840, 10045, 2, 1 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 7, 0x2A, -122, 0x48, -50, 0x3D, 0x02, 0x01 }), + false).getInts()); + assertArrayEquals(new Integer[]{ 0, 2, 840, 10045, 2, 1 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 7, 2, -122, 0x48, -50, 0x3D, 0x02, 0x01 }), + false).getInts()); + assertArrayEquals(new Integer[]{ 2, 2, 840, 10045, 2, 1 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 7, 82, -122, 0x48, -50, 0x3D, 0x02, 0x01 }), + false).getInts()); + assertArrayEquals(new Integer[]{ 2, 42, 840, 10045, 2, 1 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 7, 122, -122, 0x48, -50, 0x3D, 0x02, 0x01 }), + false).getInts()); + assertArrayEquals(new Integer[]{ 1, 2, 840, 113549, 1, 9, 14 }, + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x06, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, 0x01, + 0x09, 0x0E }), false).getInts()); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 0x0 }), false)); + assertThrows(ParseException.class, () -> + new ObjectIdentifier(new BytesReader(new Byte[]{ 0x6, 0x9, 0x2A, -122, 0x48, -122, -9, 0x0D, + 0x01, 0x01, -117 }), false)); + } +} diff --git a/src/test/model/asn1/OctetStringTest.java b/src/test/model/asn1/OctetStringTest.java new file mode 100644 index 0000000..9e6e8a9 --- /dev/null +++ b/src/test/model/asn1/OctetStringTest.java @@ -0,0 +1,44 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class OctetStringTest { + @Test + void testConstructor() { + assertArrayEquals(new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }, + new OctetString(OctetString.TAG, null, new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }) + .getBytes()); + assertArrayEquals(new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }, + new OctetString(OctetString.TAG, null, new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }) + .encodeValueDER()); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(new Byte[]{ 0x04, 0x06, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }, + new OctetString(OctetString.TAG, null, new Byte[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 }) + .encodeDER()); + } + + @Test + void testParse() throws ParseException { + assertArrayEquals(new Byte[]{ 0x0A, 0x0B, 0x0C }, + new OctetString(new BytesReader(new Byte[]{ 0x4, 3, 0x0A, 0x0B, 0x0C }), false) + .getBytes()); + } + + @Test + void testParseFail() { + // EOF + assertThrows(ParseException.class, () -> + new OctetString(new BytesReader(new Byte[]{ 0x4, 2, 0x0 }), false)); + } +} diff --git a/src/test/model/asn1/PrintableStringTest.java b/src/test/model/asn1/PrintableStringTest.java new file mode 100644 index 0000000..f46f400 --- /dev/null +++ b/src/test/model/asn1/PrintableStringTest.java @@ -0,0 +1,71 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class PrintableStringTest { + private final String ILLEGAL_CHARS_TO_TEST = + "<>\\[]{}@!*&^%$#_|`~"; + + @Test + void testAcceptedString() throws ParseException { + final PrintableString string = + new PrintableString(PrintableString.TAG, null, "0123456789abCdExYz '()+,-./:=?"); + assertEquals("0123456789abCdExYz '()+,-./:=?", string.getString()); + } + + @Test + void testIllegalStrings() { + ILLEGAL_CHARS_TO_TEST.chars() + .forEach(c -> + assertThrows(ParseException.class, + () -> new PrintableString(PrintableString.TAG, null, Character.toString(c)), + String.format("Expected failing validation by char '%c'.", c)) + ); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals( + new Byte[] { 0x68, 0x68, 0x68 }, + new PrintableString(PrintableString.TAG, null, "hhh").encodeValueDER()); + assertArrayEquals( + new Byte[] { + 0x13, 0x02, // Tag - Length + 0x68, 0x69 // Value + }, new PrintableString(PrintableString.TAG, null, "hi").encodeDER()); + assertArrayEquals( + new Byte[] { + -86, 0x05, // Parent Tag - Length + 0x13, 0x03, // Inner Tag - Length + 0x68, 0x69, 0x69 // Value + }, new PrintableString(PrintableString.TAG, + new Tag(TagClass.CONTEXT_SPECIFIC, true, 10), + "hii").encodeDER()); + } + + @Test + void testParse() throws ParseException { + assertEquals("123", + new PrintableString(new BytesReader(new Byte[]{ 0x13, 3, '1', '2', '3' }), false) + .getString()); + assertEquals("", + new PrintableString(new BytesReader(new Byte[]{ 0x13, 0, '1', '2', '3' }), false) + .getString()); + } + + @Test + void testParseFail() { + // EOF + assertThrows(ParseException.class, () -> + new PrintableString(new BytesReader(new Byte[]{ 0x13, 1 }), false)); + // Illegal chars + assertThrows(ParseException.class, () -> + new PrintableString(new BytesReader(new Byte[]{ 0x13, 2, '1', '*' }), false)); + assertThrows(ParseException.class, () -> + new PrintableString(new BytesReader(new Byte[]{ 0x13, 2, '1', '@' }), false)); + } +} diff --git a/src/test/model/asn1/TagClassTest.java b/src/test/model/asn1/TagClassTest.java new file mode 100644 index 0000000..d510618 --- /dev/null +++ b/src/test/model/asn1/TagClassTest.java @@ -0,0 +1,15 @@ +package model.asn1; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TagClassTest { + @Test + void testConstructor() { + assertEquals(TagClass.Values.UNIVERSAL, TagClass.UNIVERSAL.getVal()); + assertEquals(TagClass.Values.PRIVATE, TagClass.PRIVATE.getVal()); + assertEquals(TagClass.Values.CONTENT_SPECIFIC, TagClass.CONTEXT_SPECIFIC.getVal()); + assertEquals(TagClass.Values.APPLICATION, TagClass.APPLICATION.getVal()); + } +} diff --git a/src/test/model/asn1/TagTest.java b/src/test/model/asn1/TagTest.java new file mode 100644 index 0000000..02df91f --- /dev/null +++ b/src/test/model/asn1/TagTest.java @@ -0,0 +1,107 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class TagTest { + @Test + void testConstructor() { + assertEquals(ASN1Object.TAG_SEQUENCE.getNumber(), + new Tag(TagClass.UNIVERSAL, false, ASN1Object.TAG_SEQUENCE.getNumber()).getNumber()); + assertEquals(ASN1Object.TAG_SEQUENCE.getCls(), + new Tag(TagClass.UNIVERSAL, false, ASN1Object.TAG_SEQUENCE.getNumber()).getCls()); + assertEquals(ASN1Object.TAG_SEQUENCE.isConstructive(), + new Tag(TagClass.UNIVERSAL, + ASN1Object.TAG_SEQUENCE.isConstructive(), + ASN1Object.TAG_SEQUENCE.getNumber()) + .isConstructive()); + } + + @Test + void testParseSuccess() throws ParseException { + // Test basic parsing + assertEquals(0x1, + new Tag(new BytesReader(new Byte[]{ 0x1 })).getNumber()); + assertEquals(TagClass.UNIVERSAL, + new Tag(new BytesReader(new Byte[]{ 0x1 })).getCls()); + assertFalse(new Tag(new BytesReader(new Byte[]{ 0x1 })).isConstructive()); + // Test basic parsing with different class + assertEquals(5, + new Tag(new BytesReader(new Byte[]{ 101 })).getNumber()); + assertEquals(TagClass.APPLICATION, + new Tag(new BytesReader(new Byte[]{ 101 })).getCls()); + assertTrue(new Tag(new BytesReader(new Byte[]{ 101 })).isConstructive()); + // Test different classes + assertEquals(TagClass.UNIVERSAL, + new Tag(new BytesReader(new Byte[]{ 1 })).getCls()); // 0b00000001 + assertEquals(TagClass.PRIVATE, + new Tag(new BytesReader(new Byte[]{ -63 })).getCls()); // 0b11000001 + assertEquals(TagClass.CONTEXT_SPECIFIC, + new Tag(new BytesReader(new Byte[]{ -127 })).getCls()); // 0b10000001 + assertEquals(TagClass.APPLICATION, + new Tag(new BytesReader(new Byte[]{ 65 })).getCls()); // 0b01000001 + // Test different numbers + assertEquals(0x10, + new Tag(new BytesReader(new Byte[]{ 0x10 })).getNumber()); + assertEquals(31, + new Tag(new BytesReader(new Byte[]{ 31 })).getNumber()); + // Test constructive bit + assertFalse(new Tag(new BytesReader(new Byte[]{ 0x1 })).isConstructive()); + assertTrue(new Tag(new BytesReader(new Byte[]{ 33 })).isConstructive()); + // Test modification + BytesReader reader = new BytesReader(new Byte[]{ 33 }); + assertEquals(0, reader.getIndex()); + new Tag(reader); + assertEquals(1, reader.getIndex()); + } + + @Test + void testParseFail() { + // No enough bytes + assertThrows(ParseException.class, () -> { + BytesReader reader = new BytesReader(new Byte[]{ 33 }); + reader.require(1, true); + new Tag(reader); + }); + // Number zero + assertThrows(ParseException.class, () -> new Tag(new BytesReader(new Byte[]{ 0 }))); + } + + @Test + void testEncode() { + // Basic encoding + assertArrayEquals(new Byte[]{ 1 }, new Tag(TagClass.UNIVERSAL, false, 1).encodeDER()); + assertArrayEquals(new Byte[]{ 31 }, new Tag(TagClass.UNIVERSAL, false, 31).encodeDER()); + // With different class + assertArrayEquals(new Byte[]{ -127 }, new Tag(TagClass.CONTEXT_SPECIFIC, false, 1).encodeDER()); + assertArrayEquals(new Byte[]{ -61 }, new Tag(TagClass.PRIVATE, false, 3).encodeDER()); + assertArrayEquals(new Byte[]{ 71 }, new Tag(TagClass.APPLICATION, false, 7).encodeDER()); + // With different constructive bit + assertArrayEquals(new Byte[]{ 63 }, new Tag(TagClass.UNIVERSAL, true, 31).encodeDER()); + } + + @Test + void testEnforce() { + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.UNIVERSAL, true, 9))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.UNIVERSAL, true, 11))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.UNIVERSAL, false, 10))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.APPLICATION, true, 10))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.PRIVATE, true, 10))); + assertThrows(ParseException.class, () -> + new Tag(TagClass.UNIVERSAL, true, 10) + .enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 10))); + } +} diff --git a/src/test/model/asn1/UTF8StringTest.java b/src/test/model/asn1/UTF8StringTest.java new file mode 100644 index 0000000..a2518ae --- /dev/null +++ b/src/test/model/asn1/UTF8StringTest.java @@ -0,0 +1,64 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.*; + +public class UTF8StringTest { + private static final String[] UTF8_CHARS = new String[] { + new String(new byte[]{ -16, -97, -113, -77, -17, -72, -113, -30, + -128, -115, -30, -102, -89, -17, -72, -113 }, StandardCharsets.UTF_8), + new String(new byte[]{ -16, -97, -112, -79 }, StandardCharsets.UTF_8) + }; + + @Test + void testAcceptedString() throws ParseException { + final ASN1String string = + new UTF8String(PrintableString.TAG, null, "0123456789abCdExYz '()+,-./:=?*@" + + String.join("", UTF8_CHARS)); + assertEquals("0123456789abCdExYz '()+,-./:=?*@" + String.join("", UTF8_CHARS), + string.getString()); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals( + new Byte[]{ + -16, -97, -113, -77, -17, -72, -113, -30, + -128, -115, -30, -102, -89, -17, -72, -113, + -16, -97, -112, -79 + }, new UTF8String(UTF8String.TAG, null, UTF8_CHARS[0] + UTF8_CHARS[1]) + .encodeValueDER()); + assertArrayEquals( + new Byte[] { + 0x0C, 6, // Tag - Length + 0x68, 0x69, // Value + -16, -97, -112, -79 + }, new UTF8String(UTF8String.TAG, null, "hi" + UTF8_CHARS[1]).encodeDER()); + } + + @Test + void testParse() throws ParseException { + assertEquals(UTF8_CHARS[0], + new UTF8String(new BytesReader(new Byte[]{ 0x0C, 16, + -16, -97, -113, -77, -17, -72, -113, -30, + -128, -115, -30, -102, -89, -17, -72, -113 + }), false).getString()); + assertEquals("", + new UTF8String(new BytesReader(new Byte[]{ 0x0C, 0, '1', '2', '3' }), false) + .getString()); + } + + @Test + void testParseFail() { + // EOF + assertThrows(ParseException.class, () -> + new UTF8String(new BytesReader(new Byte[]{ 0x0C, 1 }), false)); + } +} diff --git a/src/test/model/asn1/UtcTimeTest.java b/src/test/model/asn1/UtcTimeTest.java new file mode 100644 index 0000000..5ba7e13 --- /dev/null +++ b/src/test/model/asn1/UtcTimeTest.java @@ -0,0 +1,121 @@ +package model.asn1; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +public class UtcTimeTest { + @Test + void testConstructor() throws ParseException { + final ZonedDateTime now = ZonedDateTime.now(); + assertEquals(now, new UtcTime(UtcTime.TAG, null, now).getTimestamp()); + + final ASN1Time parsed = new UtcTime(new BytesReader(new Byte[] { + 0x17, 13, + '1', '9', '0', '8', '1', '0', '1', '1', '4', '5', '1', '4', 'Z' + }), false); + assertEquals("190810114514Z", + parsed.toString()); + assertEquals(ZonedDateTime.of(2019, 8, 10, 11, 45, 14, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + } + + @Test + void testParse() throws ParseException { + ASN1Time parsed = new UtcTime(new BytesReader(new Byte[] { + 0x17, 13, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), false); + + assertEquals(ZonedDateTime.of(2023, 9, 27, 11, 45, 14, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + + // No seconds + parsed = new UtcTime(new BytesReader(new Byte[] { + 0x17, 11, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }), false); + assertEquals(ZonedDateTime.of(2023, 9, 27, 11, 45, 0, + 0, ZoneId.of("UTC")), + parsed.getTimestamp()); + + // Length 0 + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 0 + }), false)); + // Early EOF + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13 + }), false)); + // No tailing Z + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 12, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4' + }), false)); + // Custom timezone + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 18, + '2', '0', '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z', '-', '0', '8' + }), false)); + // Invalid month / day / hour / minute / second + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '3', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '0', '3', '2', '1', '1', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '0', '3', '0', '2', '5', '4', '5', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '0', '3', '0', '2', '4', '6', '1', '1', '4', 'Z' + }), false)); + assertThrows(ParseException.class, () -> + new UtcTime(new BytesReader(new Byte[]{ + 0x17, 13, + '2', '3', '1', '0', '3', '0', '2', '4', '6', '0', '6', '1', 'Z' + }), false)); + } + + @Test + void testEncode() throws ParseException { + assertEquals("230927114514Z", new UtcTime(new BytesReader(new Byte[] { + -95, 15, + 0x17, 13, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', '1', '4', 'Z' + }), true).toString()); + // No seconds + assertEquals("2309271145Z", new UtcTime(new BytesReader(new Byte[] { + -95, 13, + 0x17, 11, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }), true).toString()); + + // To byte array + assertArrayEquals(new Byte[] { + 0x17, 11, + '2', '3', '0', '9', '2', '7', '1', '1', '4', '5', 'Z' + }, new UtcTime(UtcTime.TAG, null, ZonedDateTime.of(2023, 9, + 27, 11, 45, 0, 0, ZoneId.of("UTC"))) + .encodeDER()); + } +} diff --git a/src/test/model/asn1/parsing/BytesReaderTest.java b/src/test/model/asn1/parsing/BytesReaderTest.java new file mode 100644 index 0000000..3b63a79 --- /dev/null +++ b/src/test/model/asn1/parsing/BytesReaderTest.java @@ -0,0 +1,95 @@ +package model.asn1.parsing; + +import model.asn1.Tag; +import model.asn1.TagClass; +import model.asn1.exceptions.ParseException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class BytesReaderTest { + private BytesReader target; + + @BeforeEach + void setup() { + target = new BytesReader(new Byte[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + } + + @Test + void testConstructor() { + assertEquals(0, target.getIndex()); + assertEquals(10, target.getRawInput().length); + assertArrayEquals(new Byte[]{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, target.getRawInput()); + } + + @Test + void testBytesRemaining() { + assertEquals(10, target.bytesRemaining()); + target.read(5, true); + assertEquals(5, target.bytesRemaining()); + } + + @Test + void testRead() { + assertEquals(10, target.bytesRemaining()); + assertEquals(0, target.getIndex()); + assertArrayEquals(new Byte[]{ 1, 2 }, target.read(2, true)); + assertEquals(8, target.bytesRemaining()); + assertEquals(2, target.getIndex()); + assertArrayEquals(new Byte[]{ 3 }, target.read(1, false)); + assertEquals(8, target.bytesRemaining()); + assertEquals(2, target.getIndex()); + assertArrayEquals(new Byte[]{ 3, 4, 5, 6, 7 }, target.read(5, true)); + assertEquals(3, target.bytesRemaining()); + assertEquals(7, target.getIndex()); + assertArrayEquals(new Byte[]{ 8, 9, 10 }, target.read(3, false)); + assertEquals(3, target.bytesRemaining()); + assertEquals(7, target.getIndex()); + assertArrayEquals(new Byte[]{ 8, 9, 10 }, target.read(3, true)); + assertEquals(0, target.bytesRemaining()); + assertEquals(10, target.getIndex()); + } + + @Test + void testRequire() throws Exception { // TODO: Exception testing + assertEquals(10, target.bytesRemaining()); + assertEquals(0, target.getIndex()); + assertArrayEquals(new Byte[]{ 1, 2 }, target.require(2, true)); + assertArrayEquals(new Byte[]{ 3, 4, 5, 6, 7, 8, 9 }, target.require(7, true)); + assertArrayEquals(new Byte[]{ 10 }, target.require(1, false)); + assertThrows(ParseException.class, () -> target.require(2, true)); + assertArrayEquals(new Byte[]{ 10 }, target.require(1, true)); + assertThrows(ParseException.class, () -> target.require(1, true)); + } + + @Test + void testValidateSize() throws Exception { // TODO: Exception testing + assertEquals(10, target.bytesRemaining()); + assertEquals(0, target.getIndex()); + assertArrayEquals(new Byte[]{ 1, 2 }, target.require(2, true)); + assertArrayEquals(new Byte[]{ 3, 4, 5, 6, 7, 8, 9 }, target.require(7, true)); + assertArrayEquals(new Byte[]{ 10 }, target.require(1, false)); + target.validateSize(1); + assertThrows(ParseException.class, () -> target.validateSize(2)); + assertArrayEquals(new Byte[]{ 10 }, target.require(1, true)); + assertThrows(ParseException.class, () -> target.validateSize(1)); + } + + @Test + void testDetectTag() throws Exception { + final BytesReader reader = new BytesReader(new Byte[]{ -95, 0 }); + assertTrue(reader.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, true, 1))); + assertFalse(reader.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, true, 0))); + assertFalse(reader.detectTag(new Tag(TagClass.UNIVERSAL, true, 0))); + assertFalse(reader.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, false, 0))); + } + + @Test + void testGetTag() throws Exception { + BytesReader reader = new BytesReader(new Byte[]{ -95, 0 }); + assertEquals(1, reader.getTag(false).getNumber()); + reader = new BytesReader(new Byte[]{ -96, 0, -95, 0 }); + assertEquals(1, reader.getTag(true).getNumber()); + } +} diff --git a/src/test/model/csr/AttributeTest.java b/src/test/model/csr/AttributeTest.java new file mode 100644 index 0000000..f4daa4c --- /dev/null +++ b/src/test/model/csr/AttributeTest.java @@ -0,0 +1,53 @@ +package model.csr; + +import model.TestConstants; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static model.asn1.ObjectIdentifier.*; +import static org.junit.jupiter.api.Assertions.*; + +public class AttributeTest { + @Test + void testConstructor() throws ParseException { + assertArrayEquals(OID_EXTENSION_REQUEST, TestConstants.CSR_ATTR_2.getType().getInts()); + assertEquals(0x21, TestConstants.CSR_ATTR_2.getValues().getArray()[1].getLength()); + } + + @Test + void testParse() throws ParseException { + final Attribute parsed = new Attribute(new BytesReader(TestConstants.CSR_ATTR_VALUES_2_DER), false); + + assertArrayEquals(OID_EXTENSION_REQUEST, parsed.getType().getInts()); + assertEquals(2, parsed.getValues().getArray().length); + } + + @Test + void testParseFail() { + // No type + assertThrows(ParseException.class, () -> new Attribute(new BytesReader(new Byte[]{ + 0x30, 0x0 + }), false)); + // No value + assertThrows(ParseException.class, () -> new Attribute(new BytesReader(new Byte[]{ + 0x30, 0x5, 0x6, 0x3, 0x55, 0x4, 0x6 + }), false)); + // Incorrect type tag (should be OID) + assertThrows(ParseException.class, () -> new Attribute(new BytesReader(new Byte[]{ + 0x30, 0x9, 0x7, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54 + }), false)); + // Incorrect value tag (should be SET) + assertThrows(ParseException.class, () -> new Attribute(new BytesReader(new Byte[]{ + 0x30, 13, + 0x06, 0x09, 0x1A, -122, 0x48, -122, -9, 0x0D, 0x01, 0x09, 0x0E, + 0x30, 0 + }), false)); + } + + @Test + void testEncode() { + assertArrayEquals(TestConstants.CSR_ATTR_1_DER, TestConstants.CSR_ATTR_1.encodeDER()); + assertArrayEquals(TestConstants.CSR_ATTR_VALUES_2_DER, TestConstants.CSR_ATTR_2.encodeDER()); + } +} diff --git a/src/test/model/csr/AttributesTest.java b/src/test/model/csr/AttributesTest.java new file mode 100644 index 0000000..86a0112 --- /dev/null +++ b/src/test/model/csr/AttributesTest.java @@ -0,0 +1,69 @@ +package model.csr; + +import model.TestConstants; +import model.asn1.ObjectIdentifier; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class AttributesTest { + @Test + void testConstructor() { + assertEquals("10.0.19045.2", + TestConstants.CSR_ATTRS_2.getArray()[1].getValues().getArray()[0].toString()); + assertArrayEquals(ObjectIdentifier.OID_EXTENSION_REQUEST, + TestConstants.CSR_ATTRS_2.getArray()[0].getType().getInts()); + } + + @Test + void testParse() throws ParseException { + final Attributes parsed = new Attributes(new BytesReader(new Byte[]{ + -96, 30, + 0x30, 0x1C, + 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0D, 0x02, 0x03, + 0x31, 0x0E, + 0x16, 0x0C, 0x31, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x39, 0x30, 0x34, 0x35, 0x2E, 0x32 + }), false); + assertEquals(1, parsed.getArray().length); + assertEquals("10.0.19045.2", parsed.getArray()[0].getValues().getArray()[0].toString()); + } + + @Test + void testParseFail() { + // Incorrect length + assertThrows(ParseException.class, () -> new Attributes(new BytesReader(new Byte[]{ + -96, 31, // Incorrect + 0x30, 0x1C, + 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0D, 0x02, 0x03, + 0x31, 0x0E, + 0x16, 0x0C, 0x31, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x39, 0x30, 0x34, 0x35, 0x2E, 0x32 + }), false)); + // Incorrect child item tag + assertThrows(ParseException.class, () -> new Attributes(new BytesReader(new Byte[]{ + -96, 30, + 0x31, 0x1C, // Incorrect + 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0D, 0x02, 0x03, + 0x31, 0x0E, + 0x16, 0x0C, 0x31, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x39, 0x30, 0x34, 0x35, 0x2E, 0x32 + }), false)); + } + + @Test + void testEncode() { + Byte[] a2 = TestConstants.CSR_ATTR_2.encodeDER(); + Byte[] a1 = TestConstants.CSR_ATTR_1.encodeDER(); + assertArrayEquals( + Stream.of(Arrays.asList(new Byte[]{ 0x31, (byte)(a2.length + a1.length) }), + Arrays.asList(a2), + Arrays.asList(a1)) + .flatMap(Collection::stream) + .toArray(Byte[]::new), + TestConstants.CSR_ATTRS_2.encodeDER()); + } +} diff --git a/src/test/model/csr/CertificationRequestInfoTest.java b/src/test/model/csr/CertificationRequestInfoTest.java new file mode 100644 index 0000000..fe2633f --- /dev/null +++ b/src/test/model/csr/CertificationRequestInfoTest.java @@ -0,0 +1,174 @@ +package model.csr; + +import model.TestConstants; +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.pki.AlgorithmIdentifier; +import model.pki.SubjectPublicKeyInfo; +import org.junit.jupiter.api.Test; + +import static model.TestConstants.mutate; +import static org.junit.jupiter.api.Assertions.*; + +public class CertificationRequestInfoTest { + static final Byte[] CSR_1 = new Byte[] { + 0x30, -126, 0x02, 0x03, // SEQUENCE CertificationRequestInfo + 0x02, 0x01, 0x00, // Version + 0x30, 0x1c, // SEQUENCE Subject + 0x31, 0x1a, // RDN + 0x30, 0x18, // AttributeTypeAndValue + 0x06, 0x03, 0x55, 0x04, 0x03, // OID (CN) + 0x0c, 0x11, 0x4d, 0x49, 0x4b, 0x55, 0x2e, 0x41, // PrintableString (MIKU.AD.YUUTA.MOE) + 0x44, 0x2e, 0x59, 0x55, 0x55, 0x54, 0x41, 0x2e, + 0x4d, 0x4f, 0x45, + 0x30, -127, -97, // SEQUENCE SubjectPublicKeyInfo + 0x30, 0x0d, // SEQUENCE AlgorithmIdentifier + 0x06, 0x09, 0x2a, -122, 0x48, -122, -9, 0x0d, // OID (rsaEncryption) + 0x01, 0x01, 0x01, + 0x05, 0x00, // Null (Parameter) + 0x03, -127, -115, 0x00, 0x30, -127, -119, 0x02, // BIT STRING (subjectPublicKey) + -127, -127, 0x00, -67, -1, 0x4e, 0x6d, -22, + 0x62, 0x6a, 0x11, -120, 0x77, 0x0a, -92, 0x32, + -124, -37, 0x22, 0x2f, 0x3d, 0x5d, 0x2a, 0x63, + -71, -109, 0x11, -50, -92, 0x4f, -119, 0x3b, + 0x14, 0x3b, -54, 0x3c, -106, -42, 0x11, 0x42, + 0x78, -110, 0x68, -100, -25, -25, -50, 0x75, + -101, 0x21, 0x41, -34, -31, -85, -13, 0x1e, + 0x51, -81, 0x25, 0x4f, -1, 0x56, 0x77, 0x5e, + -30, 0x27, -104, 0x34, 0x67, -28, -56, 0x55, + 0x6a, 0x3c, 0x6f, -38, -85, -63, 0x5f, 0x16, + 0x7a, -93, -19, -35, 0x7f, 0x35, 0x0f, -47, + -7, -22, -12, -24, -48, 0x25, 0x6d, -114, + 0x66, 0x1a, 0x53, -77, 0x67, 0x32, -69, -39, + 0x57, -42, -65, -13, 0x5f, 0x6f, 0x53, 0x6d, + 0x62, -95, 0x42, 0x12, 0x7b, 0x13, 0x4f, 0x1a, + -26, 0x00, -72, -32, 0x2b, -83, 0x3c, 0x35, + -103, 0x18, 0x51, 0x02, 0x03, 0x01, 0x00, 0x01, + -96, -126, 0x01, 0x3c, // SEQUENCE (attributes) + 0x30, 0x1c, 0x06, 0x0a, // SEQUENCE (attribute) + 0x2b, 0x06, 0x01, 0x04, 0x01, -126, 0x37, 0x0d, + 0x02, 0x03, 0x31, 0x0e, 0x16, 0x0c, 0x31, 0x30, + 0x2e, 0x30, 0x2e, 0x31, 0x39, 0x30, 0x34, 0x35, + 0x2e, 0x32, + 0x30, 0x42, 0x06, // SEQUENCE (attribute) + 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, -126, 0x37, + 0x15, 0x14, 0x31, 0x35, 0x30, 0x33, 0x02, 0x01, + 0x05, 0x0c, 0x11, 0x4d, 0x49, 0x4b, 0x55, 0x2e, + 0x41, 0x44, 0x2e, 0x59, 0x55, 0x55, 0x54, 0x41, + 0x2e, 0x4d, 0x4f, 0x45, 0x0c, 0x12, 0x4d, 0x49, + 0x4b, 0x55, 0x5c, 0x41, 0x64, 0x6d, 0x69, 0x6e, + 0x69, 0x73, 0x74, 0x72, 0x61, 0x74, 0x6f, 0x72, + 0x0c, 0x07, 0x4d, 0x4d, 0x43, 0x2e, 0x45, 0x58, + 0x45, + 0x30, 0x66, // SEQUENCE (attribute) + 0x06, 0x0a, 0x2b, 0x06, 0x01, 0x04, 0x01, -126, + 0x37, 0x0d, 0x02, 0x02, 0x31, 0x58, 0x30, 0x56, + 0x02, 0x01, 0x00, 0x1e, 0x4e, 0x00, 0x4d, 0x00, + 0x69, 0x00, 0x63, 0x00, 0x72, 0x00, 0x6f, 0x00, + 0x73, 0x00, 0x6f, 0x00, 0x66, 0x00, 0x74, 0x00, + 0x20, 0x00, 0x53, 0x00, 0x6f, 0x00, 0x66, 0x00, + 0x74, 0x00, 0x77, 0x00, 0x61, 0x00, 0x72, 0x00, + 0x65, 0x00, 0x20, 0x00, 0x4b, 0x00, 0x65, 0x00, + 0x79, 0x00, 0x20, 0x00, 0x53, 0x00, 0x74, 0x00, + 0x6f, 0x00, 0x72, 0x00, 0x61, 0x00, 0x67, 0x00, + 0x65, 0x00, 0x20, 0x00, 0x50, 0x00, 0x72, 0x00, + 0x6f, 0x00, 0x76, 0x00, 0x69, 0x00, 0x64, 0x00, + 0x65, 0x00, 0x72, 0x03, 0x01, 0x00, + 0x30, 0x70, 0x06, 0x09, // SEQUENCE (attribute) + 0x2a, -122, 0x48, -122, -9, 0x0d, 0x01, 0x09, + 0x0e, 0x31, 0x63, 0x30, 0x61, 0x30, 0x0e, 0x06, + 0x03, 0x55, 0x1d, 0x0f, 0x01, 0x01, -1, 0x04, + 0x04, 0x03, 0x02, 0x05, -96, 0x30, 0x13, 0x06, + 0x03, 0x55, 0x1d, 0x25, 0x04, 0x0c, 0x30, 0x0a, + 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, + 0x03, 0x01, 0x30, 0x1b, 0x06, 0x09, 0x2b, 0x06, + 0x01, 0x04, 0x01, -126, 0x37, 0x15, 0x0a, 0x04, + 0x0e, 0x30, 0x0c, 0x30, 0x0a, 0x06, 0x08, 0x2b, + 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, 0x30, + 0x1d, 0x06, 0x03, 0x55, 0x1d, 0x0e, 0x04, 0x16, + 0x04, 0x14, -15, 0x3e, -110, -16, 0x4d, 0x1b, + -47, 0x6e, 0x53, 0x7f, -102, 0x1d, 0x19, -75, + 0x5e, -22, 0x64, 0x7f, 0x1f, -110, + }; + + @Test + void testConstructor() { + final CertificationRequestInfo info = new CertificationRequestInfo( + ASN1Object.TAG_SEQUENCE, null, + new Int(Int.TAG, null, CertificationRequestInfo.VERSION_V1), + TestConstants.NAME_2, + new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null, + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new BitString(BitString.TAG, null, + 0, new Byte[]{ 1, 2, 3, 4, 5 })), + TestConstants.CSR_ATTRS_2); + assertEquals(CertificationRequestInfo.VERSION_V1, info.getVersion().getLong()); + assertEquals(3, info.getSubject().getRdnSequence().length); + assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION, + info.getSubjectPKInfo().getAlgorithm().getType().getInts()); + assertEquals(2, info.getAttributes().getArray().length); + } + + @Test + void testParse() throws ParseException { + final CertificationRequestInfo parsed = + new CertificationRequestInfo(new BytesReader(CSR_1), false); + assertEquals("CN=MIKU.AD.YUUTA.MOE", parsed.getSubject().toString()); + assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, + parsed.getSubjectPKInfo().getAlgorithm().getType().getInts()); + assertEquals(4, parsed.getAttributes().getArray().length); + assertEquals(1, parsed.getAttributes().getArray()[3].getValues().getArray().length); + } + + @Test + void testParseFail() { + // No version + assertThrows(ParseException.class, () -> { + new CertificationRequestInfo(new BytesReader(new Byte[]{ + 0x30, 0 + }), false); + }); + // Incorrect version tag + assertThrows(ParseException.class, () -> { + new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 4, (byte) Int.TAG.getNumber(), 0x3)), + false); + }); + // Incorrect version + assertThrows(ParseException.class, () -> { + new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 6, CertificationRequestInfo.VERSION_V1, 1)), + false); + }); + // No subject + assertThrows(ParseException.class, () -> { + Byte[] test = new Byte[5]; + test[0] = 0x30; + test[1] = 3; + System.arraycopy(CSR_1, 4, test, 2, 3); + new CertificationRequestInfo(new BytesReader(test), false); + }); + // Incorrect subject tag + assertThrows(ParseException.class, () -> { + new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 7, 0x30, 0x31)), + false); + }); + // Incorrect subject pk info tag + assertThrows(ParseException.class, () -> { + new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 37, 0x30, 0x31)), + false); + }); + // Incorrect attributes tag + assertThrows(ParseException.class, () -> { + new CertificationRequestInfo(new BytesReader(mutate(CSR_1, 199, -96, 0x31)), + false); + }); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(CSR_1, new CertificationRequestInfo(new BytesReader(CSR_1), false).encodeDER()); + } +} diff --git a/src/test/model/csr/CertificationRequestTest.java b/src/test/model/csr/CertificationRequestTest.java new file mode 100644 index 0000000..962e90b --- /dev/null +++ b/src/test/model/csr/CertificationRequestTest.java @@ -0,0 +1,114 @@ +package model.csr; + +import model.TestConstants; +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.pki.AlgorithmIdentifier; +import model.pki.SubjectPublicKeyInfo; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +import static model.TestConstants.mutate; +import static org.junit.jupiter.api.Assertions.*; + +public class CertificationRequestTest { + private static final Byte[] CSR_1 = Stream.of( + // SEQUENCE (CertificationRequest) + Arrays.asList(new Byte[]{ 0x30, -126, 0x02, -102 }), + // SEQUENCE (CertificationRequestInfo) + Arrays.asList(CertificationRequestInfoTest.CSR_1), + // SEQUENCE (AlgorithmIdentifier) + Arrays.asList(new Byte[]{ + 0x30, 0x0D, 0x06, 0x09, 0x2A, -122, 0x48, -122, + -9, 0x0D, 0x01, 0x01, 0x0B, 0x05, 0x00 + }), + // BIT STRING (Signature) + Arrays.asList(new Byte[]{ + 0x03, -127, -127, + 0x00, 0x6F, 0x61, 0x5C, -25, 0x29, 0x48, 0x3F, + -78, 0x1B, -117, 0x2C, -93, -114, 0x7D, -77, + 0x62, 0x14, 0x21, 0x4B, -99, 0x74, -95, -93, + 0x16, 0x38, 0x31, 0x40, 0x5E, 0x72, -77, -55, + 0x6D, -69, 0x19, -108, 0x52, -95, 0x19, -121, + -81, -71, 0x74, -123, 0x6B, -27, -20, 0x4C, + -126, 0x42, -89, 0x66, 0x6A, 0x52, -34, 0x62, + 0x72, 0x40, 0x2C, -79, 0x78, -117, -100, -70, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x7E, + })).flatMap(Collection::stream).toArray(Byte[]::new); + + @Test + void testConstructor() { + final CertificationRequest request = new CertificationRequest( + ASN1Object.TAG_SEQUENCE, null, + new CertificationRequestInfo( + ASN1Object.TAG_SEQUENCE, null, + new Int(Int.TAG, null, CertificationRequestInfo.VERSION_V1), + TestConstants.NAME_2, + new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null, + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new BitString(BitString.TAG, null, + 0, new Byte[]{ 1, 2, 3, 4, 5 })), + TestConstants.CSR_ATTRS_2), + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new BitString(BitString.TAG, null, + 0, new Byte[]{ 2, 4, 6, 8, 10 })); + + assertEquals(CertificationRequestInfo.VERSION_V1, + request.getCertificationRequestInfo().getVersion().getLong()); + assertEquals(3, + request.getCertificationRequestInfo().getSubject().getRdnSequence().length); + assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION, + request.getSignatureAlgorithm().getType().getInts()); + assertArrayEquals(new Byte[]{ 2, 4, 6, 8, 10 }, + request.getSignature().getConvertedVal()); + } + + @Test + void testParse() throws ParseException { + final CertificationRequest parsed = + new CertificationRequest(new BytesReader(CSR_1), false); + assertEquals("CN=MIKU.AD.YUUTA.MOE", + parsed.getCertificationRequestInfo().getSubject().toString()); + assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION, + parsed.getSignatureAlgorithm().getType().getInts()); + } + + @Test + void testParseFail() throws ParseException { + // Incorrect info tag + assertThrows(ParseException.class, () -> { + new CertificationRequest(new BytesReader(mutate(CSR_1, 4, 0x30, 0x31)), false); + }); + // Incorrect algorithm info tag + assertThrows(ParseException.class, () -> { + new CertificationRequest(new BytesReader(mutate(CSR_1, 523, 0x30, 0x31)), false); + }); + // Incorrect signature tag + assertThrows(ParseException.class, () -> { + new CertificationRequest(new BytesReader(mutate(CSR_1, 538, 0x3, 0x31)), false); + }); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(CSR_1, new CertificationRequest(new BytesReader(CSR_1), false).encodeDER()); + } +} diff --git a/src/test/model/csr/ValuesTest.java b/src/test/model/csr/ValuesTest.java new file mode 100644 index 0000000..93229a4 --- /dev/null +++ b/src/test/model/csr/ValuesTest.java @@ -0,0 +1,133 @@ +package model.csr; + +import model.asn1.ASN1Object; +import model.asn1.Null; +import model.asn1.ObjectIdentifier; +import model.asn1.PrintableString; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.x501.AttributeTypeAndValue; +import model.x501.RelativeDistinguishedName; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static model.asn1.ASN1Object.TAG_SET; +import static model.asn1.ObjectIdentifier.OID_C; +import static model.asn1.ObjectIdentifier.OID_OU; +import static org.junit.jupiter.api.Assertions.*; + +public class ValuesTest { + // An opaque value of extended key usage block. + private ASN1Object extKeyUsage; + + // Example SubjectAlternativeName request attribute. + private ASN1Object san; + + private Values values; + + @BeforeEach + void setup() throws ParseException { + extKeyUsage = ASN1Object.parse(new BytesReader(new Byte[]{ + 0x30, 0x1F, // SEQUENCE (AttributeValue) + 0x30, 0x1D, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage + 0x04, 0x16, // OCTET STRING + 0x30, 0x14, // SEQUENCE + // 1.3.6.1.5.5.7.3.1 serverAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, + // 1.3.6.1.5.5.7.3.2 clientAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02 + }), false); + + san = ASN1Object.parse(new BytesReader(new Byte[]{ + 0x30, 0x21, // SEQUENCE (AttributeValue) + 0x30, 0x1F, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName + 0x04, 0x18, // OCTET STRING + 0x30, 0x16, // SEQUENCE + -126, 0x14, // [2] + 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe + 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65 + }), false); + + values = new Values(TAG_SET, null, new ASN1Object[]{ + extKeyUsage, + san + }); + } + + @Test + void testConstructor() { + assertEquals(0x1F, values.getArray()[0].getLength()); + assertEquals(0x21, values.getArray()[1].getLength()); + } + + @Test + void testParse() throws ParseException { + assertEquals(0x1F, new Values(new BytesReader(new Byte[]{ + 0x31, 0x21, + 0x30, 0x1F, // SEQUENCE (AttributeValue) + 0x30, 0x1D, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage + 0x04, 0x16, // OCTET STRING + 0x30, 0x14, // SEQUENCE + // 1.3.6.1.5.5.7.3.1 serverAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, + // 1.3.6.1.5.5.7.3.2 clientAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02 + }), false).getArray()[0].getLength()); + assertEquals(0x21, new Values(new BytesReader(new Byte[]{ + 0x31, 0x23, + 0x30, 0x21, // SEQUENCE (AttributeValue) + 0x30, 0x1F, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName + 0x04, 0x18, // OCTET STRING + 0x30, 0x16, // SEQUENCE + -126, 0x14, // [2] + 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe + 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65 + }), false).getArray()[0].getLength()); + } + + @Test + void testParseFail() { + // Incorrect length + assertThrows(ParseException.class, () -> new Values(new BytesReader(new Byte[]{ + 0x31, 0x29, // Incorrect length! + 0x30, 0x21, // SEQUENCE (AttributeValue) + 0x30, 0x1F, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName + 0x04, 0x18, // OCTET STRING + 0x30, 0x16, // SEQUENCE + -126, 0x14, // [2] + 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe + 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65 + }), false)); + } + + @Test + void testEncode() { + assertArrayEquals(new Byte[]{ + 0x31, 68, + + 0x30, 0x1F, // SEQUENCE (AttributeValue) + 0x30, 0x1D, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x25, // 2.5.29.37 extKeyUsage + 0x04, 0x16, // OCTET STRING + 0x30, 0x14, // SEQUENCE + // 1.3.6.1.5.5.7.3.1 serverAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x01, + // 1.3.6.1.5.5.7.3.2 clientAuth + 0x06, 0x08, 0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x03, 0x02, + + 0x30, 0x21, // SEQUENCE (AttributeValue) + 0x30, 0x1F, // SEQUENCE + 0x06, 0x03, 0x55, 0x1D, 0x11, // 2.5.29.17 subjectAltName + 0x04, 0x18, // OCTET STRING + 0x30, 0x16, // SEQUENCE + -126, 0x14, // [2] + 0x6C, 0x70, 0x2D, 0x62, 0x32, 0x35, 0x35, 0x2E, 0x61, // lp-b255.yuuta.moe + 0x64, 0x2E, 0x79, 0x75, 0x75, 0x74, 0x61, 0x2E, 0x6D, 0x6F, 0x65 + }, values.encodeDER()); + } +} diff --git a/src/test/model/pki/AlgorithmIdentifierTest.java b/src/test/model/pki/AlgorithmIdentifierTest.java new file mode 100644 index 0000000..8bcc80e --- /dev/null +++ b/src/test/model/pki/AlgorithmIdentifierTest.java @@ -0,0 +1,84 @@ +package model.pki; + +import model.asn1.ASN1Object; +import model.asn1.Null; +import model.asn1.ObjectIdentifier; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static model.asn1.ObjectIdentifier.*; +import static org.junit.jupiter.api.Assertions.*; + +public class AlgorithmIdentifierTest { + @Test + void testConstructor() { + assertArrayEquals(OID_SHA256_WITH_RSA_ENCRYPTION, + new AlgorithmIdentifier( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)) + .getType().getInts()); + assertEquals(Null.TAG.getNumber(), + new AlgorithmIdentifier( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)) + .getParameters().getTag().getNumber()); + } + + @Test + void testParse() throws ParseException { + AlgorithmIdentifier parsed = new AlgorithmIdentifier(new BytesReader(new Byte[]{ + 0x30, 0x0d, // SEQUENCE AlgorithmIdentifier + 0x06, 0x09, 0x2a, -122, 0x48, -122, -9, 0x0d, // OID (rsaEncryption) + 0x01, 0x01, 0x01, + 0x05, 0x00, // Null (Parameter) + }), false); + assertArrayEquals(OID_RSA_ENCRYPTION, parsed.getType().getInts()); + assertEquals(Null.TAG.getNumber(), parsed.getParameters().getTag().getNumber()); + + parsed = new AlgorithmIdentifier(new BytesReader(new Byte[]{ + 0x30, 0x0D, // SEQUENCE AlgorithmIdentifier + 0x06, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, // OID (sha256WithRsaEncryption) + 0x01, 0x01, 0x0B, + 0x05, 0x00 // Null (Parameter) + }), false); + assertArrayEquals(OID_SHA256_WITH_RSA_ENCRYPTION, parsed.getType().getInts()); + assertEquals(Null.TAG.getNumber(), parsed.getParameters().getTag().getNumber()); + + parsed = new AlgorithmIdentifier(new BytesReader(new Byte[]{ + 0x30, 0x0A, // SEQUENCE AlgorithmIdentifier + 0x06, 0x08, 0x2A, -122, 0x48, -50, 0x3D, 0x04, 0x03, 0x02 // OID (ecdsaWithSHA256) + }), false); + assertArrayEquals(OID_ECDSA_WITH_SHA256, parsed.getType().getInts()); + assertNull(parsed.getParameters()); + } + + @Test + void testParseFail() { + // No type + assertThrows(ParseException.class, () -> new AlgorithmIdentifier(new BytesReader(new Byte[]{ + 0x30, 0x0 + }), false)); + // Incorrect type tag (should be OID) + assertThrows(ParseException.class, () -> new AlgorithmIdentifier(new BytesReader(new Byte[]{ + 0x30, 0x0B, // SEQUENCE AlgorithmIdentifier + 0x07, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, // Incorrect tag + 0x01, 0x01, 0x0B, + }), false)); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(new Byte[]{ + 0x30, 0x0D, // SEQUENCE AlgorithmIdentifier + 0x06, 0x09, 0x2A, -122, 0x48, -122, -9, 0x0D, // OID (sha256WithRsaEncryption) + 0x01, 0x01, 0x0B, + 0x05, 0x00 // Null (Parameter) + }, new AlgorithmIdentifier( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)).encodeDER()); + } +} diff --git a/src/test/model/pki/SubjectPublicKeyInfoTest.java b/src/test/model/pki/SubjectPublicKeyInfoTest.java new file mode 100644 index 0000000..f1cfff0 --- /dev/null +++ b/src/test/model/pki/SubjectPublicKeyInfoTest.java @@ -0,0 +1,115 @@ +package model.pki; + +import model.asn1.ASN1Object; +import model.asn1.BitString; +import model.asn1.Null; +import model.asn1.ObjectIdentifier; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.csr.CertificationRequestInfo; +import model.csr.CertificationRequestInfoTest; +import org.junit.jupiter.api.Test; + +import static model.TestConstants.mutate; +import static org.junit.jupiter.api.Assertions.*; + +public class SubjectPublicKeyInfoTest { + private static final Byte[] RSA = new Byte[] { + 0x30, -127, -97, // SEQUENCE SubjectPublicKeyInfo + 0x30, 0x0d, // SEQUENCE AlgorithmIdentifier + 0x06, 0x09, 0x2a, -122, 0x48, -122, -9, 0x0d, // OID (rsaEncryption) + 0x01, 0x01, 0x01, + 0x05, 0x00, // Null (Parameter) + 0x03, -127, -115, 0x00, 0x30, -127, -119, 0x02, // BIT STRING (subjectPublicKey) + -127, -127, 0x00, -67, -1, 0x4e, 0x6d, -22, + 0x62, 0x6a, 0x11, -120, 0x77, 0x0a, -92, 0x32, + -124, -37, 0x22, 0x2f, 0x3d, 0x5d, 0x2a, 0x63, + -71, -109, 0x11, -50, -92, 0x4f, -119, 0x3b, + 0x14, 0x3b, -54, 0x3c, -106, -42, 0x11, 0x42, + 0x78, -110, 0x68, -100, -25, -25, -50, 0x75, + -101, 0x21, 0x41, -34, -31, -85, -13, 0x1e, + 0x51, -81, 0x25, 0x4f, -1, 0x56, 0x77, 0x5e, + -30, 0x27, -104, 0x34, 0x67, -28, -56, 0x55, + 0x6a, 0x3c, 0x6f, -38, -85, -63, 0x5f, 0x16, + 0x7a, -93, -19, -35, 0x7f, 0x35, 0x0f, -47, + -7, -22, -12, -24, -48, 0x25, 0x6d, -114, + 0x66, 0x1a, 0x53, -77, 0x67, 0x32, -69, -39, + 0x57, -42, -65, -13, 0x5f, 0x6f, 0x53, 0x6d, + 0x62, -95, 0x42, 0x12, 0x7b, 0x13, 0x4f, 0x1a, + -26, 0x00, -72, -32, 0x2b, -83, 0x3c, 0x35, + -103, 0x18, 0x51, 0x02, 0x03, 0x01, 0x00, 0x01, + }; + + private static final Byte[] ECC = new Byte[] { + 0x30, 0x59, // SEQUENCE SubjectPublicKeyInfo + 0x30, 0x13, // SEQUENCE AlgorithmIdentifier + 0x06, 0x07, 0x2A, -122, 0x48, -50, 0x3D, 0x02, 0x01, // OID (ecPublicKey) + 0x06, 0x08, 0x2A, -122, 0x48, -50, 0x3D, 0x03, 0x01, // OID Parameter (prime256v1) + 0x07, + 0x03, 0x42, // BIT STRING + 0x00, 0x04, 0x1D, -24, 0x71, -68, -35, 0x48, 0x70, + 0x26, 0x71, 0x6C, -35, 0x04, 0x5B, 0x3F, 0x5D, -34, + 0x14, 0x31, -117, 0x3F, 0x31, -128, 0x18, 0x2A, 0x33, + -27, 0x19, -122, 0x13, -42, -25, 0x48, 0x2F, -107, + 0x15, 0x3A, 0x59, -115, -19, 0x09, -28, 0x53, 0x1A, + -13, 0x61, -78, 0x35, 0x61, 0x6E, 0x66, 0x5F, 0x5F, + -49, 0x0A, -30, 0x65, 0x65, 0x3D, 0x22, 0x2B, 0x30, + 0x71, 0x2C, 0x24 + }; + + @Test + void testConstructor() { + assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION, + new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null, + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new BitString(BitString.TAG, null, + 0, new Byte[]{ 1, 2, 3})) + .getAlgorithm().getType().getInts()); + assertArrayEquals(new Byte[]{ 1, 2, 3 }, + new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null, + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, + ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new BitString(BitString.TAG, null, + 0, new Byte[]{ 1, 2, 3})) + .getSubjectPublicKey().getConvertedVal()); + } + + @Test + void testParse() throws ParseException { + SubjectPublicKeyInfo parsed = new SubjectPublicKeyInfo(new BytesReader(RSA), false); + assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, parsed.getAlgorithm().getType().getInts()); + assertEquals(Null.TAG.getNumber(), parsed.getAlgorithm().getParameters().getTag().getNumber()); + assertEquals(140, parsed.getSubjectPublicKey().getConvertedVal().length); + + parsed = new SubjectPublicKeyInfo(new BytesReader(ECC), false); + assertArrayEquals(ObjectIdentifier.OID_EC_PUBLIC_KEY, parsed.getAlgorithm().getType().getInts()); + assertEquals(ObjectIdentifier.TAG.getNumber(), parsed.getAlgorithm().getParameters().getTag().getNumber()); + assertArrayEquals(ObjectIdentifier.OID_PRIME256_V1, + ((ObjectIdentifier) parsed.getAlgorithm().getParameters()).getInts()); + assertEquals(65, parsed.getSubjectPublicKey().getConvertedVal().length); + } + + @Test + void testParseFail() { + // No algorithm + assertThrows(ParseException.class, () -> { + new SubjectPublicKeyInfo(new BytesReader(new Byte[]{ + 0x30, 0 + }), false); + }); + // Incorrect algorithm ID tag + assertThrows(ParseException.class, () -> { + new CertificationRequestInfo(new BytesReader(mutate(RSA, 3, 0x30, 0x31)), false); + }); + // Incorrect public key tag + assertThrows(ParseException.class, () -> { + new CertificationRequestInfo(new BytesReader(mutate(RSA, 18, BitString.TAG.getNumber(), 0x31)), + false); + }); + } +} diff --git a/src/test/model/pki/cert/CertificateTest.java b/src/test/model/pki/cert/CertificateTest.java new file mode 100644 index 0000000..70564fc --- /dev/null +++ b/src/test/model/pki/cert/CertificateTest.java @@ -0,0 +1,81 @@ +package model.pki.cert; + +import model.TestConstants; +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.csr.CertificationRequest; +import model.csr.CertificationRequestInfo; +import model.csr.CertificationRequestInfoTest; +import model.pki.AlgorithmIdentifier; +import model.pki.SubjectPublicKeyInfo; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +import static model.TestConstants.mutate; +import static org.junit.jupiter.api.Assertions.*; + +public class CertificateTest { + @Test + void testConstructor() { + final Certificate certificate = new Certificate(ASN1Object.TAG_SEQUENCE, null, + TestConstants.CERT_GENERATED, + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new BitString(BitString.TAG, null, 0, new Byte[]{ 1, 2, 3 })); + + assertEquals(TbsCertificate.VERSION_V3, + certificate.getCertificate().getVersion().getLong()); + assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, + certificate.getSignatureAlgorithm().getType().getInts()); + assertArrayEquals(new Byte[]{ 1, 2, 3 }, + certificate.getSignature().getConvertedVal()); + } + + @Test + void testParse() throws ParseException { + Certificate parsed = new Certificate(new BytesReader(TestConstants.CERT_L2_RSA), false); + assertEquals(TbsCertificate.VERSION_V3, + parsed.getCertificate().getVersion().getLong()); + assertArrayEquals(ObjectIdentifier.OID_ECDSA_WITH_SHA512, parsed.getSignatureAlgorithm().getType().getInts()); + assertNull(parsed.getSignatureAlgorithm().getParameters()); + assertEquals(70, parsed.getSignature().getVal().length); + + parsed = new Certificate(new BytesReader(TestConstants.CERT_L1_ECC), false); + assertEquals(TbsCertificate.VERSION_V3, + parsed.getCertificate().getVersion().getLong()); + assertArrayEquals(ObjectIdentifier.OID_ECDSA_WITH_SHA256, parsed.getSignatureAlgorithm().getType().getInts()); + assertNull(parsed.getSignatureAlgorithm().getParameters()); + assertEquals(71, parsed.getSignature().getVal().length); + } + + @Test + void testParseFail() { + // Incorrect certificate tag + assertThrows(ParseException.class, () -> + new Certificate(new BytesReader(mutate(TestConstants.CERT_L1_ECC, 4, 0x30, 0x31)), false) + ); + // Incorrect signatureAlgorithm tag + assertThrows(ParseException.class, () -> + new Certificate(new BytesReader(mutate(TestConstants.CERT_L1_ECC, 349, 0x30, 0x31)), false) + ); + // Incorrect signature tag + assertThrows(ParseException.class, () -> + new Certificate(new BytesReader(mutate(TestConstants.CERT_L1_ECC, 361, 0x3, 0x5)), false) + ); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(TestConstants.CERT_V1, + new Certificate(new BytesReader(TestConstants.CERT_V1), false).encodeDER()); + assertArrayEquals(TestConstants.CERT_L1_ECC, + new Certificate(new BytesReader(TestConstants.CERT_L1_ECC), false).encodeDER()); + assertArrayEquals(TestConstants.CERT_L2_RSA, + new Certificate(new BytesReader(TestConstants.CERT_L2_RSA), false).encodeDER()); + } +} diff --git a/src/test/model/pki/cert/ExtensionTest.java b/src/test/model/pki/cert/ExtensionTest.java new file mode 100644 index 0000000..06561ba --- /dev/null +++ b/src/test/model/pki/cert/ExtensionTest.java @@ -0,0 +1,118 @@ +package model.pki.cert; + +import model.asn1.ASN1Object; +import model.asn1.Bool; +import model.asn1.ObjectIdentifier; +import model.asn1.OctetString; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ExtensionTest { + static final Byte[] EXT_SUBJECT_KEY_ID = new Byte[] { + 0x30, 0x1D, // SEQUENCE Extension + 0x06, 0x03, 0x55, 0x1D, 0x0E, // OID subjectKeyIdentifier + 0x04, 0x16, // OCTET STRING + 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66, + 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29, + -67, 0x6B, 0x49, 0x09, 0x61, -64 + }; + + static final Byte[] EXT_KEY_USAGE = new Byte[] { + 0x30, 0x0E, // SEQUENCE Extension + 0x06, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage + 0x01, 0x01, -1, // BOOLEAN critical + 0x04, 0x04, // OCTET STRING + 0x03, 0x02, 0x01, -122 + }; + + @Test + void testConstructor() throws ParseException { + final Extension ext = new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_BASIC_CONSTRAINTS), + new Bool(Bool.TAG, null, true), + new OctetString(OctetString.TAG, null, new Byte[]{ 0x30, 0x03, 0x01, 0x01, -1 })); + assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS, ext.getExtnId().getInts()); + assertTrue(ext.getCritical().getValue()); + assertArrayEquals(new Byte[]{ 0x30, 0x03, 0x01, 0x01, -1 }, ext.getExtnValue().getBytes()); + } + + @Test + void testParse() throws ParseException { + Extension parsed = new Extension(new BytesReader(EXT_SUBJECT_KEY_ID), false); + assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER, parsed.getExtnId().getInts()); + assertNull(parsed.getCritical()); + assertArrayEquals(new Byte[] { + 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66, + 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29, + -67, 0x6B, 0x49, 0x09, 0x61, -64 + }, parsed.getExtnValue().getBytes()); + + parsed = new Extension(new BytesReader(EXT_KEY_USAGE), false); + assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE, parsed.getExtnId().getInts()); + assertTrue(parsed.getCritical().getValue()); + assertArrayEquals(new Byte[] { + 0x03, 0x02, 0x01, -122 + }, parsed.getExtnValue().getBytes()); + } + + @Test + void testParseFail() throws ParseException { + // Too short (no ID) + assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{ + 0x30, 0x00 + }), false)); + // Wrong ID tag + assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{ + 0x30, 0x0E, // SEQUENCE Extension + 0x07, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage + 0x01, 0x01, -1, // BOOLEAN critical + 0x04, 0x04, // OCTET STRING + 0x03, 0x02, 0x01, -122 + }), false)); + // Wrong critical tag (neither bool nor sequence) + assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{ + 0x30, 0x0E, // SEQUENCE Extension + 0x06, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage + 0x05, 0x01, -1, // BOOLEAN critical + 0x04, 0x04, // OCTET STRING + 0x03, 0x02, 0x01, -122 + }), false)); + // Critical and wrong value tag + assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{ + 0x30, 0x0E, // SEQUENCE Extension + 0x06, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage + 0x01, 0x01, -1, // BOOLEAN critical + 0x09, 0x04, // OCTET STRING + 0x03, 0x02, 0x01, -122 + }), false)); + + // No critical and wrong value tag + assertThrows(ParseException.class, () -> new Extension(new BytesReader(new Byte[]{ + 0x30, 0x0B, // SEQUENCE Extension + 0x06, 0x03, 0x55, 0x1D, 0x0F, // OID keyUsage + 0x09, 0x04, // OCTET STRING + 0x03, 0x02, 0x01, -122 + }), false)); + } + + @Test + void testEncode() { + assertArrayEquals(EXT_SUBJECT_KEY_ID, new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER), + null, + new OctetString(OctetString.TAG, null, new Byte[] { + 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66, + 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29, + -67, 0x6B, 0x49, 0x09, 0x61, -64 + })).encodeDER()); + assertArrayEquals(EXT_KEY_USAGE, new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_KEY_USAGE), + new Bool(Bool.TAG, null, true), + new OctetString(OctetString.TAG, null, new Byte[] { + 0x03, 0x02, 0x01, -122 + })).encodeDER()); + } +} diff --git a/src/test/model/pki/cert/ExtensionsTest.java b/src/test/model/pki/cert/ExtensionsTest.java new file mode 100644 index 0000000..e50b3e6 --- /dev/null +++ b/src/test/model/pki/cert/ExtensionsTest.java @@ -0,0 +1,112 @@ +package model.pki.cert; + +import model.asn1.ASN1Object; +import model.asn1.Bool; +import model.asn1.ObjectIdentifier; +import model.asn1.OctetString; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +public class ExtensionsTest { + @Test + void testConstructor() { + final Extension ext1 = new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_BASIC_CONSTRAINTS), + new Bool(Bool.TAG, null, true), + new OctetString(OctetString.TAG, null, new Byte[]{0x30, 0x03, 0x01, 0x01, -1})); + final Extension ext2 = new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER), + null, + new OctetString(OctetString.TAG, null, new Byte[]{ + 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66, + 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29, + -67, 0x6B, 0x49, 0x09, 0x61, -64 + })); + final Extensions extensions = new Extensions(ASN1Object.TAG_SEQUENCE, null, new Extension[]{ + ext1, ext2 + }); + + assertEquals(2, extensions.getExtensions().length); + assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS, extensions.getExtensions()[0].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER, extensions.getExtensions()[1].getExtnId().getInts()); + } + + @Test + void testParse() throws ParseException { + final Extensions parsed = new Extensions(new BytesReader( + Stream.of(Arrays.asList(new Byte[]{0x30, + (byte) (ExtensionTest.EXT_KEY_USAGE.length + ExtensionTest.EXT_SUBJECT_KEY_ID.length)}), + Arrays.asList(ExtensionTest.EXT_KEY_USAGE), + Arrays.asList(ExtensionTest.EXT_SUBJECT_KEY_ID)) + .flatMap(Collection::stream) + .toArray(Byte[]::new)), false); + assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE, parsed.getExtensions()[0].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER, parsed.getExtensions()[1].getExtnId().getInts()); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> { + new Extensions(new BytesReader(new Byte[]{0x30, 0x1}), false); + }); + assertThrows(ParseException.class, () -> { + Byte[] bytes = + Stream.of(Arrays.asList(new Byte[]{0x30, + (byte) (ExtensionTest.EXT_KEY_USAGE.length + ExtensionTest.EXT_SUBJECT_KEY_ID.length)}), + Arrays.asList(ExtensionTest.EXT_KEY_USAGE), + Arrays.asList(ExtensionTest.EXT_SUBJECT_KEY_ID)) + .flatMap(Collection::stream) + .toArray(Byte[]::new); + assertEquals((byte) 0x30, bytes[2]); + bytes[2] = 0x31; + new Extensions(new BytesReader(bytes), false); + }); + assertThrows(ParseException.class, () -> { + Byte[] bytes = + Stream.of(Arrays.asList(new Byte[]{0x30, + (byte) (ExtensionTest.EXT_KEY_USAGE.length + ExtensionTest.EXT_SUBJECT_KEY_ID.length)}), + Arrays.asList(ExtensionTest.EXT_KEY_USAGE), + Arrays.asList(ExtensionTest.EXT_SUBJECT_KEY_ID)) + .flatMap(Collection::stream) + .toArray(Byte[]::new); + assertEquals((byte) 0x30, bytes[2 + ExtensionTest.EXT_KEY_USAGE.length]); + bytes[2 + ExtensionTest.EXT_KEY_USAGE.length] = 0x31; + new Extensions(new BytesReader(bytes), false); + }); + } + + @Test + void testEncode() { + assertArrayEquals( + Stream.of(Arrays.asList(new Byte[]{0x30, + (byte) (ExtensionTest.EXT_KEY_USAGE.length + + ExtensionTest.EXT_SUBJECT_KEY_ID.length)}), + Arrays.asList(ExtensionTest.EXT_SUBJECT_KEY_ID), + Arrays.asList(ExtensionTest.EXT_KEY_USAGE)) + .flatMap(Collection::stream) + .toArray(Byte[]::new), + new Extensions(ASN1Object.TAG_SEQUENCE, null, new Extension[]{ + new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER), + null, + new OctetString(OctetString.TAG, null, new Byte[]{ + 0x04, 0x14, -79, -62, -89, -127, 0x63, 0x66, + 0x4B, 0x72, 0x0A, -35, -3, 0x7D, 0x20, 0x29, + -67, 0x6B, 0x49, 0x09, 0x61, -64 + })), + new Extension(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_KEY_USAGE), + new Bool(Bool.TAG, null, true), + new OctetString(OctetString.TAG, null, new Byte[]{ + 0x03, 0x02, 0x01, -122 + })) + }).encodeDER()); + } +} diff --git a/src/test/model/pki/cert/TbsCertificateTest.java b/src/test/model/pki/cert/TbsCertificateTest.java new file mode 100644 index 0000000..ae92ace --- /dev/null +++ b/src/test/model/pki/cert/TbsCertificateTest.java @@ -0,0 +1,184 @@ +package model.pki.cert; + +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.TestConstants; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Arrays; + +import static model.TestConstants.mutate; +import static org.junit.jupiter.api.Assertions.*; + +public class TbsCertificateTest { + @Test + void testConstructor() { + assertEquals(TbsCertificate.VERSION_V3, TestConstants.CERT_GENERATED.getVersion().getLong()); + assertEquals(100, TestConstants.CERT_GENERATED.getSerialNumber().getLong()); + assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, TestConstants.CERT_GENERATED.getSignature().getType().getInts()); + assertEquals("CN=Test CA,C=CA", TestConstants.CERT_GENERATED.getIssuer().toString()); + assertEquals(TestConstants.NOW, TestConstants.CERT_GENERATED.getValidity().getNotBefore().getTimestamp()); + assertEquals(TestConstants.NOW.plusYears(1), + TestConstants.CERT_GENERATED.getValidity().getNotAfter().getTimestamp()); + assertEquals("CN=Yuuta Liang,C=CA", TestConstants.CERT_GENERATED.getSubject().toString()); + assertArrayEquals(ObjectIdentifier.OID_EC_PUBLIC_KEY, + TestConstants.CERT_GENERATED.getSubjectPublicKeyInfo().getAlgorithm().getType().getInts()); + assertEquals(2, TestConstants.CERT_GENERATED.getExtensions().getExtensions().length); + assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS, + TestConstants.CERT_GENERATED.getExtensions().getExtensions()[0].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE, + TestConstants.CERT_GENERATED.getExtensions().getExtensions()[1].getExtnId().getInts()); + } + + @Test + void testParse() throws ParseException { + TbsCertificate parsed = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_L1_ECC)), + false); + assertEquals(TbsCertificate.VERSION_V3, parsed.getVersion().getLong()); + assertEquals(0, parsed.getSerialNumber().getValue() + .compareTo(new BigInteger("644983544608556543477205958886697401602227090424"))); + assertArrayEquals(ObjectIdentifier.OID_ECDSA_WITH_SHA256, + parsed.getSignature().getType().getInts()); + assertNull(parsed.getSignature().getParameters()); + assertEquals("CN=Yuuta Root CA,C=CA", parsed.getIssuer().toString()); + assertEquals(ZonedDateTime.of(2023, 6, 23, + 2, 50, 46, 0, ZoneId.of("UTC")), + parsed.getValidity().getNotBefore().getTimestamp()); + assertEquals(ZonedDateTime.of(2048, 6, 23, + 2, 50, 46, 0, ZoneId.of("UTC")), + parsed.getValidity().getNotAfter().getTimestamp()); + assertEquals("CN=Yuuta Root CA,C=CA", parsed.getSubject().toString()); + assertArrayEquals(ObjectIdentifier.OID_EC_PUBLIC_KEY, + parsed.getSubjectPublicKeyInfo().getAlgorithm().getType().getInts()); + assertArrayEquals(ObjectIdentifier.OID_PRIME256_V1, + ((ObjectIdentifier) parsed.getSubjectPublicKeyInfo().getAlgorithm().getParameters()).getInts()); + assertEquals(4, parsed.getExtensions().getExtensions().length); + assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER, + parsed.getExtensions().getExtensions()[0].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_AUTHORITY_KEY_IDENTIFIER, + parsed.getExtensions().getExtensions()[1].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS, + parsed.getExtensions().getExtensions()[2].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE, + parsed.getExtensions().getExtensions()[3].getExtnId().getInts()); + + parsed = new TbsCertificate( + new BytesReader(trimToTbs(TestConstants.CERT_L2_RSA)), + false); + assertEquals(TbsCertificate.VERSION_V3, parsed.getVersion().getLong()); + assertEquals(0, parsed.getSerialNumber().getValue() + .compareTo(new BigInteger("354327098948136693059815576591331472151989570311"))); + assertArrayEquals(ObjectIdentifier.OID_ECDSA_WITH_SHA512, + parsed.getSignature().getType().getInts()); + assertNull(parsed.getSignature().getParameters()); + assertEquals("CN=Yuuta Root CA,C=CA", parsed.getIssuer().toString()); + assertEquals(ZonedDateTime.of(2023, 6, 24, + 0, 15, 22, 0, ZoneId.of("UTC")), + parsed.getValidity().getNotBefore().getTimestamp()); + assertEquals(ZonedDateTime.of(2033, 6, 21, + 0, 15, 22, 0, ZoneId.of("UTC")), + parsed.getValidity().getNotAfter().getTimestamp()); + assertEquals("DC=MOE,DC=YUUTA,DC=AD,CN=Yuuta Home Issuing CA", parsed.getSubject().toString()); + assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, + parsed.getSubjectPublicKeyInfo().getAlgorithm().getType().getInts()); + assertEquals(Null.TAG.getNumber(), + parsed.getSubjectPublicKeyInfo().getAlgorithm().getParameters().getTag().getNumber()); + assertEquals(526, parsed.getSubjectPublicKeyInfo().getSubjectPublicKey().getVal().length); + assertEquals(6, parsed.getExtensions().getExtensions().length); + assertArrayEquals(ObjectIdentifier.OID_SUBJECT_KEY_IDENTIFIER, + parsed.getExtensions().getExtensions()[0].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_AUTHORITY_KEY_IDENTIFIER, + parsed.getExtensions().getExtensions()[1].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_BASIC_CONSTRAINTS, + parsed.getExtensions().getExtensions()[2].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_KEY_USAGE, + parsed.getExtensions().getExtensions()[3].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_CRL_DISTRIBUTION_POINTS, + parsed.getExtensions().getExtensions()[4].getExtnId().getInts()); + assertArrayEquals(ObjectIdentifier.OID_AUTHORITY_INFO_ACCESS, + parsed.getExtensions().getExtensions()[5].getExtnId().getInts()); + + parsed = new TbsCertificate( + new BytesReader(Arrays.stream(TestConstants.CERT_V1).skip(4).toArray(Byte[]::new)), + false); + assertNull(parsed.getVersion()); + assertNull(parsed.getExtensions()); + } + + @Test + void testParseFail() throws ParseException { + final Byte[] in = trimToTbs(TestConstants.CERT_L2_RSA); + // Wrong version parent tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 4, -96, 2)), false)); + // Wrong version inner tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 6, 0x2, 3)), false)); + // Wrong serial number tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 9, 0x2, 3)), false)); + // Wrong signature tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 31, 0x30, 3)), false)); + // Wrong issuer tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 43, 0x30, 0x31)), false)); + // Wrong validity tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 82, 0x30, 0x31)), false)); + // Wrong subject tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 114, 0x30, 0x31)), false)); + // Wrong subject public key info tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 212, 0x30, 0x31)), false)); + // Wrong extensions parent tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 762, -93, 0x31)), false)); + // Wrong extensions inner tag + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 765, 0x30, 0x31)), false)); + // Extensions exist, but wrong version + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 8, 0x2, TbsCertificate.VERSION_V2)), + false)); + // Totally wrong version + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(mutate(in, 8, 0x2, TbsCertificate.VERSION_V3 + 1)), + false)); + + // Extensions exist, but no version + final TbsCertificate certV1 = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_V1)), false); + final TbsCertificate certV3 = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_L2_RSA)), false); + Byte[] wrongCert = new TbsCertificate(ASN1Object.TAG_SEQUENCE, null, + null, + certV1.getSerialNumber(), + certV1.getSignature(), + certV1.getIssuer(), + certV1.getValidity(), + certV1.getSubject(), + certV1.getSubjectPublicKeyInfo(), + certV3.getExtensions()) + .encodeDER(); + assertThrows(ParseException.class, () -> + new TbsCertificate(new BytesReader(wrongCert), false)); + } + + @Test + void testEncode() throws ParseException { + Byte[] in = trimToTbs(TestConstants.CERT_L1_ECC); + assertArrayEquals(Arrays.copyOfRange(in, 0, 345), new TbsCertificate(new BytesReader(in), false).encodeDER()); + in = trimToTbs(TestConstants.CERT_L2_RSA); + assertArrayEquals(Arrays.copyOfRange(in, 0, 989), new TbsCertificate(new BytesReader(in), false).encodeDER()); + in = trimToTbs(TestConstants.CERT_V1); + assertArrayEquals(Arrays.copyOfRange(in, 0, 583), new TbsCertificate(new BytesReader(in), false).encodeDER()); + } + + private static Byte[] trimToTbs(Byte[] in) { + return Arrays.stream(in).skip(4).toArray(Byte[]::new); + } +} diff --git a/src/test/model/pki/cert/ValidityTest.java b/src/test/model/pki/cert/ValidityTest.java new file mode 100644 index 0000000..eba5092 --- /dev/null +++ b/src/test/model/pki/cert/ValidityTest.java @@ -0,0 +1,118 @@ +package model.pki.cert; + +import jdk.jshell.EvalException; +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Stream; + +import static model.TestConstants.combine; +import static model.TestConstants.mutate; +import static org.junit.jupiter.api.Assertions.*; + +public class ValidityTest { + private ZonedDateTime now; + + @BeforeEach + void setup() { + now = ZonedDateTime.now(ZoneId.of("UTC")).withNano(0); + } + + @Test + void testConstructor() { + final ASN1Time time = new GeneralizedTime(GeneralizedTime.TAG, null, now); + assertEquals(time.getTimestamp(), new Validity(ASN1Object.TAG_SEQUENCE, null, time, time) + .getNotBefore().getTimestamp()); + assertEquals(time.getTimestamp(), new Validity(ASN1Object.TAG_SEQUENCE, null, time, time) + .getNotAfter().getTimestamp()); + } + + @Test + void testParse() throws ParseException { + final ASN1Time utc = new UtcTime(UtcTime.TAG, null, now); + final ASN1Time gen = new GeneralizedTime(GeneralizedTime.TAG, null, now); + final Byte[] utcBytes = utc.encodeDER(); + final Byte[] genBytes = gen.encodeDER(); + + // UTC, Generalized + Validity parsed = new Validity(new BytesReader(combine((byte) ASN1Object.TAG_SEQUENCE.getNumber(), utcBytes, + genBytes)), false); + assertEquals(UtcTime.TAG.getNumber(), parsed.getNotBefore().getTag().getNumber()); + assertEquals(now, parsed.getNotBefore().getTimestamp()); + assertEquals(GeneralizedTime.TAG.getNumber(), parsed.getNotAfter().getTag().getNumber()); + assertEquals(now, parsed.getNotAfter().getTimestamp()); + + // UTC, UTC + parsed = new Validity(new BytesReader(combine((byte) ASN1Object.TAG_SEQUENCE.getNumber(), utcBytes, utcBytes)), + false); + assertEquals(UtcTime.TAG.getNumber(), parsed.getNotBefore().getTag().getNumber()); + assertEquals(now, parsed.getNotBefore().getTimestamp()); + assertEquals(UtcTime.TAG.getNumber(), parsed.getNotAfter().getTag().getNumber()); + assertEquals(now, parsed.getNotAfter().getTimestamp()); + + // Generalized, Generalized + parsed = new Validity(new BytesReader(combine((byte) ASN1Object.TAG_SEQUENCE.getNumber(), genBytes, genBytes)), + false); + assertEquals(GeneralizedTime.TAG.getNumber(), parsed.getNotBefore().getTag().getNumber()); + assertEquals(now, parsed.getNotBefore().getTimestamp()); + assertEquals(GeneralizedTime.TAG.getNumber(), parsed.getNotAfter().getTag().getNumber()); + assertEquals(now, parsed.getNotAfter().getTimestamp()); + + // Generalized, UTC + parsed = new Validity(new BytesReader(combine((byte) ASN1Object.TAG_SEQUENCE.getNumber(), genBytes, utcBytes)), + false); + assertEquals(GeneralizedTime.TAG.getNumber(), parsed.getNotBefore().getTag().getNumber()); + assertEquals(now, parsed.getNotBefore().getTimestamp()); + assertEquals(UtcTime.TAG.getNumber(), parsed.getNotAfter().getTag().getNumber()); + assertEquals(now, parsed.getNotAfter().getTimestamp()); + } + + @Test + void testParseFail() { + final ASN1Time utc = new UtcTime(UtcTime.TAG, null, now); + final Byte[] utcBytes = utc.encodeDER(); + + // Too short + assertThrows(ParseException.class, () -> + new Validity(new BytesReader(new Byte[] { + 0x30, 0x0 + }), false)); + assertThrows(ParseException.class, () -> { + new Validity(new BytesReader(combine((byte) 0x30, utcBytes)), false); + }); + + // Illegal notBefore tag + assertThrows(ParseException.class, () -> { + new Validity(new BytesReader(mutate(combine((byte) 0x30, utcBytes, utcBytes), 2, + UtcTime.TAG.getNumber(), 0x2)), false); + }); + // Illegal notAfter tag + assertThrows(ParseException.class, () -> { + new Validity(new BytesReader(mutate(combine((byte) 0x30, utcBytes, utcBytes), utcBytes.length + 2, + UtcTime.TAG.getNumber(), 0x2)), false); + }); + } + + @Test + void testEncode() { + final ASN1Time utc = new UtcTime(UtcTime.TAG, null, now); + final ASN1Time gen = new GeneralizedTime(GeneralizedTime.TAG, null, now); + final Byte[] utcBytes = utc.encodeDER(); + final Byte[] genBytes = gen.encodeDER(); + + assertArrayEquals(Stream.of(Arrays.asList(new Byte[]{ 0x30, (byte) (utcBytes.length + genBytes.length) }), + Arrays.asList(utcBytes), + Arrays.asList(genBytes)) + .flatMap(Collection::stream) + .toArray(Byte[]::new), + new Validity(ASN1Object.TAG_SEQUENCE, null, utc, gen) + .encodeDER()); + } +} diff --git a/src/test/model/pki/crl/CertificateListContentTest.java b/src/test/model/pki/crl/CertificateListContentTest.java new file mode 100644 index 0000000..ec18629 --- /dev/null +++ b/src/test/model/pki/crl/CertificateListContentTest.java @@ -0,0 +1,37 @@ +package model.pki.crl; + +import model.TestConstants; +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.pki.AlgorithmIdentifier; +import model.x501.AttributeTypeAndValue; +import model.x501.Name; +import model.x501.RelativeDistinguishedName; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static model.TestConstants.*; +import static org.junit.jupiter.api.Assertions.*; + +public class CertificateListContentTest { + @Test + void testConstructor() { + assertEquals(1, CRL_CONTENT_1.getVersion().getLong()); + assertEquals("CN=Test CA", CRL_CONTENT_1.getIssuer().toString()); + assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION, + CRL_CONTENT_1.getSignature().getType().getInts()); + assertEquals(GeneralizedTime.TAG.getNumber(), + CRL_CONTENT_1.getThisUpdate().getTag().getNumber()); + assertNull(CRL_CONTENT_1.getNextUpdate()); + assertEquals(2, CRL_CONTENT_1.getRevokedCertificates().length); + } + + @Test + void testEncode() { + assertArrayEquals(CRL_CONTENT_1_DER, CRL_CONTENT_1.encodeDER()); + assertArrayEquals(CRL_CONTENT_2_DER, CRL_CONTENT_2.encodeDER()); + } +} diff --git a/src/test/model/pki/crl/CertificateListTest.java b/src/test/model/pki/crl/CertificateListTest.java new file mode 100644 index 0000000..0f4f06c --- /dev/null +++ b/src/test/model/pki/crl/CertificateListTest.java @@ -0,0 +1,52 @@ +package model.pki.crl; + +import model.TestConstants; +import model.asn1.ASN1Object; +import model.asn1.BitString; +import model.asn1.Null; +import model.asn1.ObjectIdentifier; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.pki.AlgorithmIdentifier; +import model.pki.cert.Certificate; +import model.pki.cert.TbsCertificate; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static model.TestConstants.combine; +import static model.TestConstants.mutate; +import static org.junit.jupiter.api.Assertions.*; + +public class CertificateListTest { + private CertificateList crl; + + @BeforeEach + void setup() { + crl = new CertificateList(ASN1Object.TAG_SEQUENCE, null, + TestConstants.CRL_CONTENT_1, + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_RSA_ENCRYPTION), + new Null(Null.TAG, null)), + new BitString(BitString.TAG, null, 0, new Byte[]{ 1, 2, 3 })); + } + + @Test + void testConstructor() { + assertEquals(2, crl.getCrl().getRevokedCertificates().length); + assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, + crl.getSignatureAlgorithm().getType().getInts()); + assertArrayEquals(new Byte[]{ 1, 2, 3 }, + crl.getSignature().getConvertedVal()); + } + + @Test + void testEncode() { + assertArrayEquals(combine((byte) 0x30, + TestConstants.CRL_CONTENT_1_DER, + new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_RSA_ENCRYPTION), + new Null(Null.TAG, null)).encodeDER(), + new BitString(BitString.TAG, null, 0, new Byte[]{ 1, 2, 3 }).encodeDER()), + crl.encodeDER()); + } +} diff --git a/src/test/model/pki/crl/RevokedCertificateTest.java b/src/test/model/pki/crl/RevokedCertificateTest.java new file mode 100644 index 0000000..659e421 --- /dev/null +++ b/src/test/model/pki/crl/RevokedCertificateTest.java @@ -0,0 +1,25 @@ +package model.pki.crl; + +import model.asn1.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static model.TestConstants.*; +import static org.junit.jupiter.api.Assertions.*; + +public class RevokedCertificateTest { + @Test + void testConstructor() { + assertEquals(123, REVOKED_CESSATION.getSerialNumber().getLong()); + assertEquals(UtcTime.TAG.getNumber(), REVOKED_CESSATION.getRevocationDate().getTag().getNumber()); + assertEquals(Reason.CESSATION_OF_OPERATION, REVOKED_CESSATION.getReason()); + } + + @Test + void testEncode() { + assertArrayEquals(REVOKED_KEY_COMPROMISE_DER, REVOKED_KEY_COMPROMISE.encodeDER()); + } +} diff --git a/src/test/model/x501/AttributeTypeAndValueTest.java b/src/test/model/x501/AttributeTypeAndValueTest.java new file mode 100644 index 0000000..ea9c17e --- /dev/null +++ b/src/test/model/x501/AttributeTypeAndValueTest.java @@ -0,0 +1,90 @@ +package model.x501; + +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import org.junit.jupiter.api.Test; + +import static model.asn1.ObjectIdentifier.*; +import static org.junit.jupiter.api.Assertions.*; + +public class AttributeTypeAndValueTest { + @Test + void testConstructor() throws ParseException { + assertArrayEquals(OID_OU, + new AttributeTypeAndValue( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_OU), + new Null(Null.TAG, null)) + .getType().getInts()); + assertEquals("123", + new AttributeTypeAndValue( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_OU), + new PrintableString(PrintableString.TAG, null, "123")) + .getValue().toString()); + } + + @Test + void testParse() throws ParseException { + // C = IT + assertArrayEquals(OID_C, new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54 + }), false).getType().getInts()); + assertEquals("IT", ((PrintableString) new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54 + }), false).getValue()).getString()); + + // CN = Test ed25519 + assertArrayEquals(OID_CN, new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, + 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64, + 0x32, 0x35, 0x35, 0x31, 0x39 + }), false).getType().getInts()); + assertEquals("Test ed25519", ((ASN1String) new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, + 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64, + 0x32, 0x35, 0x35, 0x31, 0x39 + }), false).getValue()).getString()); + } + + @Test + void testParseFail() { + // No type + assertThrows(ParseException.class, () -> new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x0 + }), false)); + // No value + assertThrows(ParseException.class, () -> new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x5, 0x6, 0x3, 0x55, 0x4, 0x6 + }), false)); + // Incorrect type tag (should be OID) + assertThrows(ParseException.class, () -> new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x9, 0x7, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54 + }), false)); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(new Byte[]{ + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, + 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64, + 0x32, 0x35, 0x35, 0x31, 0x39 + }, new AttributeTypeAndValue(ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_CN), + new UTF8String(UTF8String.TAG, null, "Test ed25519")) + .encodeDER()); + } + + @Test + void testToString() throws ParseException { + assertEquals("CN=Test ed25519", new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, + 0x0C, 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64, + 0x32, 0x35, 0x35, 0x31, 0x39 + }), false).toString()); + assertEquals("C=IT", new AttributeTypeAndValue(new BytesReader(new Byte[]{ + 0x30, 0x9, 0x6, 0x3, 0x55, 0x4, 0x6, 0x13, 0x2, 0x49, 0x54 + }), false).toString()); + } +} diff --git a/src/test/model/x501/NameTest.java b/src/test/model/x501/NameTest.java new file mode 100644 index 0000000..c649798 --- /dev/null +++ b/src/test/model/x501/NameTest.java @@ -0,0 +1,167 @@ +package model.x501; + +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.TestConstants; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class NameTest { + @Test + void testConstructor() { + assertEquals("users", + TestConstants.NAME_1.getRdnSequence()[1].getArray()[0].getValue().toString()); + assertEquals("CN", + TestConstants.NAME_2.getRdnSequence()[2].getArray()[0].getValue().toString()); + } + + @Test + void testParse() throws ParseException { + assertEquals("CA", new Name(new BytesReader(new Byte[]{ + 0x30, 45, // Name + + 0x31, 14, // RDN[0] + 0x30, 12, // KV[0] + 0x6, 3, 0x55, 0x04, 0x03, // CN + 0x13, 5, 'y', 'u', 'u', 't', 'a', // yuuta + + 0x31, 14, // RDN[1] + 0x30, 12, // KV[0] + 0x6, 3, 0x55, 0x04, 0xB, // OU + 0x13, 5, 'u', 's', 'e', 'r', 's', // users + + 0x31, 11, // RDN[2] + 0x30, 9, // KV[0] + 0x6, 3, 0x55, 0x04, 0x6, // C + 0x13, 2, 'C', 'A' // CA + }), false).getRdnSequence()[2].getArray()[0].getValue().toString()); + + assertEquals("SN=Qwq", new Name(new BytesReader(new Byte[]{ + 0x30, 38, // Name + + 0x31, 12, // RDN[0] + 0x30, 10, // KV[0] + 0x6, 3, 0x55, 0x04, 0x04, // CN + 0x13, 3, 'Q', 'w', 'q', // Qwq + + 0x31, 9, // RDN[1] + 0x30, 7, // KV[0] + 0x6, 3, 0x55, 0x04, 0xA, // O + 0x13, 2, 'I', 'T', // IT + + 0x31, 11, // RDN[2] + 0x30, 9, // KV[0] + 0x6, 3, 0x55, 0x04, 0x6, // C + 0x13, 2, 'C', 'N' // CN + }), false).getRdnSequence()[0].toString()); + } + + @Test + void testParseFail() { + assertThrows(ParseException.class, () -> new Name(new BytesReader(new Byte[]{ + 0x30, 38, // Name + + // Wrong tag here + 0x30, 12, // RDN[0] + 0x30, 10, // KV[0] + 0x6, 3, 0x55, 0x04, 0x04, // CN + 0x13, 3, 'Q', 'w', 'q', // Qwq + + 0x31, 9, // RDN[1] + 0x30, 7, // KV[0] + 0x6, 3, 0x55, 0x04, 0xA, // O + 0x13, 2, 'I', 'T', // IT + + 0x31, 11, // RDN[2] + 0x30, 9, // KV[0] + 0x6, 3, 0x55, 0x04, 0x6, // C + 0x13, 2, 'C', 'N' // CN + }), false)); + assertThrows(ParseException.class, () -> new Name(new BytesReader(new Byte[]{ + 0x30, 38, // Name + + 0x31, 12, // RDN[0] + 0x30, 10, // KV[0] + 0x6, 3, 0x55, 0x04, 0x04, // CN + 0x13, 3, 'Q', 'w', 'q', // Qwq + + 0x31, 9, // RDN[1] + // Wrong tag here + 0x31, 7, // KV[0] + 0x6, 3, 0x55, 0x04, 0xA, // O + 0x13, 2, 'I', 'T', // IT + + 0x31, 11, // RDN[2] + 0x30, 9, // KV[0] + 0x6, 3, 0x55, 0x04, 0x6, // C + 0x13, 2, 'C', 'N' // CN + }), false)); + assertThrows(ParseException.class, () -> new Name(new BytesReader(new Byte[]{ + 0x30, 38, // Name + + 0x31, 12, // RDN[0] + 0x30, 10, // KV[0] + 0x6, 3, 0x55, 0x04, 0x04, // CN + 0x13, 3, 'Q', 'w', 'q', // Qwq + + 0x31, 9, // RDN[1] + 0x30, 7, // KV[0] + 0x6, 3, 0x55, 0x04, 0xA, // O + 0x13, 2, 'I', 'T', // IT + + // Wrong tag here + 0x30, 11, // RDN[2] + 0x30, 9, // KV[0] + 0x6, 3, 0x55, 0x04, 0x6, // C + 0x13, 2, 'C', 'N' // CN + }), false)); + } + + @Test + void testEncode() { + assertArrayEquals(new Byte[]{ + 0x30, 45, // Name + + 0x31, 14, // RDN[0] + 0x30, 12, // KV[0] + 0x6, 3, 0x55, 0x04, 0x03, // CN + 0x13, 5, 'y', 'u', 'u', 't', 'a', // yuuta + + 0x31, 14, // RDN[1] + 0x30, 12, // KV[0] + 0x6, 3, 0x55, 0x04, 0xB, // OU + 0x13, 5, 'u', 's', 'e', 'r', 's', // users + + 0x31, 11, // RDN[2] + 0x30, 9, // KV[0] + 0x6, 3, 0x55, 0x04, 0x6, // C + 0x13, 2, 'C', 'A' // CA + }, TestConstants.NAME_1.encodeDER()); + + assertArrayEquals(new Byte[]{ + 0x30, 40, // Name + + 0x31, 12, // RDN[0] + 0x30, 10, // KV[0] + 0x6, 3, 0x55, 0x04, 0x04, // CN + 0x13, 3, 'Q', 'w', 'q', // Qwq + + 0x31, 11, // RDN[1] + 0x30, 9, // KV[0] + 0x6, 3, 0x55, 0x04, 0xA, // O + 0x13, 2, 'I', 'T', // IT + + 0x31, 11, // RDN[2] + 0x30, 9, // KV[0] + 0x6, 3, 0x55, 0x04, 0x6, // C + 0x13, 2, 'C', 'N' // CN + }, TestConstants.NAME_2.encodeDER()); + } + + @Test + void testToString() { + assertEquals("CN=yuuta,OU=users,C=CA", TestConstants.NAME_1.toString()); + assertEquals("SN=Qwq,O=IT,C=CN", TestConstants.NAME_2.toString()); + } +} diff --git a/src/test/model/x501/RelativeDistinguishedNameTest.java b/src/test/model/x501/RelativeDistinguishedNameTest.java new file mode 100644 index 0000000..d066010 --- /dev/null +++ b/src/test/model/x501/RelativeDistinguishedNameTest.java @@ -0,0 +1,95 @@ +package model.x501; + +import model.asn1.*; +import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; +import model.TestConstants; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +import static model.asn1.ASN1Object.TAG_SET; +import static model.asn1.ObjectIdentifier.OID_C; +import static model.asn1.ObjectIdentifier.OID_OU; +import static org.junit.jupiter.api.Assertions.*; + +public class RelativeDistinguishedNameTest { + @Test + void testConstructor() throws ParseException { + assertArrayEquals(OID_OU, + new RelativeDistinguishedName(TAG_SET, null, + new AttributeTypeAndValue[]{ + new AttributeTypeAndValue( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_OU), + new Null(Null.TAG, null)) + }).getArray()[0].getType().getInts()); + assertEquals("123", + new RelativeDistinguishedName(TAG_SET, null, + new AttributeTypeAndValue[]{ + new AttributeTypeAndValue( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_OU), + new PrintableString(PrintableString.TAG, null, "123")) + }).getArray()[0].getValue().toString()); + } + + @Test + void testParse() throws ParseException { + assertEquals(1, TestConstants.L_MILANO.getArray().length); + assertEquals("Milano", TestConstants.L_MILANO.getArray()[0].getValue().toString()); + + assertEquals(2, TestConstants.L_MILANO_CN_TEST_ED25519.getArray().length); + assertEquals("Test ed25519", + TestConstants.L_MILANO_CN_TEST_ED25519.getArray()[0].getValue().toString()); + } + + @Test + void testParseFail() { + // Invalid child tag + assertThrows(ParseException.class, () -> + new RelativeDistinguishedName(new BytesReader(new Byte[] { + 0x31, 0x0F, + 0x31, 0x0D, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0C, + 0x06, 0x4D, 0x69, 0x6C, 0x61, 0x6E, 0x6F + }), false)); + assertThrows(ParseException.class, () -> + new RelativeDistinguishedName(new BytesReader(new Byte[] { + 0x31, 0x23, + // CN + 0x30, 0x13, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0C, + 0x54, 0x65, 0x73, 0x74, 0x20, 0x65, 0x64, 0x32, + 0x35, 0x35, 0x31, 0x39, + // L + 0x31, 0x0D, 0x06, 0x03, 0x55, 0x04, 0x07, 0x0C, + 0x06, 0x4D, 0x69, 0x6C, 0x61, 0x6E, 0x6F + }), false)); + } + + @Test + void testEncode() throws ParseException { + assertArrayEquals(TestConstants.combine((byte) 0x31, + TestConstants.CN_TEST_ED25519_DER, + TestConstants.L_MILANO_DER), + TestConstants.L_MILANO_CN_TEST_ED25519.encodeDER()); + assertArrayEquals(new Byte[]{ + 0x31, 15, + 0x30, 13, + 0x06, 3, 0x55, 0x04, 0x06, + 0x13, 6, '1', '2', '3', '1', '2', '3' + + }, new RelativeDistinguishedName(TAG_SET, null, + new AttributeTypeAndValue[]{ + new AttributeTypeAndValue( + ASN1Object.TAG_SEQUENCE, null, + new ObjectIdentifier(ObjectIdentifier.TAG, null, OID_C), + new PrintableString(PrintableString.TAG, null, "123123"))}) + .encodeDER()); + } + + @Test + void testToString() { + assertEquals("L=Milano", TestConstants.L_MILANO.toString()); + assertEquals("CN=Test ed25519+L=Milano", TestConstants.L_MILANO_CN_TEST_ED25519.toString()); + } +} -- cgit v1.2.3