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/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 ++++++++++++ 5 files changed, 454 insertions(+) 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 (limited to 'src/main/model/csr') 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; + } +} -- cgit v1.2.3