aboutsummaryrefslogtreecommitdiff
path: root/src/main/model/pki
diff options
context:
space:
mode:
authorYuuta Liang <yuutaw@students.cs.ubc.ca>2023-10-12 12:10:33 +0800
committerYuuta Liang <yuutaw@students.cs.ubc.ca>2023-10-12 12:10:33 +0800
commitd342a45d98c4795b3a3fe1aaef5236ad4a782b55 (patch)
treef4ebc0ad962b138d9371413fcc71c97a559df506 /src/main/model/pki
parente60c9c76243cfe0a408af98dc60bedb973e815db (diff)
downloadjca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar
jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar.gz
jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.tar.bz2
jca-d342a45d98c4795b3a3fe1aaef5236ad4a782b55.zip
Implement data structures from X.680, X.501, X.509, and PKCS#10, with X.690 encoding / decoding support
The implementation took four days, and it is still a little bit rough. Updated version should arrive soon. Signed-off-by: Yuuta Liang <yuutaw@students.cs.ubc.ca>
Diffstat (limited to 'src/main/model/pki')
-rw-r--r--src/main/model/pki/AlgorithmIdentifier.java105
-rw-r--r--src/main/model/pki/SubjectPublicKeyInfo.java83
-rw-r--r--src/main/model/pki/cert/Certificate.java127
-rw-r--r--src/main/model/pki/cert/Extension.java113
-rw-r--r--src/main/model/pki/cert/Extensions.java67
-rw-r--r--src/main/model/pki/cert/TbsCertificate.java263
-rw-r--r--src/main/model/pki/cert/Validity.java95
-rw-r--r--src/main/model/pki/crl/CertificateList.java76
-rw-r--r--src/main/model/pki/crl/CertificateListContent.java106
-rw-r--r--src/main/model/pki/crl/Reason.java27
-rw-r--r--src/main/model/pki/crl/RevokedCertificate.java72
11 files changed, 1134 insertions, 0 deletions
diff --git a/src/main/model/pki/AlgorithmIdentifier.java b/src/main/model/pki/AlgorithmIdentifier.java
new file mode 100644
index 0000000..421aa5a
--- /dev/null
+++ b/src/main/model/pki/AlgorithmIdentifier.java
@@ -0,0 +1,105 @@
+package model.pki;
+
+import model.asn1.ASN1Object;
+import model.asn1.ObjectIdentifier;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+/**
+ * Implements the following:
+ * <pre>
+ * AttributeTypeAndValue ::= SEQUENCE {
+ * type ATTRIBUTE.&id({SupportedAttributes}),
+ * value ATTRIBUTE.&Type({SupportedAttributes}{@type}) OPTIONAL,
+ * ... }
+ * </pre>
+ */
+public class AlgorithmIdentifier extends ASN1Object {
+ /**
+ * The type of that attribute. For example, <pre>1.2.840.113549.1.1.11</pre> is sha256WithRSAEncryption.
+ */
+ private final ObjectIdentifier type;
+
+ /**
+ * Additional parameters for that algorithm. Optional, and could be ASN.1 NULL or Java null (absent).
+ * According to RFC8017$A.2, it should be NULL for a number of algorithms:
+ * <pre>
+ * PKCS1Algorithms ALGORITHM-IDENTIFIER ::= {
+ * { OID rsaEncryption PARAMETERS NULL } |
+ * { OID md2WithRSAEncryption PARAMETERS NULL } |
+ * { OID md5WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha1WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha224WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha256WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha384WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha512WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha512-224WithRSAEncryption PARAMETERS NULL } |
+ * { OID sha512-256WithRSAEncryption PARAMETERS NULL } |
+ * { OID id-RSAES-OAEP PARAMETERS RSAES-OAEP-params } |
+ * PKCS1PSourceAlgorithms |
+ * { OID id-RSASSA-PSS PARAMETERS RSASSA-PSS-params },
+ * ... -- Allows for future expansion --
+ * }
+ * </pre>
+ */
+ private final ASN1Object parameters;
+
+ /**
+ * EFFECT: Init the object with tag, parentTag, type, and parameters. For tag and parentTag, see {@link ASN1Object}.
+ * REQUIRES: The values must match the type. Type tag should be UNIVERSAL OID. Parameters nullable.
+ */
+ public AlgorithmIdentifier(Tag tag, Tag parentTag,
+ ObjectIdentifier type, ASN1Object parameters) {
+ super(tag, parentTag);
+ this.type = type;
+ this.parameters = parameters;
+ }
+
+ /**
+ * EFFECTS: Parse input DER. Parameters are not checked against the type.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public AlgorithmIdentifier(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ int i = encoded.getIndex();
+ this.type = new ObjectIdentifier(encoded, false);
+ this.type.getTag().enforce(ObjectIdentifier.TAG);
+ i = encoded.getIndex() - i;
+
+ if (getLength() > i) {
+ this.parameters = ASN1Object.parse(encoded, false);
+ } else {
+ this.parameters = null;
+ }
+ }
+
+ /**
+ * EFFECTS: Encode the fields into DER, in the order.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(type.encodeDER()),
+ parameters == null ? Collections.<Byte>emptyList() : Arrays.asList(parameters.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public ObjectIdentifier getType() {
+ return type;
+ }
+
+ public ASN1Object getParameters() {
+ return parameters;
+ }
+}
diff --git a/src/main/model/pki/SubjectPublicKeyInfo.java b/src/main/model/pki/SubjectPublicKeyInfo.java
new file mode 100644
index 0000000..ac72055
--- /dev/null
+++ b/src/main/model/pki/SubjectPublicKeyInfo.java
@@ -0,0 +1,83 @@
+package model.pki;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents the following ASN.1 structure:
+ * <pre>
+ * SubjectPublicKeyInfo ::= SEQUENCE {
+ * algorithm AlgorithmIdentifier{{SupportedAlgorithms}},
+ * subjectPublicKey BIT STRING,
+ * ... }
+ * </pre>
+ * It represents the public key of a subject, in a certificate.
+ */
+public class SubjectPublicKeyInfo extends ASN1Object {
+ /**
+ * The algorithm used.
+ */
+ private final AlgorithmIdentifier algorithm;
+
+ /**
+ * The public key.
+ */
+ private final BitString subjectPublicKey;
+
+ /**
+ * EFFECTS: Init with tags, algorithm, subjectPublicKey. For tags, see {@link ASN1Object}.
+ * REQUIRES: The public key should be a valid $algorithm key. Algorithm and publicKey should have default UNIVERSAL
+ * tags (SEQUENCE and BIT STRING).
+ */
+ public SubjectPublicKeyInfo(Tag tag, Tag parentTag,
+ final AlgorithmIdentifier algorithm,
+ final BitString subjectPublicKey) {
+ super(tag, parentTag);
+ this.algorithm = algorithm;
+ this.subjectPublicKey = subjectPublicKey;
+ }
+
+ /**
+ * EFFECTS: Parse input DER.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing (info, algorithm, signature)
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public SubjectPublicKeyInfo(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.algorithm = new AlgorithmIdentifier(encoded, false);
+ this.algorithm.getTag().enforce(TAG_SEQUENCE);
+
+ this.subjectPublicKey = new BitString(encoded, false);
+ this.subjectPublicKey.getTag().enforce(BitString.TAG);
+ }
+
+ /**
+ * EFFECTS: Encode the fields into DER, in the order.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(algorithm.encodeDER()),
+ Arrays.asList(subjectPublicKey.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public AlgorithmIdentifier getAlgorithm() {
+ return algorithm;
+ }
+
+ public BitString getSubjectPublicKey() {
+ return subjectPublicKey;
+ }
+}
diff --git a/src/main/model/pki/cert/Certificate.java b/src/main/model/pki/cert/Certificate.java
new file mode 100644
index 0000000..4e6c291
--- /dev/null
+++ b/src/main/model/pki/cert/Certificate.java
@@ -0,0 +1,127 @@
+package model.pki.cert;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents an X.509 signed certificate.
+ * <pre>
+ * Certificate ::= SIGNED{TBSCertificate}
+ *
+ * ENCRYPTED{ToBeEnciphered} ::= BIT STRING (CONSTRAINED BY {
+ * -- shall be the result of applying an encipherment procedure
+ * -- to the BER-encoded octets of a value of -- ToBeEnciphered } )
+ *
+ * HASH{ToBeHashed} ::= SEQUENCE {
+ * algorithmIdentifier AlgorithmIdentifier{{SupportedAlgorithms}},
+ * hashValue BIT STRING (CONSTRAINED BY {
+ * -- shall be the result of applying a hashing procedure to the DER-encoded
+ * -- octets of a value of -- ToBeHashed } ),
+ * ... }
+ *
+ * ENCRYPTED-HASH{ToBeSigned} ::= BIT STRING (CONSTRAINED BY {
+ * -- shall be the result of applying a hashing procedure to the DER-encoded (see 6.2)
+ * -- octets of a value of -- ToBeSigned -- and then applying an encipherment procedure
+ * -- to those octets -- } )
+ *
+ * SIGNATURE{ToBeSigned} ::= SEQUENCE {
+ * algorithmIdentifier AlgorithmIdentifier{{SupportedAlgorithms}},
+ * encrypted ENCRYPTED-HASH{ToBeSigned},
+ * ... }
+ *
+ * SIGNED{ToBeSigned} ::= SEQUENCE {
+ * toBeSigned ToBeSigned,
+ * COMPONENTS OF SIGNATURE{ToBeSigned},
+ * ... }
+ * </pre>
+ *
+ * A certificate creates a binding between the proposed subject name and the public key. It is only valid once a trusted
+ * CA signs it. Relying parties only need to trust a single trust anchor (the Root CA), and all of its issued certs are
+ * trusted. This is done through the cert tree: each certificate contains the Issued By field, indicating the DN of the
+ * upper level, all the way until the root CA, which is hard-coded in relying parties.
+ */
+public class Certificate extends ASN1Object {
+ /**
+ * All info of that cert, excluding the signature.
+ * It will be signed, and the signature is in <pre>signature</pre>.
+ */
+ private final TbsCertificate certificate;
+
+ /**
+ * The algorithm used for <pre>signature</pre>.
+ */
+ private final AlgorithmIdentifier signatureAlgorithm;
+
+ /**
+ * The signature.
+ */
+ private final BitString signature;
+
+ /**
+ * EFFECTS: Initialize the object with the given tag and parentTag, and info, signatureAlgorithm, and signature.
+ * REQUIRES: The algorithm must match the signature. The fields must have correct tags as described in the class
+ * specification (SEQUENCE, SEQUENCE, BIT STRING).
+ */
+ public Certificate(Tag tag, Tag parentTag,
+ final TbsCertificate certificate,
+ final AlgorithmIdentifier signatureAlgorithm,
+ final BitString signature) {
+ super(tag, parentTag);
+ this.certificate = certificate;
+ this.signatureAlgorithm = signatureAlgorithm;
+ this.signature = signature;
+ }
+
+ /**
+ * EFFECTS: Parse input DER, without verifying the signature.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public Certificate(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ this.certificate = new TbsCertificate(encoded, false);
+ this.certificate.getTag().enforce(TAG_SEQUENCE);
+
+ this.signatureAlgorithm = new AlgorithmIdentifier(encoded, false);
+ this.signatureAlgorithm.getTag().enforce(TAG_SEQUENCE);
+
+ this.signature = new BitString(encoded, false);
+ this.signature.getTag().enforce(BitString.TAG);
+ }
+
+ /**
+ * EFFECT: Encode that sequence into an ordered array of bytes, following the class specification.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(certificate.encodeDER()),
+ Arrays.asList(signatureAlgorithm.encodeDER()),
+ Arrays.asList(signature.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public TbsCertificate getCertificate() {
+ return certificate;
+ }
+
+ public AlgorithmIdentifier getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public BitString getSignature() {
+ return signature;
+ }
+}
diff --git a/src/main/model/pki/cert/Extension.java b/src/main/model/pki/cert/Extension.java
new file mode 100644
index 0000000..0c104a4
--- /dev/null
+++ b/src/main/model/pki/cert/Extension.java
@@ -0,0 +1,113 @@
+package model.pki.cert;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+/**
+ * A X.509v3 certificate extension entry.
+ * <pre>
+ * Extension ::= SEQUENCE {
+ * extnId EXTENSION.&id({ExtensionSet}),
+ * critical BOOLEAN DEFAULT FALSE,
+ * extnValue OCTET STRING
+ * (CONTAINING EXTENSION.&ExtnType({ExtensionSet}{@extnId})
+ * ENCODED BY der),
+ * ... }
+ * </pre>
+ * Extensions only exist in v3 certificates. They allow the CA and the relying party to add additional verification
+ * stages to the certificate to constraint its use or to supply additional information. For example, the CA may put a
+ * CDP (CRL Distribution Point) into the extensions.
+ */
+public class Extension extends ASN1Object {
+ /**
+ * The ID of the type of that extension.
+ */
+ private final ObjectIdentifier extnId;
+
+ /**
+ * Marking an extension critical means that the relying-party
+ * must reject that certificate if the type is unrecognized.
+ * If the type is recognized but cannot be fully parsed, the
+ * behaviour is undefined.
+ * Marking an extension critical reduces compatibility.
+ */
+ private final Bool critical;
+
+ /**
+ * The DER-encoded ASN.1 content of that extension.
+ */
+ private final OctetString extnValue;
+
+ /**
+ * EFFECTS: Init with tags, extnId, critical, and extnValue. For tags, see {@link ASN1Object}.
+ * extnValue is not checked against extnId.
+ * REQUIRES: Tags of extnId, critical, extnValue should be OID, BOOLEAN, OCTET STRING. The value should be a DER
+ * bytes octet string. If critical is unspecified (which defaults to false), put null.
+ */
+ public Extension(Tag tag, Tag parentTag,
+ final ObjectIdentifier extnId,
+ final Bool critical,
+ final OctetString extnValue) {
+ super(tag, parentTag);
+ this.extnId = extnId;
+ this.critical = critical;
+ this.extnValue = extnValue;
+ }
+
+ /**
+ * EFFECTS: Parse input DER.
+ * Throws {@link ParseException} if the input is invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * Note that critical is optional, and if it does not exist, it will be left as null, and it should be treated as
+ * false.
+ * MODIFIES: this, encoded
+ */
+ public Extension(BytesReader encoded, boolean hasParentType) throws ParseException {
+ super(encoded, hasParentType);
+ this.extnId = new ObjectIdentifier(encoded, false);
+ this.extnId.getTag().enforce(ObjectIdentifier.TAG);
+
+ if (encoded.detectTag(Bool.TAG)) {
+ critical = new Bool(encoded, false);
+ } else {
+ critical = null;
+ }
+
+ this.extnValue = new OctetString(encoded, false);
+ this.extnValue.getTag().enforce(OctetString.TAG);
+ }
+
+ /**
+ * EFFECTS: Encode the DER.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(extnId.encodeDER()),
+ critical == null ? Collections.<Byte>emptyList() :
+ Arrays.asList(critical.encodeDER()),
+ Arrays.asList(extnValue.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public ObjectIdentifier getExtnId() {
+ return extnId;
+ }
+
+ public Bool getCritical() {
+ return critical;
+ }
+
+ public OctetString getExtnValue() {
+ return extnValue;
+ }
+}
diff --git a/src/main/model/pki/cert/Extensions.java b/src/main/model/pki/cert/Extensions.java
new file mode 100644
index 0000000..780fa2c
--- /dev/null
+++ b/src/main/model/pki/cert/Extensions.java
@@ -0,0 +1,67 @@
+package model.pki.cert;
+
+import model.asn1.ASN1Object;
+import model.asn1.Encodable;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.x501.RelativeDistinguishedName;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ * Represents an X.509 certificate extensions list:
+ * <pre>
+ * Extensions ::= SEQUENCE OF Extension
+ * </pre>
+ */
+public class Extensions extends ASN1Object {
+ private final Extension[] extensions;
+
+ /**
+ * EFFECT: Initialize with the given tags and extensions. For tag and parentTag, consult
+ * {@link ASN1Object}.
+ * REQUIRES: Extensions should have SEQUENCE tag.
+ */
+ public Extensions(Tag tag, Tag parentTag, Extension[] extensions) {
+ super(tag, parentTag);
+ this.extensions = extensions;
+ }
+
+ /**
+ * EFFECT: Parse the Name from input DER bytes. For details on parsing, refer to {@link ASN1Object}.
+ * Throws {@link ParseException} for invalid input.
+ * MODIFIES: this, encoded
+ */
+ public Extensions(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ final List<Extension> list = new ArrayList<>();
+ for (int i = 0; i < getLength();) {
+ int index = encoded.getIndex();
+ final Extension ext = new Extension(encoded, false);
+ ext.getTag().enforce(TAG_SEQUENCE);
+ list.add(ext);
+ index = encoded.getIndex() - index;
+ i += index;
+ }
+ this.extensions = list.toArray(new Extension[0]);
+ }
+
+ /**
+ * EFFECTS: Encode the SEQUENCE OF into DER, keep order. RDNs will be encoded one-by-one.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(extensions)
+ .map(Encodable::encodeDER)
+ .flatMap(Arrays::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Extension[] getExtensions() {
+ return extensions;
+ }
+}
diff --git a/src/main/model/pki/cert/TbsCertificate.java b/src/main/model/pki/cert/TbsCertificate.java
new file mode 100644
index 0000000..1175456
--- /dev/null
+++ b/src/main/model/pki/cert/TbsCertificate.java
@@ -0,0 +1,263 @@
+package model.pki.cert;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+import model.pki.SubjectPublicKeyInfo;
+import model.x501.Name;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+/**
+ * Represents a X.509 certificate
+ *
+ * <pre>
+ * Certificate ::= SIGNED{TBSCertificate}
+ * TBSCertificate ::= SEQUENCE {
+ * version [0] Version DEFAULT v1,
+ * serialNumber CertificateSerialNumber,
+ * signature AlgorithmIdentifier{{SupportedAlgorithms}},
+ * issuer Name,
+ * validity Validity,
+ * subject Name,
+ * subjectPublicKeyInfo SubjectPublicKeyInfo,
+ * issuerUniqueIdentifier [1] IMPLICIT UniqueIdentifier OPTIONAL,
+ * ...,
+ * [[2: -- if present, version shall be v2 or v3
+ * subjectUniqueIdentifier [2] IMPLICIT UniqueIdentifier OPTIONAL]],
+ * [[3: -- if present, version shall be v2 or v3
+ * extensions [3] Extensions OPTIONAL]]
+ * -- If present, version shall be v3]]
+ * }
+ *
+ * Version ::= INTEGER {v1(0), v2(1), v3(2)}
+ * CertificateSerialNumber ::= INTEGER
+ *
+ * uniqueIdentifier ATTRIBUTE ::= {
+ * WITH SYNTAX UniqueIdentifier
+ * EQUALITY MATCHING RULE bitStringMatch
+ * LDAP-SYNTAX bitString.&id
+ * LDAP-NAME {"x500UniqueIdentifier"}
+ * ID id-at-uniqueIdentifier }
+ * UniqueIdentifier ::= BIT STRING
+ * </pre>
+ *
+ * NOTE that subjectUniqueIdentifier and issuerUniqueIdentifier are not supported.
+ */
+public class TbsCertificate extends ASN1Object {
+ // Version ::= INTEGER {v1(0), v2(1), v3(2)}
+ public static final int VERSION_V1 = 0;
+ public static final int VERSION_V2 = 1;
+ public static final int VERSION_V3 = 2;
+
+ /**
+ * The X.509 cert version. subjectUniqueIdentifier is v2 only, and extensions is v3 only.
+ * <pre>
+ * [0] Version DEFAULT v1
+ * </pre>
+ */
+ private final Int version;
+
+ /**
+ * The serial number of that certificate that is unique across the CA.
+ * <pre>
+ * serialNumber CertificateSerialNumber
+ * CertificateSerialNumber ::= INTEGER
+ * </pre>
+ */
+ private final Int serialNumber;
+
+ private final AlgorithmIdentifier signature;
+
+ /**
+ * The subject and issuer distinguished names.
+ * <pre>
+ * issuer Name,
+ * subject Name
+ * </pre>
+ */
+ private final Name issuer;
+
+ /**
+ * The validity period of that certificate.
+ * Validity ::= SEQUENCE { notBefore Time, notAfter Time, ... }
+ */
+ private final Validity validity;
+
+ /**
+ * See the comments on issuer.
+ */
+ private final Name subject;
+
+ private final SubjectPublicKeyInfo subjectPublicKeyInfo;
+
+ /**
+ * [3] Optional.
+ */
+ private final Extensions extensions;
+
+ /**
+ * EFFECTS: Init with the given parameters. For tag and parentTag, see {@link ASN1Object}.
+ * REQUIRES:
+ * - Version must be V1, V2, or V3.
+ * - {issuer,subject}UniqueIdentifier could be null.
+ * - If {issuer,subject}UniqueIdentifier presents, version must be V2 or V3.
+ * - Extensions could be null.
+ * - If extensions presents, version must be V3.
+ * - The signature should be valid.
+ * - Field and Desired Tags:
+ * version CONTEXT SPECIFIC 0 (EXPLICIT), INTEGER, OPTIONAL DEFAULT v1
+ * serialNumber INTEGER
+ * signature SEQUENCE
+ * issuer SEQUENCE
+ * validity SEQUENCE
+ * subject SEQUENCE
+ * subjectPublicKeyInfo SEQUENCE
+ * extensions CONTEXT SPECIFIC 3 (EXPLICIT), SEQUENCE, OPTIONAL
+ */
+ public TbsCertificate(Tag tag, Tag parentTag,
+ final Int version,
+ final Int serialNumber,
+ final AlgorithmIdentifier signature,
+ final Name issuer,
+ final Validity validity,
+ final Name subject,
+ final SubjectPublicKeyInfo subjectPublicKeyInfo,
+ final Extensions extensions) {
+ super(tag, parentTag);
+ this.version = version;
+ this.serialNumber = serialNumber;
+ this.signature = signature;
+ this.issuer = issuer;
+ this.validity = validity;
+ this.subject = subject;
+ this.subjectPublicKeyInfo = subjectPublicKeyInfo;
+ this.extensions = extensions;
+ }
+
+ /**
+ * EFFECTS: Parse input DER.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing
+ * - Any fields having an incorrect parent / inner tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - extensions are specified, but the version is v1 or v2
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public TbsCertificate(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ int i = encoded.getIndex();
+ if (encoded.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, true, 0))) {
+ this.version = new Int(encoded, true);
+ } else {
+ this.version = null;
+ }
+ this.serialNumber = new Int(encoded, false);
+ this.signature = new AlgorithmIdentifier(encoded, false);
+ this.issuer = new Name(encoded, false);
+ this.validity = new Validity(encoded, false);
+ this.subject = new Name(encoded, false);
+ this.subjectPublicKeyInfo = new SubjectPublicKeyInfo(encoded, false);
+ if (encoded.detectTag(new Tag(TagClass.CONTEXT_SPECIFIC, true, 3))) {
+ this.extensions = new Extensions(encoded, true);
+ } else {
+ // Enforce the extensions tag - nothing else should be here.
+ if (Integer.compareUnsigned(getLength(), (encoded.getIndex() - i)) != 0) {
+ new Tag(encoded).enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 3));
+ }
+ this.extensions = null;
+ }
+ enforceInput();
+ enforceVersion();
+ }
+
+ /**
+ * EFFECTS: Throw {@link ParseException} if any field have illegal tags.
+ */
+ private void enforceInput() throws ParseException {
+ if (this.version != null) {
+ this.version.getTag().enforce(Int.TAG);
+ this.version.getParentTag().enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 0));
+ }
+ this.serialNumber.getTag().enforce(Int.TAG);
+ this.signature.getTag().enforce(TAG_SEQUENCE);
+ this.issuer.getTag().enforce(TAG_SEQUENCE);
+ this.validity.getTag().enforce(TAG_SEQUENCE);
+ this.subject.getTag().enforce(TAG_SEQUENCE);
+ this.subjectPublicKeyInfo.getTag().enforce(TAG_SEQUENCE);
+ if (extensions != null) {
+ this.extensions.getTag().enforce(TAG_SEQUENCE);
+ this.extensions.getParentTag().enforce(new Tag(TagClass.CONTEXT_SPECIFIC, true, 3));
+ }
+ }
+
+ /**
+ * EFFECTS: Throw {@link ParseException} if the version is incorrect.
+ */
+ private void enforceVersion() throws ParseException {
+ if (version != null
+ && (version.getLong() != VERSION_V1
+ && version.getLong() != VERSION_V2
+ && version.getLong() != VERSION_V3)) {
+ throw new ParseException("Illegal certificate version: " + version.getLong());
+ }
+ if (extensions != null && (version == null || version.getLong() != VERSION_V3)) {
+ throw new ParseException("Extensions present. The version must be v3 or above.");
+ }
+ }
+
+ /**
+ * EFFECTS: Encode into ordered DER.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(version == null ? Collections.<Byte>emptyList() : Arrays.asList(version.encodeDER()),
+ Arrays.asList(serialNumber.encodeDER()),
+ Arrays.asList(signature.encodeDER()),
+ Arrays.asList(issuer.encodeDER()),
+ Arrays.asList(validity.encodeDER()),
+ Arrays.asList(subject.encodeDER()),
+ Arrays.asList(subjectPublicKeyInfo.encodeDER()),
+ extensions == null ? Collections.<Byte>emptyList()
+ : Arrays.asList(extensions.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Int getVersion() {
+ return version;
+ }
+
+ public Int getSerialNumber() {
+ return serialNumber;
+ }
+
+ public AlgorithmIdentifier getSignature() {
+ return signature;
+ }
+
+ public Name getIssuer() {
+ return issuer;
+ }
+
+ public Validity getValidity() {
+ return validity;
+ }
+
+ public Name getSubject() {
+ return subject;
+ }
+
+ public SubjectPublicKeyInfo getSubjectPublicKeyInfo() {
+ return subjectPublicKeyInfo;
+ }
+
+ public Extensions getExtensions() {
+ return extensions;
+ }
+}
diff --git a/src/main/model/pki/cert/Validity.java b/src/main/model/pki/cert/Validity.java
new file mode 100644
index 0000000..76279ed
--- /dev/null
+++ b/src/main/model/pki/cert/Validity.java
@@ -0,0 +1,95 @@
+package model.pki.cert;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents the following ASN.1 structure:
+ * <pre>
+ * Validity ::= SEQUENCE {
+ * notBefore Time,
+ * notAfter Time,
+ * ...
+ * }
+ *
+ * Time ::= CHOICE {
+ * utcTime UTCTime,
+ * generalizedTime GeneralizedTime
+ * }
+ * </pre>
+ * It describes the validity period of the certificate.
+ */
+public class Validity extends ASN1Object {
+ /**
+ * The certificate is not valid before that time.
+ */
+ private final ASN1Time notBefore;
+
+ /**
+ * The certificate is not valid after that time.
+ */
+ private final ASN1Time notAfter;
+
+ /**
+ * EFFECTS: Init with the given tag, parentTag, notBefore, and notAfter. For more info on tag and parentTag, see
+ * {@link ASN1Object}.
+ * REQUIRES: notBefore and notAfter are either UTCTime or GeneralizedTime.
+ */
+ public Validity(Tag tag, Tag parentTag,
+ ASN1Time notBefore, ASN1Time notAfter) {
+ super(tag, parentTag);
+ this.notBefore = notBefore;
+ this.notAfter = notAfter;
+ }
+
+ /**
+ * EFFECTS: Parse input DER.
+ * Throws {@link ASN1Object} if invalid:
+ * - Any fields missing (info, algorithm, signature)
+ * - Any fields having an incorrect tag (as seen in the ASN.1 definition)
+ * - Any fields with encoding instructions that violate implicit / explicit encoding rules
+ * - Other issues found during parsing the object, like early EOF (see {@link ASN1Object})
+ * MODIFIES: this, encoded
+ */
+ public Validity(BytesReader encoded, boolean hasParentTag) throws ParseException {
+ super(encoded, hasParentTag);
+ if (encoded.detectTag(GeneralizedTime.TAG)) {
+ this.notBefore = new GeneralizedTime(encoded, false);
+ this.notBefore.getTag().enforce(GeneralizedTime.TAG);
+ } else {
+ this.notBefore = new UtcTime(encoded, false);
+ this.notBefore.getTag().enforce(UtcTime.TAG);
+ }
+ if (encoded.detectTag(GeneralizedTime.TAG)) {
+ this.notAfter = new GeneralizedTime(encoded, false);
+ this.notAfter.getTag().enforce(GeneralizedTime.TAG);
+ } else {
+ this.notAfter = new UtcTime(encoded, false);
+ this.notAfter.getTag().enforce(UtcTime.TAG);
+ }
+ }
+
+ /**
+ * EFFECTS: Encode into ordered DER.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(notBefore.encodeDER()),
+ Arrays.asList(notAfter.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public ASN1Time getNotBefore() {
+ return notBefore;
+ }
+
+ public ASN1Time getNotAfter() {
+ return notAfter;
+ }
+}
diff --git a/src/main/model/pki/crl/CertificateList.java b/src/main/model/pki/crl/CertificateList.java
new file mode 100644
index 0000000..5142101
--- /dev/null
+++ b/src/main/model/pki/crl/CertificateList.java
@@ -0,0 +1,76 @@
+package model.pki.crl;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Tag;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.pki.AlgorithmIdentifier;
+import model.pki.cert.TbsCertificate;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Represents a signed X.509 CRL.
+ * <pre>
+ * CertificateList ::= SIGNED{CertificateListContent}
+ * </pre>
+ */
+public class CertificateList extends ASN1Object {
+ /**
+ * All info of that CRL, excluding the signature.
+ * It will be signed, and the signature is in <pre>signature</pre>.
+ */
+ private final CertificateListContent crl;
+
+ /**
+ * The algorithm used for <pre>signature</pre>.
+ */
+ private final AlgorithmIdentifier signatureAlgorithm;
+
+ /**
+ * The signature.
+ */
+ private final BitString signature;
+
+ /**
+ * EFFECTS: Initialize the object with the given tag and parentTag, and list, signatureAlgorithm, and signature.
+ * REQUIRES: The algorithm must match the signature. The fields must have correct tags as described in the class
+ * specification (SEQUENCE, SEQUENCE, BIT STRING).
+ */
+ public CertificateList(Tag tag, Tag parentTag,
+ final CertificateListContent crl,
+ final AlgorithmIdentifier signatureAlgorithm,
+ final BitString signature) {
+ super(tag, parentTag);
+ this.crl = crl;
+ this.signatureAlgorithm = signatureAlgorithm;
+ this.signature = signature;
+ }
+
+ /**
+ * EFFECT: Encode that sequence into an ordered array of bytes, following the class specification.
+ */
+ @Override
+ public Byte[] encodeValueDER() {
+ return Stream.of(Arrays.asList(crl.encodeDER()),
+ Arrays.asList(signatureAlgorithm.encodeDER()),
+ Arrays.asList(signature.encodeDER()))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public CertificateListContent getCrl() {
+ return crl;
+ }
+
+ public AlgorithmIdentifier getSignatureAlgorithm() {
+ return signatureAlgorithm;
+ }
+
+ public BitString getSignature() {
+ return signature;
+ }
+}
diff --git a/src/main/model/pki/crl/CertificateListContent.java b/src/main/model/pki/crl/CertificateListContent.java
new file mode 100644
index 0000000..6f75d71
--- /dev/null
+++ b/src/main/model/pki/crl/CertificateListContent.java
@@ -0,0 +1,106 @@
+package model.pki.crl;
+
+import model.asn1.*;
+import model.pki.AlgorithmIdentifier;
+import model.x501.Name;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Represents a CRL content:
+ *
+ * <pre>
+ * CertificateListContent ::= SEQUENCE {
+ * version Version OPTIONAL,
+ * -- if present, version shall be v2
+ * signature AlgorithmIdentifier{{SupportedAlgorithms}},
+ * issuer Name,
+ * thisUpdate Time,
+ * nextUpdate Time OPTIONAL,
+ * revokedCertificates SEQUENCE OF SEQUENCE {
+ * serialNumber CertificateSerialNumber,
+ * revocationDate Time,
+ * crlEntryExtensions Extensions OPTIONAL,
+ * ...} OPTIONAL,
+ * ...,
+ * ...,
+ * crlExtensions [0] Extensions OPTIONAL }
+ * </pre>
+ *
+ * A CRL is a signed object published by the CA that revokes any certificates signed by this CA before their
+ * expiration. Relying-parties should check the CRL from corresponding CDPs to see if the certificate to check is
+ * already revoked.
+ * Because the CA will only generate CRLs, this object won't be parsed.
+ */
+public class CertificateListContent extends ASN1Object {
+ private final Int version = new Int(Int.TAG, null, 1);
+ private final Name issuer;
+ private final AlgorithmIdentifier signature;
+ private final ASN1Time thisUpdate;
+ private final ASN1Time nextUpdate;
+ private final RevokedCertificate[] revokedCertificates;
+
+ /**
+ * EFFECTS: Init with tags and the given parameters. Version is always set to 1.
+ * REQUIRES: except for nextUpdate, all other fields are non-null; items in revokedCerts should be SEQUENCE.
+ */
+ public CertificateListContent(Tag tag, Tag parentTag,
+ Name issuer,
+ AlgorithmIdentifier signature,
+ ASN1Time thisUpdate,
+ ASN1Time nextUpdate,
+ RevokedCertificate[] revokedCertificates) {
+ super(tag, parentTag);
+ this.issuer = issuer;
+ this.signature = signature;
+ this.thisUpdate = thisUpdate;
+ this.nextUpdate = nextUpdate;
+ this.revokedCertificates = revokedCertificates;
+ }
+
+ @Override
+ public Byte[] encodeValueDER() {
+ final List<Byte> itemsEncoded = Arrays.stream(revokedCertificates)
+ .map(Encodable::encodeDER)
+ .flatMap(Arrays::stream)
+ .collect(Collectors.toList());
+ return Stream.of(Arrays.asList(version.encodeDER()),
+ Arrays.asList(issuer.encodeDER()),
+ Arrays.asList(signature.encodeDER()),
+ Arrays.asList(thisUpdate.encodeDER()),
+ nextUpdate == null ? Collections.<Byte>emptyList() : Arrays.asList(nextUpdate.encodeDER()),
+ Arrays.asList(new Tag(TagClass.UNIVERSAL, true, 0x30).encodeDER()),
+ Arrays.asList(new ASN1Length(itemsEncoded.size()).encodeDER()), itemsEncoded)
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Int getVersion() {
+ return version;
+ }
+
+ public Name getIssuer() {
+ return issuer;
+ }
+
+ public AlgorithmIdentifier getSignature() {
+ return signature;
+ }
+
+ public ASN1Time getThisUpdate() {
+ return thisUpdate;
+ }
+
+ public ASN1Time getNextUpdate() {
+ return nextUpdate;
+ }
+
+ public RevokedCertificate[] getRevokedCertificates() {
+ return revokedCertificates;
+ }
+}
diff --git a/src/main/model/pki/crl/Reason.java b/src/main/model/pki/crl/Reason.java
new file mode 100644
index 0000000..e47609e
--- /dev/null
+++ b/src/main/model/pki/crl/Reason.java
@@ -0,0 +1,27 @@
+package model.pki.crl;
+
+/**
+ * Identify the reason for revocation.
+ */
+public enum Reason {
+ UNSPECIFIED(0),
+ KEY_COMPROMISE(1),
+ CA_COMPROMISE(2),
+ AFFILIATION_CHANGED(3),
+ SUPERSEDED(4),
+ CESSATION_OF_OPERATION(5);
+
+ private final int val;
+
+ /**
+ * EFFECTS: Init with the specific val.
+ * REQUIRES: 0 <= val <= 0xFF
+ */
+ Reason(int val) {
+ this.val = val;
+ }
+
+ public int getVal() {
+ return val;
+ }
+}
diff --git a/src/main/model/pki/crl/RevokedCertificate.java b/src/main/model/pki/crl/RevokedCertificate.java
new file mode 100644
index 0000000..457ecb8
--- /dev/null
+++ b/src/main/model/pki/crl/RevokedCertificate.java
@@ -0,0 +1,72 @@
+package model.pki.crl;
+
+import model.asn1.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Stream;
+
+/**
+ * Indicates the revocation status of a certificate, given its serial number, revocation date, and reason.
+ * <pre>
+ * SEQUENCE {
+ * serialNumber CertificateSerialNumber,
+ * revocationDate Time,
+ * crlEntryExtensions Extensions OPTIONAL,
+ * ...}
+ * </pre>
+ */
+public class RevokedCertificate extends ASN1Object {
+ private final Int serialNumber;
+ private final ASN1Time revocationDate;
+ private final Reason reason;
+
+ /**
+ * EFFECT: Init with tags and parameters. See {@link ASN1Object} for tags.
+ * REQUIRES: revocationDate should be either UtcTime or GeneralTime.
+ */
+ public RevokedCertificate(Tag tag, Tag parentTag,
+ Int serialNumber,
+ ASN1Time revocationDate,
+ Reason reason) {
+ super(tag, parentTag);
+ this.serialNumber = serialNumber;
+ this.revocationDate = revocationDate;
+ this.reason = reason;
+ }
+
+ @Override
+ public Byte[] encodeValueDER() {
+ final Byte[] r = new OctetString(OctetString.TAG,
+ null,
+ new Byte[]{ 0x0A, 0x01, (byte) reason.getVal() })
+ .encodeDER();
+ final Byte[] oid = new ObjectIdentifier(ObjectIdentifier.TAG, null, ObjectIdentifier.OID_CRL_REASON)
+ .encodeDER();
+ final Byte[] seqExt = Stream.of(Arrays.asList(TAG_SEQUENCE.encodeDER()),
+ Arrays.asList(new ASN1Length(r.length + oid.length).encodeDER()),
+ Arrays.asList(oid),
+ Arrays.asList(r))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ return Stream.of(Arrays.asList(serialNumber.encodeDER()),
+ Arrays.asList(revocationDate.encodeDER()),
+ Arrays.asList(TAG_SEQUENCE.encodeDER()),
+ Arrays.asList(new ASN1Length(seqExt.length).encodeDER()),
+ Arrays.asList(seqExt))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ public Int getSerialNumber() {
+ return serialNumber;
+ }
+
+ public ASN1Time getRevocationDate() {
+ return revocationDate;
+ }
+
+ public Reason getReason() {
+ return reason;
+ }
+}