aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuuta Liang <yuutaw@students.cs.ubc.ca>2023-10-14 05:12:06 +0800
committerYuuta Liang <yuutaw@students.cs.ubc.ca>2023-10-14 05:12:06 +0800
commit0bcc057e741af3fbc108f42b75f9d42f48f6a51e (patch)
treed638c81c0778554a8902efc59000e61db74060be
parentf369da34cf9aca151df0150d90e651e6a88ee700 (diff)
downloadjca-0bcc057e741af3fbc108f42b75f9d42f48f6a51e.tar
jca-0bcc057e741af3fbc108f42b75f9d42f48f6a51e.tar.gz
jca-0bcc057e741af3fbc108f42b75f9d42f48f6a51e.tar.bz2
jca-0bcc057e741af3fbc108f42b75f9d42f48f6a51e.zip
Implement the CA
Signed-off-by: Yuuta Liang <yuutaw@students.cs.ubc.ca>
-rw-r--r--README.md4
-rw-r--r--src/main/model/asn1/ASN1Object.java2
-rw-r--r--src/main/model/asn1/Int.java13
-rw-r--r--src/main/model/ca/AuditLogEntry.java39
-rw-r--r--src/main/model/ca/CACertificate.java279
-rw-r--r--src/main/model/ca/Template.java104
-rw-r--r--src/main/model/pki/cert/TbsCertificate.java15
-rw-r--r--src/main/model/pki/crl/CertificateListContent.java2
-rw-r--r--src/main/ui/IssueScreen.java138
-rw-r--r--src/main/ui/JCA.java203
-rw-r--r--src/main/ui/Main.java4
-rw-r--r--src/main/ui/MainScreen.java200
-rw-r--r--src/main/ui/MgmtScreen.java170
-rw-r--r--src/main/ui/Screen.java31
-rw-r--r--src/main/ui/TemplateSetScreen.java131
-rw-r--r--src/main/ui/TemplatesScreen.java108
-rw-r--r--src/main/ui/UIHandler.java45
-rw-r--r--src/main/ui/Utils.java44
-rw-r--r--src/test/model/TestConstants.java4
-rw-r--r--src/test/model/asn1/IntTest.java3
-rw-r--r--src/test/model/ca/AuditLogEntryTest.java26
-rw-r--r--src/test/model/ca/CACertificateTest.java178
-rw-r--r--src/test/model/ca/TemplateTest.java27
-rw-r--r--src/test/model/pki/cert/TbsCertificateTest.java12
-rw-r--r--src/test/ui/UtilsTest.java67
-rw-r--r--tests/.gitignore1
-rw-r--r--tests/Makefile40
-rw-r--r--tests/ca.cnf108
-rw-r--r--tests/leaf.csr.cnf9
29 files changed, 1974 insertions, 33 deletions
diff --git a/README.md b/README.md
index 3ab9a43..907ba30 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ proprietary).
As a user, I want to be able to:
-1. Import a CA cryptography key-pair and its corresponding X.509 certificate
+1. Generate a CA cryptography key-pair and its corresponding X.509 certificate
into the program and view it. The private key must not be displayed or exported
in any format under any circumstances for security purposes.
2. Input CSRs, edit the certificate properties (e.g., subject, not before,
@@ -41,7 +41,7 @@ optionally revoke any of them with a corresponding PKCS#10 reason. The
certificates must not be deleted from the list under any circumstances but
only revoked because some future administrators or the legal team may need
to audit it.
-4. Publish base and optionally delta CRLs.
+4. Publish base CRLs.
5. Add, enable, disable, or remove custom certificate templates (also called
policies) that constraints what each type of certificates can and cannot have
and their properties (e.g., TLS server certificates vs user logon certificates
diff --git a/src/main/model/asn1/ASN1Object.java b/src/main/model/asn1/ASN1Object.java
index d1bce06..9b4a98c 100644
--- a/src/main/model/asn1/ASN1Object.java
+++ b/src/main/model/asn1/ASN1Object.java
@@ -159,7 +159,7 @@ public class ASN1Object implements Encodable {
list.addAll(Arrays.asList(tag.encodeDER()));
list.addAll(Arrays.asList(new ASN1Length(val.length).encodeDER()));
- list.addAll(Arrays.asList(encodeValueDER()));
+ list.addAll(Arrays.asList(val));
if (parentTag != null) { // Explicit
final List<Byte> newList = new ArrayList<>(list.size() + 3);
diff --git a/src/main/model/asn1/Int.java b/src/main/model/asn1/Int.java
index 5b75a73..4eeeedf 100644
--- a/src/main/model/asn1/Int.java
+++ b/src/main/model/asn1/Int.java
@@ -25,9 +25,18 @@ public class Int extends ASN1Object {
* encoding. For more information, consult {@link ASN1Object}.
* REQUIRES: Consult {@link ASN1Object}.
*/
- public Int(Tag tag, Tag parentTag, long value) {
+ public Int(Tag tag, Tag parentTag, BigInteger value) {
super(tag, parentTag);
- this.value = BigInteger.valueOf(value);
+ this.value = 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) {
+ this(tag, parentTag, BigInteger.valueOf(value));
}
/**
diff --git a/src/main/model/ca/AuditLogEntry.java b/src/main/model/ca/AuditLogEntry.java
new file mode 100644
index 0000000..a8d1929
--- /dev/null
+++ b/src/main/model/ca/AuditLogEntry.java
@@ -0,0 +1,39 @@
+package model.ca;
+
+import java.time.ZonedDateTime;
+
+/**
+ * An audit log entry. Audit logs record who did what at when, used for future auditing purposes.
+ * These logs will never be deleted.
+ */
+public class AuditLogEntry {
+ private final String user;
+ private final ZonedDateTime time;
+ private final String action;
+
+ /**
+ * EFFECTS: Init the entry with the given user, time, and action.
+ */
+ public AuditLogEntry(String user, ZonedDateTime time, String action) {
+ this.user = user;
+ this.time = time;
+ this.action = action;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s\t%s\t%s", time, user, action);
+ }
+
+ public String getUser() {
+ return user;
+ }
+
+ public ZonedDateTime getTime() {
+ return time;
+ }
+
+ public String getAction() {
+ return action;
+ }
+}
diff --git a/src/main/model/ca/CACertificate.java b/src/main/model/ca/CACertificate.java
new file mode 100644
index 0000000..36a9ac5
--- /dev/null
+++ b/src/main/model/ca/CACertificate.java
@@ -0,0 +1,279 @@
+package model.ca;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.csr.*;
+import model.pki.AlgorithmIdentifier;
+import model.pki.SubjectPublicKeyInfo;
+import model.pki.cert.*;
+import model.pki.cert.Certificate;
+import model.pki.crl.CertificateList;
+import model.pki.crl.CertificateListContent;
+import model.pki.crl.RevokedCertificate;
+import model.x501.AttributeTypeAndValue;
+import model.x501.Name;
+import model.x501.RelativeDistinguishedName;
+import ui.Utils;
+
+import java.math.BigInteger;
+import java.security.*;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.RSAPrivateKeySpec;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.*;
+import java.util.stream.Stream;
+
+/**
+ * Holds a CA private key, its certificate, and signed / revoked list.
+ */
+public class CACertificate {
+ /**
+ * The key pair.
+ */
+ private KeyPair key;
+
+ /**
+ * The signed certificate.
+ */
+ private Certificate certificate;
+
+ /**
+ * Signed certificates.
+ */
+ private List<Certificate> signed;
+
+ /**
+ * The next serial number.
+ */
+ private int serial;
+
+ /**
+ * Revoked certs.
+ */
+ private List<RevokedCertificate> revoked;
+
+ /**
+ * EFFECT: Init with a null key and null certificate, empty signed and revoked list, and serial at 1.
+ */
+ public CACertificate() {
+ this.key = null;
+ this.certificate = null;
+ this.serial = 1;
+ this.signed = new ArrayList<>();
+ this.revoked = new ArrayList<>();
+ }
+
+ /**
+ * EFFECTS: Generate a new RSA2048 private key.
+ * REQUIRES: getPublicKey() is null (i.e., no private key had been installed)
+ */
+ public void generateKey() throws NoSuchAlgorithmException {
+ final KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
+ gen.initialize(2048);
+ this.key = gen.generateKeyPair();
+ }
+
+ /**
+ * EFFECT: Install the CA certificate.
+ * MODIFIES: this
+ * REQUIRES:
+ * - The new certificate must have the same algorithm and public key as getPublicKey(), except for testing purpose
+ * - It must be a v3 certificate
+ * - It must have basicConstraints { cA = TRUE }
+ * - It must contain key usage Digital Signature, Certificate Sign, CRL Sign
+ * - getCertificate() must be null (i.e., no certificate is installed yet).
+ */
+ public void installCertificate(Certificate certificate) {
+ this.certificate = certificate;
+ }
+
+ /**
+ * EFFECTS: Generate a CSR based on public key. It will have subject = CN=JCA.
+ */
+ private CertificationRequestInfo generateCSR() throws ParseException {
+ return new CertificationRequestInfo(ASN1Object.TAG_SEQUENCE, null,
+ new Int(Int.TAG, null, CertificationRequestInfo.VERSION_V1),
+ 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, "JCA"))
+ })
+ }),
+ getCAPublicKeyInfo(),
+ new Attributes(new Tag(TagClass.CONTEXT_SPECIFIC, true, 0), // IMPLICIT
+ null,
+ new Attribute[]{
+ new Attribute(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ new Integer[]{ 1, 3, 6, 1, 4, 1, 311, 13, 2, 3 }),
+ new Values(ASN1Object.TAG_SET, null,
+ new ASN1Object[]{
+ new IA5String(IA5String.TAG, null,
+ "10.0.20348.2")
+ }))}));
+ }
+
+ private Byte[] getPubKeyBitStream() {
+ final RSAPublicKey pub = (RSAPublicKey) key.getPublic();
+ final BigInteger exponent = pub.getPublicExponent();
+ byte[] modules = pub.getModulus().toByteArray();
+ final Int asn1Exponent = new Int(Int.TAG, null, exponent);
+ // Use OctetString to avoid leading zero issues.
+ final ASN1Object asn1Modules = new OctetString(Int.TAG, null, Utils.byteToByte(modules));
+ final Byte[] asn1ExponentDER = asn1Exponent.encodeDER();
+ final Byte[] asn1ModulesDER = asn1Modules.encodeDER();
+ return Stream.of(Arrays.asList(ASN1Object.TAG_SEQUENCE.encodeDER()),
+ Arrays.asList(new ASN1Length(asn1ModulesDER.length + asn1ExponentDER.length).encodeDER()),
+ Arrays.asList(asn1ModulesDER),
+ Arrays.asList(asn1ExponentDER))
+ .flatMap(Collection::stream)
+ .toArray(Byte[]::new);
+ }
+
+ /**
+ * EFFECTS: Encode the RSA public key into SubjectPubicKeyInfo format (BIT STRING -> SEQUENCE -> { INT INT }).
+ */
+ public SubjectPublicKeyInfo getCAPublicKeyInfo() {
+ return new SubjectPublicKeyInfo(ASN1Object.TAG_SEQUENCE, null,
+ 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, getPubKeyBitStream()));
+ }
+
+ /**
+ * EFFECT: Generate CSR and sign it, so the CA can request itself a certificate.
+ * REQUIRES: The CA cert must not be installed.
+ */
+ public CertificationRequest signCSR()
+ throws ParseException, NoSuchAlgorithmException, SignatureException, InvalidKeyException {
+ final CertificationRequestInfo info = generateCSR();
+ return new CertificationRequest(ASN1Object.TAG_SEQUENCE, null,
+ info,
+ getSigningAlgorithm(),
+ new BitString(BitString.TAG, null, 0, signBytes(info.encodeDER())));
+ }
+
+ /**
+ * EFFECT: Return SHA256withRSA.
+ */
+ private AlgorithmIdentifier getSigningAlgorithm() {
+ return new AlgorithmIdentifier(ASN1Object.TAG_SEQUENCE, null,
+ new ObjectIdentifier(ObjectIdentifier.TAG, null,
+ ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION),
+ new Null(Null.TAG, null));
+ }
+
+ /**
+ * EFFECTS: Sign the CSR based on the template.
+ * REQUIRES: The CA cert must be installed first, req must have a subject, template must be enabled.
+ * MODIFIES: this
+ */
+ public Certificate signCert(CertificationRequestInfo req, Template template)
+ throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
+ final TbsCertificate newCert = generateCert(req, template);
+ final Certificate cert = new Certificate(ASN1Object.TAG_SEQUENCE, null,
+ newCert,
+ getSigningAlgorithm(),
+ new BitString(BitString.TAG, null, 0,
+ signBytes(newCert.encodeValueDER())));
+ this.signed.add(cert);
+ return cert;
+ }
+
+ /**
+ * EFFECTS: Hash the input message with SHA256 and sign it with RSA and get the signature.
+ */
+ private Byte[] signBytes(Byte[] message)
+ throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
+ final Signature signature = Signature.getInstance("SHA256withRSA");
+ signature.initSign(key.getPrivate());
+ signature.update(Utils.byteToByte(message));
+ return Utils.byteToByte(signature.sign());
+ }
+
+ /**
+ * EFFECTS: Apply the template.
+ * For the new certificate:
+ * - Issuer will be set to CA#getCertificate()#getSubject()
+ * - The template will be applied (subject, validity, cdp)
+ * - A serial number will be generated
+ */
+ private TbsCertificate generateCert(CertificationRequestInfo req, Template template) {
+ final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
+ return new TbsCertificate(ASN1Object.TAG_SEQUENCE, null,
+ new Int(Int.TAG, new Tag(TagClass.CONTEXT_SPECIFIC, true, 0),
+ TbsCertificate.VERSION_V3),
+ new Int(Int.TAG, null, serial++),
+ getSigningAlgorithm(),
+ certificate.getCertificate().getSubject(),
+ new Validity(ASN1Object.TAG_SEQUENCE, null,
+ new GeneralizedTime(GeneralizedTime.TAG, null, now),
+ new UtcTime(UtcTime.TAG, null,
+ now.plusDays(template.getValidity()))),
+ template.getSubject() == null ? req.getSubject() :
+ template.getSubject(),
+ req.getSubjectPKInfo(),
+ null);
+ }
+
+ /**
+ * EFFECTS: Add the revocation info to revoked list.
+ * REQUIRES: revoked should have the serial of an issued certificate; its date should be current.
+ * MODIFIES: this
+ */
+ public void revoke(RevokedCertificate rev) {
+ revoked.add(rev);
+ }
+
+ /**
+ * EFFECTS: Generate and sign the CRL, based on getRevokedCerts(). The CSR will have current time as thisUpdate with
+ * no nextUptime, and it will have issuer same as the CA's subject.
+ * REQUIRES: The CA cert must be installed first.
+ */
+ public CertificateList signCRL()
+ throws NoSuchAlgorithmException, SignatureException, InvalidKeyException {
+ CertificateListContent content = new CertificateListContent(ASN1Object.TAG_SEQUENCE, null,
+ certificate.getCertificate().getSubject(),
+ getSigningAlgorithm(),
+ new GeneralizedTime(GeneralizedTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))),
+ null,
+ revoked.toArray(new RevokedCertificate[0]));
+ return new CertificateList(ASN1Object.TAG_SEQUENCE, null,
+ content,
+ getSigningAlgorithm(),
+ new BitString(BitString.TAG, null, 0,
+ signBytes(content.encodeValueDER())));
+ }
+
+ public Certificate getCertificate() {
+ return certificate;
+ }
+
+ public List<Certificate> getSigned() {
+ return signed;
+ }
+
+ /**
+ * EFFECTS: Get the public key, or null if no private key is installed.
+ */
+ public PublicKey getPublicKey() {
+ if (key == null) {
+ return null;
+ }
+ return key.getPublic();
+ }
+
+ public List<RevokedCertificate> getRevoked() {
+ return revoked;
+ }
+
+ public int getSerial() {
+ return serial;
+ }
+}
diff --git a/src/main/model/ca/Template.java b/src/main/model/ca/Template.java
new file mode 100644
index 0000000..ff2510e
--- /dev/null
+++ b/src/main/model/ca/Template.java
@@ -0,0 +1,104 @@
+package model.ca;
+
+import model.asn1.*;
+import model.asn1.exceptions.ParseException;
+import model.pki.cert.TbsCertificate;
+import model.x501.AttributeTypeAndValue;
+import model.x501.Name;
+import model.x501.RelativeDistinguishedName;
+
+import java.util.List;
+
+/**
+ * Represents a certificate template. Certificate templates are like policies the define part of the issued certificates
+ * of what to have in common.
+ */
+public class Template {
+ /**
+ * Name of the template.
+ */
+ private String name;
+
+ /**
+ * Whether the template is usable or not.
+ */
+ private boolean enabled;
+
+ /**
+ * Subject of the issued certs. Null -> unspecified
+ */
+ private Name subject;
+
+ /**
+ * Length of validity in days since the point of issue.
+ */
+ private long validity;
+
+ /**
+ * EFFECTS: Init with all given parameters.
+ * REQUIRES: name should be non-null; subject should be a valid X.509 subject name; validity should be > 0
+ */
+ public Template(String name,
+ boolean enabled,
+ Name subject,
+ long validity) {
+ this.name = name;
+ this.enabled = enabled;
+ this.subject = subject;
+ this.validity = validity;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public Name getSubject() {
+ return subject;
+ }
+
+ public void setSubject(Name subject) {
+ this.subject = subject;
+ }
+
+ /**
+ * EFFECTS: Set the subject to CN=commonName,C=CA
+ * Throws {@link ParseException} if commonName is not a valid PrintableString
+ */
+ public void setSubject(String commonName) throws ParseException {
+ if (commonName == null) {
+ this.subject = null;
+ return;
+ }
+ setSubject(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, commonName))}),
+ 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"))})}));
+ }
+
+ public long getValidity() {
+ return validity;
+ }
+
+ public void setValidity(long validity) {
+ this.validity = validity;
+ }
+}
diff --git a/src/main/model/pki/cert/TbsCertificate.java b/src/main/model/pki/cert/TbsCertificate.java
index 1175456..ce228af 100644
--- a/src/main/model/pki/cert/TbsCertificate.java
+++ b/src/main/model/pki/cert/TbsCertificate.java
@@ -229,6 +229,21 @@ public class TbsCertificate extends ASN1Object {
.toArray(Byte[]::new);
}
+ /**
+ * EFFECT: Get the extension by ID. If the certificate is V1 or does not have any extensions or does not have the
+ * specified extension, null is returned.
+ * REQUIRES: extnId should be a valid X.509 certificate extension ID.
+ */
+ public Extension getExtension(Integer[] extnId) {
+ if (extensions == null) {
+ return null;
+ }
+ return Arrays.stream(extensions.getExtensions())
+ .filter(extn -> Arrays.equals(extnId, extn.getExtnId().getInts()))
+ .findFirst()
+ .orElse(null);
+ }
+
public Int getVersion() {
return version;
}
diff --git a/src/main/model/pki/crl/CertificateListContent.java b/src/main/model/pki/crl/CertificateListContent.java
index 6f75d71..c7e901d 100644
--- a/src/main/model/pki/crl/CertificateListContent.java
+++ b/src/main/model/pki/crl/CertificateListContent.java
@@ -70,8 +70,8 @@ public class CertificateListContent extends ASN1Object {
.flatMap(Arrays::stream)
.collect(Collectors.toList());
return Stream.of(Arrays.asList(version.encodeDER()),
- Arrays.asList(issuer.encodeDER()),
Arrays.asList(signature.encodeDER()),
+ Arrays.asList(issuer.encodeDER()),
Arrays.asList(thisUpdate.encodeDER()),
nextUpdate == null ? Collections.<Byte>emptyList() : Arrays.asList(nextUpdate.encodeDER()),
Arrays.asList(new Tag(TagClass.UNIVERSAL, true, 0x30).encodeDER()),
diff --git a/src/main/ui/IssueScreen.java b/src/main/ui/IssueScreen.java
new file mode 100644
index 0000000..e152b0d
--- /dev/null
+++ b/src/main/ui/IssueScreen.java
@@ -0,0 +1,138 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.ca.Template;
+import model.csr.CertificationRequest;
+import model.pki.cert.Certificate;
+
+public class IssueScreen implements UIHandler {
+ private final JCA session;
+
+ private Template template;
+ private CertificationRequest incomingCSR;
+
+ /**
+ * EFFECTS: Init with the session.
+ */
+ public IssueScreen(JCA session) {
+ this.session = session;
+ }
+
+ /**
+ * EFFECTS: Set current template and CSR in use by args.
+ * REQUIRES: args.length = 2, args[0] instanceof CertificateRequest, args[1] instanceof Template
+ * MODIFIES: args[1]
+ */
+ @Override
+ public void enter(Object... args) {
+ this.incomingCSR = (CertificationRequest) args[0];
+ this.template = (Template) args[1];
+ }
+
+ @Override
+ public void help() {
+ System.out.print("show\tView the current certificate\n"
+ + "set\tSet properties or template\n"
+ + "commit\tIssue the certificate\n"
+ + "exit\tDiscard and go to main menu\n"
+ + "help\tPrint this message\n");
+ }
+
+ @Override
+ public void show() {
+ System.out.println("Requested Subject:\t" + incomingCSR.getCertificationRequestInfo().getSubject());
+ System.out.println("Subject:\t" + (template.getSubject() == null
+ ? incomingCSR.getCertificationRequestInfo().getSubject()
+ : template.getSubject()));
+ System.out.println("Template:\t" + template.getName());
+ System.out.println("Validity:\t" + template.getValidity() + " days");
+ }
+
+ @Override
+ public void commit() {
+ try {
+ Certificate certificate = session.getCa().signCert(incomingCSR.getCertificationRequestInfo(), template);
+ System.out.println(Utils.toPEM(certificate.encodeDER(), "CERTIFICATE"));
+ session.log("A certificate was issued.");
+ session.setScreen(Screen.MAIN);
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void handleIssueSetSubject(String val) {
+ try {
+ template.setSubject(val);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void handleIssueSetValidity(String val) {
+ if (val == null) {
+ System.out.println("Cannot unset validity");
+ return;
+ }
+ try {
+ long i = Long.parseLong(val);
+ if (i <= 0) {
+ System.out.println("Invalid validity days");
+ return;
+ }
+ template.setValidity(i);
+ } catch (NumberFormatException ignored) {
+ System.out.println("Invalid validity days");
+ }
+ }
+
+ private void handleIssueSet(String... args) {
+ if (args.length != 2 && args.length != 3) {
+ System.out.println("Usage: set <key> <value>");
+ System.out.println("Supported keys: subject validity");
+ return;
+ }
+ String val = args.length == 3 ? args[2] : null;
+ switch (args[1]) {
+ case "subject":
+ handleIssueSetSubject(val);
+ break;
+ case "validity":
+ handleIssueSetValidity(val);
+ break;
+ default:
+ System.out.println("Unknown key");
+ break;
+ }
+ }
+
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "set":
+ handleIssueSet(args);
+ break;
+ default:
+ help();
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Clear the certificates and return main.
+ * MODIFIES: this
+ */
+ @Override
+ public Screen exit() {
+ incomingCSR = null;
+ template = null;
+ return Screen.MAIN;
+ }
+
+ @Override
+ public String getPS1() {
+ return String.format("/%s/ %%", template.getSubject() == null
+ ? incomingCSR.getCertificationRequestInfo().getSubject()
+ : template.getSubject());
+ }
+}
diff --git a/src/main/ui/JCA.java b/src/main/ui/JCA.java
new file mode 100644
index 0000000..f9467ea
--- /dev/null
+++ b/src/main/ui/JCA.java
@@ -0,0 +1,203 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import model.ca.AuditLogEntry;
+import model.ca.CACertificate;
+import model.ca.Template;
+
+import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+/**
+ * Main program
+ */
+public class JCA {
+ /**
+ * The current screen.
+ */
+ private UIHandler screen;
+
+ /**
+ * Instances of the five screens;
+ */
+ private final UIHandler mainScreen;
+ private final UIHandler mgmtScreen;
+ private final UIHandler issueScreen;
+ private final UIHandler templatesScreen;
+ private final UIHandler templateSetScreen;
+
+ /**
+ * Templates
+ */
+ private final List<Template> templates;
+
+ /**
+ * The CA
+ */
+ private final CACertificate ca;
+
+ /**
+ * Audit logs
+ */
+ private final List<AuditLogEntry> logs;
+
+ /**
+ * Current user
+ */
+ private final String user;
+
+ /**
+ * EFFECTS: Init with main screen, empty templates, logs, user 'yuuta', and generate a private key with no CA cert.
+ * Throws {@link NoSuchAlgorithmException} when crypto issue happens.
+ */
+ public JCA() throws NoSuchAlgorithmException {
+ this.mainScreen = new MainScreen(this);
+ this.mgmtScreen = new MgmtScreen(this);
+ this.issueScreen = new IssueScreen(this);
+ this.templatesScreen = new TemplatesScreen(this);
+ this.templateSetScreen = new TemplateSetScreen(this);
+
+ setScreen(Screen.MAIN);
+
+ this.templates = new ArrayList<>();
+ this.ca = new CACertificate();
+ this.logs = new ArrayList<>();
+ this.user = "yuuta";
+
+ this.ca.generateKey();
+ }
+
+ /**
+ * EFFECT: Checks if the CA is installed or not (according to the desired state) and print if not matching. Returns
+ * true if matching.
+ */
+ public boolean checkCA(boolean requireInstalled) {
+ if (requireInstalled && ca.getCertificate() == null) {
+ System.out.println("The CA is not installed yet");
+ return false;
+ } else if (!requireInstalled && ca.getCertificate() != null) {
+ System.out.println("The CA is already installed");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * EFFECTS: Read PEM from stdin, matched the given tag.
+ * Throws {@link ParseException} if the input is incorrect.
+ */
+ public Byte[] handleInputPEM(String desiredTag) throws ParseException {
+ final Scanner scanner = new Scanner(System.in);
+ StringBuilder in = new StringBuilder();
+ while (true) {
+ final String line = scanner.nextLine();
+ in.append(line);
+ in.append("\n");
+ if (line.matches("-----END .*-----")) {
+ break;
+ }
+ }
+ return Utils.parsePEM(Utils.byteToByte(in.toString().getBytes(StandardCharsets.UTF_8)), desiredTag);
+ }
+
+ /**
+ * EFFECTS: Find the template based on name, or null if not found.
+ */
+ public Template findTemplate(String name, boolean requireEnabled) {
+ Optional<Template> opt = templates.stream().filter(temp -> {
+ if (requireEnabled && !temp.isEnabled()) {
+ return false;
+ }
+ return temp.getName().equals(name);
+ }).findFirst();
+ return opt.orElse(null);
+ }
+
+ /**
+ * EFFECT: Set the current screen with optional args. Exit the program when mode is null.
+ * MODIFIES: this
+ */
+ public void setScreen(Screen mode, Object... args) {
+ if (mode == null) {
+ System.exit(0);
+ }
+ switch (mode) {
+ case MAIN:
+ this.screen = mainScreen;
+ break;
+ case MGMT:
+ this.screen = mgmtScreen;
+ break;
+ case ISSUE:
+ this.screen = issueScreen;
+ break;
+ case TEMPLATES:
+ this.screen = templatesScreen;
+ break;
+ case TEMPLATE_SET:
+ this.screen = templateSetScreen;
+ break;
+ }
+ screen.enter(args);
+ }
+
+ private void handleLine(String... args) {
+ if (!args[0].isBlank()) {
+ switch (args[0]) {
+ case "help":
+ screen.help();
+ break;
+ case "show":
+ screen.show();
+ break;
+ case "commit":
+ screen.commit();
+ break;
+ case "exit":
+ setScreen(screen.exit());
+ break;
+ default:
+ screen.command(args);
+ break;
+ }
+ }
+ printPS1();
+ }
+
+ private void printPS1() {
+ System.out.printf("%s@JCA %s ", user, screen.getPS1());
+ }
+
+ /**
+ * EFFECT: Log an action to the audit log
+ * MODIFIES: this
+ */
+ public void log(String action) {
+ this.logs.add(new AuditLogEntry(user, ZonedDateTime.now(), action));
+ }
+
+ /**
+ * EFFECTS: Run the program
+ */
+ public void run() {
+ printPS1();
+ final Scanner scanner = new Scanner(System.in);
+ while (true) {
+ handleLine(scanner.nextLine().split(" "));
+ }
+ }
+
+ public List<Template> getTemplates() {
+ return templates;
+ }
+
+ public CACertificate getCa() {
+ return ca;
+ }
+
+ public List<AuditLogEntry> getLogs() {
+ return logs;
+ }
+}
diff --git a/src/main/ui/Main.java b/src/main/ui/Main.java
index d80be21..1429532 100644
--- a/src/main/ui/Main.java
+++ b/src/main/ui/Main.java
@@ -1,7 +1,7 @@
package ui;
public class Main {
- public static void main(String[] args) {
-
+ public static void main(String[] args) throws Throwable {
+ new JCA().run();
}
}
diff --git a/src/main/ui/MainScreen.java b/src/main/ui/MainScreen.java
new file mode 100644
index 0000000..69cb32c
--- /dev/null
+++ b/src/main/ui/MainScreen.java
@@ -0,0 +1,200 @@
+package ui;
+
+import model.asn1.ASN1Object;
+import model.asn1.UtcTime;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.ca.Template;
+import model.csr.CertificationRequest;
+import model.pki.cert.Certificate;
+import model.pki.crl.Reason;
+import model.pki.crl.RevokedCertificate;
+
+import java.io.*;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+
+public class MainScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the parent session.
+ */
+ public MainScreen(JCA session) {
+ this.session = session;
+ }
+
+ @Override
+ public void help() {
+ System.out.print("mgmt\tView and manage the CA certificate\n"
+ + "issue\tIssue a certificate\n"
+ + "show\tList all issued certificates\n"
+ + "export\tExport a certificate to file (DER)\n"
+ + "template\tManage templates\n"
+ + "revoke\tRevoke a certificate\n"
+ + "crl\t\tSign CRL\n"
+ + "log\t\tView audit logs\n"
+ + "exit\tExit\n"
+ + "help\tPrint this message\n");
+ }
+
+ @Override
+ public void show() {
+ session.getCa().getSigned().forEach(cert -> {
+ System.out.printf("%s\t%d\t%s\n",
+ cert.getCertificate().getSubject().toString(),
+ cert.getCertificate().getSerialNumber().getLong(),
+ session.getCa().getRevoked().stream().anyMatch(rev -> rev.getSerialNumber().getLong()
+ == cert.getCertificate().getSerialNumber().getLong()) ? "REVOKED" : "OK");
+ });
+ }
+
+ private CertificationRequest handleIssueInputCSR() {
+ try {
+ return new CertificationRequest(new BytesReader(session.handleInputPEM("CERTIFICATE REQUEST")),
+ false);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ return null;
+ }
+ }
+
+ private void handleIssue(String... args) {
+ if (!session.checkCA(true)) {
+ return;
+ }
+ if (args.length <= 1) {
+ System.out.println("Usage: issue <template>");
+ return;
+ }
+ Template tmp = session.findTemplate(args[1], true);
+ if (tmp == null) {
+ System.out.println("Cannot find the template specified");
+ return;
+ }
+ CertificationRequest req = handleIssueInputCSR();
+ if (req != null) {
+ session.setScreen(Screen.ISSUE, req, new Template(tmp.getName(),
+ true,
+ tmp.getSubject(),
+ tmp.getValidity()));
+ }
+ }
+
+ /**
+ * EFFECTS: Find issued and not revoked certificate by serial. Return null if not found.
+ */
+ private Certificate findCertBySerial(int serial) {
+ Optional<Certificate> c = session.getCa().getSigned()
+ .stream()
+ .filter(cert -> cert.getCertificate().getSerialNumber().getLong() == serial)
+ .findFirst();
+ if (c.isEmpty()) {
+ System.out.println("Cannot find the certificate specified");
+ return null;
+ }
+ if (session.getCa().getRevoked().stream().anyMatch(rev -> rev.getSerialNumber().getLong() == serial)) {
+ System.out.println("The certificate has already been revoked.");
+ return null;
+ }
+ return c.get();
+ }
+
+ private void handleRevoke(String... args) {
+ if (args.length < 3) {
+ System.out.println("Usage: revoke <serial> <reason>");
+ return;
+ }
+ try {
+ final Reason reason = Reason.valueOf(args[2]);
+ int serial = Integer.parseInt(args[1]);
+ Certificate c = findCertBySerial(serial);
+ if (c == null) {
+ return;
+ }
+ session.getCa().revoke(new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null,
+ c.getCertificate().getSerialNumber(),
+ new UtcTime(UtcTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))), reason));
+ session.log("A certificate has been revoked.");
+ } catch (IllegalArgumentException ignored) {
+ System.out.println("Illegal serial number or reason");
+ }
+ }
+
+ private void handleExport(String... args) {
+ if (args.length < 3) {
+ System.out.println("Usage: export <serial> <path>");
+ return;
+ }
+ try {
+ int serial = Integer.parseInt(args[1]);
+ Certificate c = findCertBySerial(serial);
+ if (c == null) {
+ return;
+ }
+ final File fd = new File(args[2]);
+ final OutputStream out = new FileOutputStream(fd);
+ out.write(Utils.byteToByte(c.encodeDER()));
+ out.close();
+ } catch (IllegalArgumentException ignored) {
+ System.out.println("Illegal serial number or reason");
+ } catch (IOException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void handleCRL() {
+ if (!session.checkCA(true)) {
+ return;
+ }
+ try {
+ System.out.println(Utils.toPEM(session.getCa().signCRL().encodeDER(), "X509 CRL"));
+ session.log("A CRL was signed");
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void handleLog() {
+ session.getLogs().forEach(System.out::println);
+ }
+
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "mgmt":
+ session.setScreen(Screen.MGMT);
+ return;
+ case "issue":
+ handleIssue(args);
+ return;
+ case "revoke":
+ handleRevoke(args);
+ return;
+ case "export":
+ handleExport(args);
+ return;
+ case "template":
+ session.setScreen(Screen.TEMPLATES);
+ return;
+ case "crl":
+ handleCRL();
+ return;
+ case "log":
+ handleLog();
+ return;
+ }
+ help();
+ }
+
+ @Override
+ public Screen exit() {
+ return null;
+ }
+
+ @Override
+ public String getPS1() {
+ return "/ %";
+ }
+}
diff --git a/src/main/ui/MgmtScreen.java b/src/main/ui/MgmtScreen.java
new file mode 100644
index 0000000..613aa50
--- /dev/null
+++ b/src/main/ui/MgmtScreen.java
@@ -0,0 +1,170 @@
+package ui;
+
+import model.asn1.ASN1Object;
+import model.asn1.BitString;
+import model.asn1.Bool;
+import model.asn1.ObjectIdentifier;
+import model.asn1.exceptions.ParseException;
+import model.asn1.parsing.BytesReader;
+import model.csr.CertificationRequest;
+import model.pki.SubjectPublicKeyInfo;
+import model.pki.cert.Certificate;
+import model.pki.cert.Extension;
+import model.pki.cert.TbsCertificate;
+
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.BitSet;
+
+public class MgmtScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the parent session.
+ */
+ public MgmtScreen(JCA session) {
+ this.session = session;
+ }
+
+ @Override
+ public void help() {
+ System.out.print("show\tView the public key and CA certificate\n"
+ + "csr\tGenerate a CSR for a upper-level CA to sign\n"
+ + "install\tInstall a CA certificate\n"
+ + "exit\tGo to main menu\n"
+ + "help\tPrint this message\n");
+ }
+
+ /**
+ * EFFECTS: Format the public key and CA
+ */
+ @Override
+ public void show() {
+ System.out.printf("Public Key:\t%s\n",
+ Base64.getEncoder().encodeToString(session.getCa().getPublicKey().getEncoded()));
+ if (!session.checkCA(true)) {
+ return;
+ }
+ final TbsCertificate info = session.getCa().getCertificate().getCertificate();
+ System.out.printf("Subject:\t%s\n", info.getSubject().toString());
+ System.out.printf("Issuer:\t%s\n", info.getIssuer().toString());
+ System.out.printf("Not Before:\t%s\n", info.getValidity().getNotBefore().getTimestamp());
+ System.out.printf("Not After:\t%s\n", info.getValidity().getNotAfter().getTimestamp());
+ System.out.printf("Signature:\t%s\n",
+ Base64.getEncoder().encodeToString(Utils.byteToByte(info.getSubjectPublicKeyInfo()
+ .getSubjectPublicKey().getConvertedVal())));
+ }
+
+ private void handleCSR() {
+ if (!session.checkCA(false)) {
+ return;
+ }
+ try {
+ CertificationRequest req = session.getCa().signCSR();
+ System.out.println(Utils.toPEM(req.encodeDER(), "CERTIFICATE REQUEST"));
+ session.log("Signed CA CSR.");
+ } catch (Throwable e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void validateCACertificateVersion(Certificate cert) throws ParseException {
+ if (cert.getCertificate().getVersion() == null
+ || cert.getCertificate().getVersion().getLong() != TbsCertificate.VERSION_V3) {
+ throw new ParseException("The input certificate must be V3");
+ }
+ }
+
+ private void validateCACertificatePublicKey(Certificate cert) throws ParseException {
+ final SubjectPublicKeyInfo expectedPKInfo = session.getCa().getCAPublicKeyInfo();
+ if (!Arrays.equals(cert.getCertificate().getSubjectPublicKeyInfo().getAlgorithm().getType().getInts(),
+ expectedPKInfo.getAlgorithm().getType().getInts())
+ || !Arrays.equals(cert.getCertificate().getSubjectPublicKeyInfo().getSubjectPublicKey().getVal(),
+ expectedPKInfo.getSubjectPublicKey().getVal())) {
+ throw new ParseException("The input certificate does not have the corresponding public key");
+ }
+ }
+
+ private void validateCACertificateBasicConstraints(Certificate cert) throws ParseException {
+ final Extension basicConstraints = cert.getCertificate().getExtension(ObjectIdentifier.OID_BASIC_CONSTRAINTS);
+ if (basicConstraints == null
+ || basicConstraints.getExtnValue().getBytes().length <= 0) {
+ throw new ParseException("The certificate does not have a valid basicConstraints extension.");
+ }
+ final ASN1Object basicConstraintsValue =
+ new ASN1Object(new BytesReader(basicConstraints.getExtnValue().getBytes()), false);
+ if (basicConstraintsValue.getLength() <= 0) {
+ throw new ParseException("The certificate does not have a valid basicConstraints extension.");
+ }
+ final ASN1Object bool =
+ ASN1Object.parse(new BytesReader(basicConstraintsValue.encodeValueDER()), false);
+ if (!(bool instanceof Bool)
+ || !((Bool) bool).getValue()) {
+ throw new ParseException("The certificate does not have a valid basicConstraints extension.");
+ }
+ }
+
+ private void validateCACertificateKeyUsage(Certificate cert) throws ParseException {
+ final Extension keyUsage = cert.getCertificate().getExtension(ObjectIdentifier.OID_KEY_USAGE);
+ if (keyUsage == null
+ || keyUsage.getExtnValue().getBytes().length <= 0) {
+ throw new ParseException("The certificate does not have a valid keyUsage extension.");
+ }
+ final ASN1Object keyUsageValue =
+ ASN1Object.parse(new BytesReader(keyUsage.getExtnValue().getBytes()), false);
+ if (keyUsageValue.getLength() <= 0
+ || !(keyUsageValue instanceof BitString)) {
+ throw new ParseException("The certificate does not have a valid keyUsage extension.");
+ }
+ final BitSet bitSet = BitSet.valueOf(Utils.byteToByte(((BitString) keyUsageValue).getVal()));
+ if (!bitSet.get(7) || !bitSet.get(2) || !bitSet.get(1)) {
+ throw new ParseException("The certificate does not have a valid keyUsage extension.");
+ }
+ }
+
+ private void handleInstall() {
+ if (!session.checkCA(false)) {
+ return;
+ }
+ try {
+ final Byte[] in = session.handleInputPEM("CERTIFICATE");
+ Certificate cert = new Certificate(new BytesReader(in), false);
+ validateCACertificateVersion(cert);
+ validateCACertificatePublicKey(cert);
+ validateCACertificateBasicConstraints(cert);
+ validateCACertificateKeyUsage(cert);
+ session.getCa().installCertificate(cert);
+ session.log("A CA certificate is installed.");
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "csr":
+ handleCSR();
+ break;
+ case "install":
+ handleInstall();
+ break;
+ default:
+ help();
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Go to main menu
+ */
+ @Override
+ public Screen exit() {
+ return Screen.MAIN;
+ }
+
+ @Override
+ public String getPS1() {
+ return "/ca/ #";
+ }
+} \ No newline at end of file
diff --git a/src/main/ui/Screen.java b/src/main/ui/Screen.java
new file mode 100644
index 0000000..31370e1
--- /dev/null
+++ b/src/main/ui/Screen.java
@@ -0,0 +1,31 @@
+package ui;
+
+/**
+ * The screen type
+ */
+public enum Screen {
+ /**
+ * Main menu (mgmt, issue, template, crl, show, revoke, log)
+ */
+ MAIN,
+
+ /**
+ * The CA management menu (show, csr, install)
+ */
+ MGMT,
+
+ /**
+ * The issue menu (show, set, commit)
+ */
+ ISSUE,
+
+ /**
+ * The templates menu (show, add, enable, disable, remove)
+ */
+ TEMPLATES,
+
+ /**
+ * The template edit menu (show, set, commit)
+ */
+ TEMPLATE_SET
+}
diff --git a/src/main/ui/TemplateSetScreen.java b/src/main/ui/TemplateSetScreen.java
new file mode 100644
index 0000000..9a31f50
--- /dev/null
+++ b/src/main/ui/TemplateSetScreen.java
@@ -0,0 +1,131 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import model.ca.Template;
+
+public class TemplateSetScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with session.
+ */
+ public TemplateSetScreen(JCA session) {
+ this.session = session;
+ }
+
+ private Template template;
+
+ @Override
+ public void help() {
+ System.out.println("show\tView the current template settings\n"
+ + "set\tSet key value\n"
+ + "commit\tSave the template\n"
+ + "exit\tDiscard changes\n"
+ + "help\tPrint this help message\n");
+ }
+
+ private void handleSetSubject(String val) {
+ try {
+ template.setSubject(val);
+ } catch (ParseException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ private void handleSetValidity(String val) {
+ if (val == null) {
+ System.out.println("Cannot unset validity");
+ return;
+ }
+ try {
+ long i = Long.parseLong(val);
+ if (i <= 0) {
+ System.out.println("Invalid validity days");
+ return;
+ }
+ template.setValidity(i);
+ } catch (NumberFormatException ignored) {
+ System.out.println("Invalid validity days");
+ }
+ }
+
+ private void handleSet(String... args) {
+ if (args.length != 2 && args.length != 3) {
+ System.out.println("Usage: set <key> <value>");
+ System.out.println("Supported keys: subject validity");
+ return;
+ }
+ String val = args.length == 3 ? args[2] : null;
+ switch (args[1]) {
+ case "subject":
+ handleSetSubject(val);
+ break;
+ case "validity":
+ handleSetValidity(val);
+ break;
+ default:
+ System.out.println("Unknown key");
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Add the template to store and switch to templates screen.
+ * MODIFIES: session
+ */
+ @Override
+ public void commit() {
+ session.getTemplates().add(template);
+ session.setScreen(Screen.TEMPLATES);
+ session.log("A new template is added.");
+ }
+
+ /**
+ * EFFECTS: Show template info.
+ */
+ @Override
+ public void show() {
+ System.out.println("Subject:\t" + template.getSubject());
+ System.out.println("Validity:\t" + template.getValidity() + " days");
+ }
+
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "set":
+ handleSet(args);
+ break;
+ default:
+ case "help":
+ help();
+ break;
+ }
+ }
+
+ /**
+ * EFFECTS: Return to templates list and clear the current template in editing.
+ */
+ @Override
+ public Screen exit() {
+ template = null;
+ return Screen.TEMPLATES;
+ }
+
+ /**
+ * EFFECTS: yuuta@JCA /templates/name/ %
+ */
+ @Override
+ public String getPS1() {
+ return String.format("/templates/%s/ %%", template.getName());
+ }
+
+ /**
+ * EFFECT: Edit args[0].
+ * REQUIRES: args.length = 1; args[0] instanceof Template
+ * MODIFIES: args[0]
+ */
+ @Override
+ public void enter(Object... args) {
+ template = (Template) args[0];
+ }
+} \ No newline at end of file
diff --git a/src/main/ui/TemplatesScreen.java b/src/main/ui/TemplatesScreen.java
new file mode 100644
index 0000000..9b0bf3e
--- /dev/null
+++ b/src/main/ui/TemplatesScreen.java
@@ -0,0 +1,108 @@
+package ui;
+
+import model.ca.Template;
+
+public class TemplatesScreen implements UIHandler {
+ private final JCA session;
+
+ /**
+ * EFFECTS: Init with the session.
+ */
+ public TemplatesScreen(JCA session) {
+ this.session = session;
+ }
+
+ @Override
+ public void help() {
+ System.out.println("show\tList templates\n"
+ + "add\tCreate a new template\n"
+ + "enable\tEnable a template\n"
+ + "disable\tDisable a template\n"
+ + "delete\tDelete a template\n"
+ + "exit\tGo to main menu\n"
+ + "help\tPrint this message");
+ }
+
+ @Override
+ public void show() {
+ session.getTemplates().forEach(tem ->
+ System.out.printf("%s[%s]\t%s\t%d Days\n",
+ tem.getName(),
+ tem.isEnabled() ? "ENABLED" : "DISABLED",
+ tem.getSubject(),
+ tem.getValidity()));
+ }
+
+ private void handleAdd(String... args) {
+ if (args.length <= 1) {
+ System.out.println("Usage: add <name>");
+ return;
+ }
+ if (session.findTemplate(args[1], false) != null) {
+ System.out.println("The template already exists.");
+ return;
+ }
+
+ session.setScreen(Screen.TEMPLATE_SET,
+ new Template(args[1], false, null, 30));
+ }
+
+ private void handleEnableDisable(boolean enable, String... args) {
+ if (args.length <= 1) {
+ System.out.printf("Usage: %s <template>\n", enable ? "enable" : "disable");
+ return;
+ }
+ Template tmp = session.findTemplate(args[1], false);
+ if (tmp == null) {
+ System.out.println("Cannot find the template specified");
+ return;
+ }
+ tmp.setEnabled(enable);
+ session.log("A template was enabled / disabled.");
+ }
+
+ private void handleDelete(String... args) {
+ if (args.length <= 1) {
+ System.out.println("Usage: delete <template>");
+ return;
+ }
+ Template tmp = session.findTemplate(args[1], true);
+ if (tmp == null) {
+ System.out.println("Cannot find the template specified");
+ return;
+ }
+ session.getTemplates().remove(tmp);
+ session.log("A template was deleted.");
+ }
+
+ @Override
+ public void command(String... args) {
+ switch (args[0]) {
+ case "add":
+ handleAdd(args);
+ break;
+ case "enable":
+ handleEnableDisable(true, args);
+ break;
+ case "disable":
+ handleEnableDisable(false, args);
+ break;
+ case "delete":
+ handleDelete(args);
+ break;
+ default:
+ help();
+ break;
+ }
+ }
+
+ @Override
+ public Screen exit() {
+ return Screen.MAIN;
+ }
+
+ @Override
+ public String getPS1() {
+ return "/templates/ %";
+ }
+}
diff --git a/src/main/ui/UIHandler.java b/src/main/ui/UIHandler.java
new file mode 100644
index 0000000..f451542
--- /dev/null
+++ b/src/main/ui/UIHandler.java
@@ -0,0 +1,45 @@
+package ui;
+
+/**
+ * Represents a screen
+ */
+public interface UIHandler {
+ /**
+ * EFFECTS: Called when the screen is switched to.
+ */
+ default void enter(Object... args) {
+
+ }
+
+ /**
+ * EFFECTS: Show objects. command() will not be called.
+ */
+ void show();
+
+ /**
+ * EFFECTS: Commit changes and exit. command() will not be called.
+ */
+ default void commit() {
+ }
+
+ /**
+ * EFFECTS: Discard changes and exit. command() will not be called. Returns the next screen.
+ */
+ Screen exit();
+
+ /**
+ * EFFECTS: Run help. command() will not be called.
+ */
+ void help();
+
+ /**
+ * EFFECTS: Any commands rather than commit / exit / help.
+ * REQUIRES: args != null && args.length >= 1
+ */
+ void command(String... args);
+
+ /**
+ * EFFECTS: Return the current PS1 prompt.
+ */
+ String getPS1();
+}
diff --git a/src/main/ui/Utils.java b/src/main/ui/Utils.java
index ccb244e..3ed1300 100644
--- a/src/main/ui/Utils.java
+++ b/src/main/ui/Utils.java
@@ -49,19 +49,8 @@ public final class Utils {
}
/**
- * 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.
+ * EFFECTS: Unpack the multibyte 64bit integer to its shortest array of byte format. Remove leading empty byte
+ * if that number is not zero.
*/
public static Byte[] valToByte(long val) {
byte[] v = BigInteger.valueOf(val).toByteArray();
@@ -77,15 +66,8 @@ public final class Utils {
}
/**
- * 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.
+ * EFFECTS: Decode the input PEM file, with optional check on tags. \n must present after each line, optional after
+ * the last.
* 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).
*/
@@ -93,7 +75,7 @@ public final class Utils {
final String str = new String(byteToByte(input), StandardCharsets.UTF_8);
Pattern pattern =
Pattern.compile("^-----BEGIN " + desiredTag
- + "-----$\n^(.*)$\n^-----END " + desiredTag + "-----$",
+ + "-----$\n^(.*)$\n^-----END " + desiredTag + "-----$\n*",
Pattern.DOTALL | Pattern.MULTILINE);
final Matcher matcher = pattern.matcher(str);
if (!matcher.matches()) {
@@ -102,4 +84,20 @@ public final class Utils {
final String b64 = matcher.group(1).replace("\n", "");
return byteToByte(Base64.getDecoder().decode(b64));
}
+
+ /**
+ * EFFECTS: Base64 encode the input bytes and convert them into PEM format.
+ * REQUIRES: desiredTag must be upper case and not empty.
+ */
+ public static String toPEM(Byte[] input, String tag) {
+ StringBuilder builder = new StringBuilder();
+ builder.append("-----BEGIN ");
+ builder.append(tag);
+ builder.append("-----\n");
+ builder.append(Base64.getEncoder().encodeToString(byteToByte(input)));
+ builder.append("\n-----END ");
+ builder.append(tag);
+ builder.append("-----");
+ return builder.toString();
+ }
}
diff --git a/src/test/model/TestConstants.java b/src/test/model/TestConstants.java
index 3356549..f0ba35a 100644
--- a/src/test/model/TestConstants.java
+++ b/src/test/model/TestConstants.java
@@ -505,8 +505,8 @@ public final class TestConstants {
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.getIssuer().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(),
@@ -516,8 +516,8 @@ public final class TestConstants {
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.getIssuer().encodeDER(),
CRL_CONTENT_2.getThisUpdate().encodeDER(),
CRL_CONTENT_2.getNextUpdate().encodeDER(),
combine((byte) 0x30, REVOKED_CESSATION_DER, REVOKED_KEY_COMPROMISE_DER));
diff --git a/src/test/model/asn1/IntTest.java b/src/test/model/asn1/IntTest.java
index 6e92eb0..23a5e23 100644
--- a/src/test/model/asn1/IntTest.java
+++ b/src/test/model/asn1/IntTest.java
@@ -4,6 +4,8 @@ import model.asn1.exceptions.ParseException;
import model.asn1.parsing.BytesReader;
import org.junit.jupiter.api.Test;
+import java.math.BigInteger;
+
import static org.junit.jupiter.api.Assertions.*;
public class IntTest {
@@ -11,6 +13,7 @@ public class IntTest {
void testConstructor() {
assertEquals(0x2, new Int(Int.TAG, null, 255).getTag().getNumber());
assertEquals(255, new Int(Int.TAG, null, 255).getLong());
+ assertEquals(255, new Int(Int.TAG, null, BigInteger.valueOf(255)).getLong());
}
@Test
diff --git a/src/test/model/ca/AuditLogEntryTest.java b/src/test/model/ca/AuditLogEntryTest.java
new file mode 100644
index 0000000..af44947
--- /dev/null
+++ b/src/test/model/ca/AuditLogEntryTest.java
@@ -0,0 +1,26 @@
+package model.ca;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.ZonedDateTime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AuditLogEntryTest {
+ @Test
+ void testConstructor() {
+ ZonedDateTime now = ZonedDateTime.now();
+ AuditLogEntry entry = new AuditLogEntry("user", now, "action123");
+ assertEquals("user", entry.getUser());
+ assertEquals(now, entry.getTime());
+ assertEquals("action123", entry.getAction());
+ }
+
+ @Test
+ void testToString() {
+ ZonedDateTime now = ZonedDateTime.now();
+ AuditLogEntry entry = new AuditLogEntry("user", now, "action123");
+ assertEquals(now + "\tuser\taction123",
+ entry.toString());
+ }
+}
diff --git a/src/test/model/ca/CACertificateTest.java b/src/test/model/ca/CACertificateTest.java
new file mode 100644
index 0000000..4db7bf4
--- /dev/null
+++ b/src/test/model/ca/CACertificateTest.java
@@ -0,0 +1,178 @@
+package model.ca;
+
+import model.TestConstants;
+import model.asn1.ASN1Object;
+import model.asn1.ObjectIdentifier;
+import model.asn1.UtcTime;
+import model.asn1.parsing.BytesReader;
+import model.csr.CertificationRequest;
+import model.csr.CertificationRequestInfo;
+import model.pki.cert.Certificate;
+import model.pki.crl.Reason;
+import model.pki.crl.RevokedCertificate;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import ui.Utils;
+
+import java.nio.charset.StandardCharsets;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class CACertificateTest {
+ private static final String CA = "-----BEGIN CERTIFICATE-----\n" +
+ "MIIC3zCCAoagAwIBAgIUWcL8J0hbxGSffN0fR76j8TlGxJswCgYIKoZIzj0EAwQw\n" +
+ "JDEVMBMGA1UEAwwMVGVzdCBSb290IENBMQswCQYDVQQGEwJDQTAeFw0yMzEwMTMw\n" +
+ "MTQ3MzFaFw0zMzEwMTAwMTQ3MzFaMA4xDDAKBgNVBAMTA0pDQTCCASIwDQYJKoZI\n" +
+ "hvcNAQEBBQADggEPADCCAQoCggEBAINbCR88MTUsx/poxNzXxN1aWt/DkkFrRA3r\n" +
+ "dHmLXQLjopULgHIJTshSq2jDe1QEYJ0Nrj9U9YclmxkWO0HvzedmTyl0YzAhPJXj\n" +
+ "HUK0T9sYSg+eE4WI03yuy7lGBJLUl9VEBR0JEZdy/mT5CRW44ryGGeeBNK3fqQrk\n" +
+ "5Rm9/wY5M2cKjYmvyp5D8E+HEd+FXNreO+r9pWpKSajPn+B6OwFUUESbRf8iWiF4\n" +
+ "v6ZLXDOBCEHFZcd2lTVHExuE+V3eDG3evn8HV5SB7FzRDZBV2Jz0Pfiqu2WlH4r8\n" +
+ "c1804G4WCjQlSX4bPs7994+KjUoFC95r40vexi2O9mVIIEF4LtkCAwEAAaOB4DCB\n" +
+ "3TAdBgNVHQ4EFgQU+c9PnChWwj4sWHFMN/dikzOS5o8wHwYDVR0jBBgwFoAUba4m\n" +
+ "yCy2hdnsc6Hhw4m8dvIbit0wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8E\n" +
+ "BAMCAYYwNQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2hvbWUueXV1dGEubW9lL3Br\n" +
+ "aS9yb290Y2EuY3JsMEAGCCsGAQUFBwEBBDQwMjAwBggrBgEFBQcwAoYkaHR0cDov\n" +
+ "L2hvbWUueXV1dGEubW9lL3BraS9yb290Y2EuY3J0MAoGCCqGSM49BAMEA0cAMEQC\n" +
+ "IETA9hpUnbrWpLfu2HUWr9UQC273jyg/nt30rJ96PNS+AiAsNzbKVyBpkG41Hf1/\n" +
+ "+355E7vortNonvf0DDGJZjC7MA==\n" +
+ "-----END CERTIFICATE-----";
+
+ private static final String CSR = "-----BEGIN CERTIFICATE REQUEST-----\n" +
+ "MIIEZjCCAk4CAQAwITELMAkGA1UEBhMCQ0ExEjAQBgNVBAMMCVRlc3QgTGVhZjCC\n" +
+ "AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYDEoMbxZEuerV5G9IXUXfe\n" +
+ "UB3u3Yf4b9QI7ewea3Vw04eS/XY4J/KC58OAKc+/3B0Vjghza1+bMalkdFHuIYls\n" +
+ "/57wbmKIoRSZouma31gHJATWPdDpzcAeZVGRfqfniw3dDfVpIea5gi63gmTFGD7l\n" +
+ "rmdn5BhQBijWXQY5gD52vGmnalqPBBL+HXgynYiTxmoGI/UNW16V1k8OTnT2F3kt\n" +
+ "OES+5/mu2r4c7fExmkh64wXYqL2EUvh7xvd4KKIh05Bsl5J2G0Lkl1gh89FJHOVW\n" +
+ "5+jrMku1wU4KBSZWNvxgcSgfKOI3IAx6iqxflhb7FKK3VTYZ7zJ/cAhaJvv1gJ7N\n" +
+ "S5AlxsFxMRMgLoFtad9Qk5wH+wX+9Ozf7jNoWZQnLgzfr7CdvjBPmYR/THg0OWFS\n" +
+ "0bkr20G8lMvtGMbmjN6Ot70KzYCIDjaCV5sX60i76p7rSheibgCslO49cRU7G266\n" +
+ "HB1GXZNQbT3xBzoaVN9B5uQnL8tUnTn0PsQ4KN2MKIfQt+IO0yesBI7yjXRHSOJX\n" +
+ "WmbZnrojfYbyAWPBKXnQ4vFcqBdXIXGuI4f67Y8BBuJjV9FOUxcCu++ypP2RtS8h\n" +
+ "sly2wgtRwPCN7BbLOY9A7qm821DJ3MneHKloGodNvcBvq9VLcwFA9QFX5tgnETV8\n" +
+ "4oc0VHxaiB7zuNchjINFAgMBAAGgADANBgkqhkiG9w0BAQsFAAOCAgEAiRKUSg4m\n" +
+ "i+qozc3Nx80LfCO9b4+oWp7/bcv0fUADfet7nsobwY8Y6INYMEs6aBNnj2ofFmEd\n" +
+ "Kup3VHh3vce7Grwkn0MWXKRdCbsLVJ5joWixxxbCDgiRZLYDVlhnU7ZFm3mxmC4l\n" +
+ "KfKMiHfW833gnemQYRAyamDErKgPV8O9spm0TLj2nLllcA5ugR98kh9TnvnQqRdq\n" +
+ "dO0ic8C2OECRPV9OVmP13qXiVJRApWYBrw+WJT+sz3LRGfQMIzaTPYWen0dd8+iG\n" +
+ "HhJot7DNbdLMf6jtWXazrsmUhjjgr5KHMZdOWcbqBCRZkVTkf1HfoRbBTt/wEkBX\n" +
+ "fJrXVpGbA7H7xXDXKFVUM19q7JJr9M5CfAvCtUGg/UnqfhDnqsFHgQqro21YwNQP\n" +
+ "/bahU44eNoz8RUiyDEKUW9ginyd0zc3aSAkd98r5u1+tOTmU0KeIr3yc0P+tKxgB\n" +
+ "bAQaKrXMlLwSHHHutEkJH2KtwKx8w66VtpYtkggfTic1ae6EoVV5LpLHIlZmRMdg\n" +
+ "CDUatEdRweCdtO0TTR7ik8wMzs6GxAVDfTMaQ41Ks8OBnmLDZQTdRfssm2u6jYut\n" +
+ "DQxdF5LWe8RVlkEHB2KZJg2fWZ8bjEWr3DkCvnxRlK4Tabo5/mlymjVxTRxxRoGR\n" +
+ "TXU09TZASjVPzxKIyZbhgNqQvkZl2/hSCE8=\n" +
+ "-----END CERTIFICATE REQUEST-----";
+
+ private CACertificate ca;
+
+ @BeforeEach
+ void setup() {
+ ca = new CACertificate();
+ }
+
+ @Test
+ void testConstructor() {
+ assertNull(ca.getPublicKey());
+ assertNull(ca.getCertificate());
+ assertEquals(1, ca.getSerial());
+ assertEquals(0, ca.getSigned().size());
+ assertEquals(0, ca.getRevoked().size());
+ }
+
+ @Test
+ void testGenerateKey() throws Throwable {
+ ca.generateKey();
+ assertNotNull(ca.getPublicKey());
+ }
+
+ @Test
+ void testInstallCertificate() throws Throwable {
+ ca.generateKey();
+ ca.installCertificate(new Certificate(new BytesReader(TestConstants.CERT_L2_RSA), false));
+ assertNotNull(ca.getCertificate());
+ }
+
+ @Test
+ void testSignCSR() throws Throwable {
+ ca.generateKey();
+ CertificationRequest req = ca.signCSR();
+ assertArrayEquals(ObjectIdentifier.OID_SHA256_WITH_RSA_ENCRYPTION,
+ req.getSignatureAlgorithm().getType().getInts());
+ assertEquals("CN=JCA", req.getCertificationRequestInfo().getSubject().toString());
+ assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION, req.getCertificationRequestInfo().getSubjectPKInfo()
+ .getAlgorithm().getType().getInts());
+ }
+
+ @Test
+ void testGetCAPublicKeyInfo() throws Throwable {
+ ca.generateKey();
+ ca.installCertificate(new Certificate(new BytesReader(Utils.parsePEM(
+ Utils.byteToByte(CA.getBytes(StandardCharsets.UTF_8)), "CERTIFICATE")), false));
+ assertArrayEquals(ObjectIdentifier.OID_RSA_ENCRYPTION,
+ ca.getCAPublicKeyInfo().getAlgorithm().getType().getInts());
+ }
+
+ @Test
+ void testSignCert() throws Throwable {
+ ca.generateKey();
+ ca.installCertificate(new Certificate(new BytesReader(Utils.parsePEM(
+ Utils.byteToByte(CA.getBytes(StandardCharsets.UTF_8)), "CERTIFICATE")), false));
+ CertificationRequestInfo req = new CertificationRequest(new BytesReader(Utils.parsePEM(
+ Utils.byteToByte(CSR.getBytes(StandardCharsets.UTF_8)), "CERTIFICATE REQUEST")),
+ false).getCertificationRequestInfo();
+ Template template = new Template("123", true, null, 60);
+ Certificate cert = ca.signCert(req, template);
+ assertEquals(req.getSubject().toString(), cert.getCertificate().getSubject().toString());
+ assertEquals(60,
+ cert.getCertificate().getValidity().getNotAfter().getTimestamp().getDayOfYear()
+ - cert.getCertificate().getValidity().getNotBefore().getTimestamp().getDayOfYear());
+ assertEquals(1, ca.getSigned().size());
+
+ template = new Template("123", true, null, 60);
+ template.setSubject("Test Test");
+ cert = ca.signCert(req, template);
+ assertEquals(60,
+ cert.getCertificate().getValidity().getNotAfter().getTimestamp().getDayOfYear()
+ - cert.getCertificate().getValidity().getNotBefore().getTimestamp().getDayOfYear());
+ assertEquals(template.getSubject().toString(), cert.getCertificate().getSubject().toString());
+ assertEquals(2, ca.getSigned().size());
+ }
+
+ @Test
+ void testRevoke() throws Throwable {
+ ca.generateKey();
+ ca.installCertificate(new Certificate(new BytesReader(Utils.parsePEM(
+ Utils.byteToByte(CA.getBytes(StandardCharsets.UTF_8)), "CERTIFICATE")), false));
+ CertificationRequestInfo req = new CertificationRequest(new BytesReader(Utils.parsePEM(
+ Utils.byteToByte(CSR.getBytes(StandardCharsets.UTF_8)), "CERTIFICATE REQUEST")),
+ false).getCertificationRequestInfo();
+ Template template = new Template("123", true, null, 60);
+ Certificate cert = ca.signCert(req, template);
+ ca.revoke(new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null,
+ cert.getCertificate().getSerialNumber(),
+ new UtcTime(UtcTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))),
+ Reason.KEY_COMPROMISE));
+ assertEquals(1, ca.getRevoked().size());
+ }
+
+ @Test
+ void testSignCRL() throws Throwable {
+ ca.generateKey();
+ ca.installCertificate(new Certificate(new BytesReader(Utils.parsePEM(
+ Utils.byteToByte(CA.getBytes(StandardCharsets.UTF_8)), "CERTIFICATE")), false));
+ CertificationRequestInfo req = new CertificationRequest(new BytesReader(Utils.parsePEM(
+ Utils.byteToByte(CSR.getBytes(StandardCharsets.UTF_8)), "CERTIFICATE REQUEST")),
+ false).getCertificationRequestInfo();
+ Template template = new Template("123", true, null, 60);
+ Certificate cert = ca.signCert(req, template);
+ ca.revoke(new RevokedCertificate(ASN1Object.TAG_SEQUENCE, null,
+ cert.getCertificate().getSerialNumber(),
+ new UtcTime(UtcTime.TAG, null, ZonedDateTime.now(ZoneId.of("UTC"))),
+ Reason.KEY_COMPROMISE));
+ assertEquals(1, ca.signCRL().getCrl().getRevokedCertificates().length);
+ }
+}
diff --git a/src/test/model/ca/TemplateTest.java b/src/test/model/ca/TemplateTest.java
new file mode 100644
index 0000000..0ca7434
--- /dev/null
+++ b/src/test/model/ca/TemplateTest.java
@@ -0,0 +1,27 @@
+package model.ca;
+
+import model.asn1.exceptions.ParseException;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TemplateTest {
+ @Test
+ void testConstructor() {
+ Template template = new Template("123", true, null, 123);
+ assertEquals("123", template.getName());
+ assertTrue(template.isEnabled());
+ assertNull(template.getSubject());
+ assertEquals(123, template.getValidity());
+ }
+
+ @Test
+ void testSetSubject() throws ParseException {
+ Template template = new Template("123", true, null, 123);
+ template.setSubject("123");
+ assertEquals("CN=123,C=CA", template.getSubject().toString());
+ template.setSubject((String) null);
+ assertNull(template.getSubject());
+ assertThrows(ParseException.class, () -> template.setSubject("*"));
+ }
+}
diff --git a/src/test/model/pki/cert/TbsCertificateTest.java b/src/test/model/pki/cert/TbsCertificateTest.java
index ae92ace..be26eb9 100644
--- a/src/test/model/pki/cert/TbsCertificateTest.java
+++ b/src/test/model/pki/cert/TbsCertificateTest.java
@@ -35,6 +35,18 @@ public class TbsCertificateTest {
}
@Test
+ void testGetExtension() throws ParseException {
+ TbsCertificate parsed = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_L1_ECC)),
+ false);
+ assertNull(parsed.getExtension(ObjectIdentifier.OID_EXTENSION_REQUEST));
+ assertNotNull(parsed.getExtension(ObjectIdentifier.OID_BASIC_CONSTRAINTS));
+ parsed = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_V1)),
+ false);
+ assertNull(parsed.getExtension(ObjectIdentifier.OID_EXTENSION_REQUEST));
+ assertNull(parsed.getExtension(ObjectIdentifier.OID_BASIC_CONSTRAINTS));
+ }
+
+ @Test
void testParse() throws ParseException {
TbsCertificate parsed = new TbsCertificate(new BytesReader(trimToTbs(TestConstants.CERT_L1_ECC)),
false);
diff --git a/src/test/ui/UtilsTest.java b/src/test/ui/UtilsTest.java
new file mode 100644
index 0000000..a7b4a52
--- /dev/null
+++ b/src/test/ui/UtilsTest.java
@@ -0,0 +1,67 @@
+package ui;
+
+import model.asn1.exceptions.ParseException;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.stream.IntStream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class UtilsTest {
+ @Test
+ void testByteToByte() {
+ final byte[] primitive = new byte[]{ 1, 1, 4, 5, 1, 4 };
+ final Byte[] boxed = new Byte[]{ 1, 1, 4, 5, 1, 4 };
+ assertArrayEquals(primitive, Utils.byteToByte(boxed));
+ assertArrayEquals(boxed, Utils.byteToByte(primitive));
+ }
+
+ @Test
+ void testBytesToInt() throws ParseException {
+ assertEquals(1, Utils.bytesToInt(new Byte[]{ 1 }));
+ assertEquals(257, Utils.bytesToInt(new Byte[]{ 1, 1 }));
+ assertThrows(ParseException.class, () -> Utils.bytesToInt(new Byte[]{ 1, -1, -1, -1, -1 }));
+ }
+
+ @Test
+ void testValToByte() {
+ assertArrayEquals(new Byte[]{ 0 },
+ Utils.valToByte(0));
+ assertArrayEquals(new Byte[]{ 1 },
+ Utils.valToByte(1));
+ assertArrayEquals(new Byte[]{ 1, 1 },
+ Utils.valToByte(257));
+ assertArrayEquals(new Byte[]{ -1 },
+ Utils.valToByte(-1));
+ }
+
+ @Test
+ void testParsePEM() throws ParseException {
+ assertArrayEquals(IntStream.range(0, 48).mapToObj(i -> (byte) 1).toArray(Byte[]::new),
+ Utils.parsePEM(Utils.byteToByte(("-----BEGIN BLABLA-----\n" +
+ "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\n" +
+ "-----END BLABLA-----").getBytes(StandardCharsets.UTF_8)),
+ "BLABLA"));
+ assertThrows(ParseException.class, () -> {
+ Utils.parsePEM(Utils.byteToByte(("-----BEGIN BLABLA-----\n" +
+ "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB\n" +
+ "-----END BLABLA-----").getBytes(StandardCharsets.UTF_8)),
+ "LALA");
+ });
+ assertThrows(ParseException.class, () -> {
+ Utils.parsePEM(Utils.byteToByte(("-----BEGIN BLABLA-----" +
+ "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB" +
+ "-----END BLABLA-----").getBytes(StandardCharsets.UTF_8)),
+ "BLABLA");
+ });
+ }
+
+ @Test
+ void testToPEM() {
+ assertEquals("-----BEGIN ABC-----\n" +
+ "AQ==\n" +
+ "-----END ABC-----",
+ Utils.toPEM(new Byte[]{ 0x1 }, "ABC"));
+ }
+}
diff --git a/tests/.gitignore b/tests/.gitignore
new file mode 100644
index 0000000..237b712
--- /dev/null
+++ b/tests/.gitignore
@@ -0,0 +1 @@
+ca.key
diff --git a/tests/Makefile b/tests/Makefile
new file mode 100644
index 0000000..6a294b6
--- /dev/null
+++ b/tests/Makefile
@@ -0,0 +1,40 @@
+.POSIX:
+
+leaf.csr: leaf.key leaf.csr.cnf
+ openssl req -new -key leaf.key -out leaf.csr -config leaf.csr.cnf
+
+leaf.key:
+ openssl genrsa -out leaf.key 4096
+
+sub.crt: sub.csr
+ mkdir -p newcerts
+ touch index.txt
+ openssl ca \
+ -verbose \
+ -config ca.cnf \
+ -extensions extensions_sub \
+ -notext \
+ -rand_serial \
+ -in sub.csr \
+ -out sub.crt
+
+ca.crt:
+ openssl req \
+ -verbose \
+ -config ca.cnf \
+ -new \
+ -x509 \
+ -key ca.key \
+ -days 9132 \
+ -out ca.crt
+
+ca.key:
+ openssl ecparam -name P-256 -genkey -out ca.key
+
+crlnumber:
+ echo 0000 > crlnumber
+
+reset:
+ echo "!!! THIS WILL RESET EVERYTHING, INCLUDING PRIVATE KEYS !!!"
+ # sleep 5
+ rm -rf newcerts serial index.txt* private certs sub.csr crlnumber* ca.crl ca.crt sub.crt ca.key
diff --git a/tests/ca.cnf b/tests/ca.cnf
new file mode 100644
index 0000000..ef5a9c9
--- /dev/null
+++ b/tests/ca.cnf
@@ -0,0 +1,108 @@
+[ ca ]
+default_ca = CA
+
+[ CA ]
+# Database
+dir = .
+certs = $dir/certs/
+crl_dir = $dir/crl
+new_certs_dir = $dir/newcerts
+database = $dir/index.txt
+# Although we use $ openssl ca -rand_serial, this seems necessary.
+serial = $dir/serial
+RANDFILE = $dir/.rand
+
+private_key = $dir/ca.key
+certificate = $dir/ca.crt
+
+# CRL
+crlnumber = $dir/crlnumber
+crl = $dir/ca.crl
+crl_extensions = crl_ext
+# Root CA CRL: 1 year
+default_crl_days = 365
+
+# Cryptography
+default_md = sha512
+
+# Policy
+name_opt = ca_default
+cert_opt = ca_default
+# Intermediate CA: 10 years
+default_days = 3650
+preserve = no
+policy = policy_ca
+
+[ policy_ca ]
+countryName = optional
+stateOrProvinceName = optional
+organizationName = optional
+organizationalUnitName = optional
+commonName = supplied
+emailAddress = optional
+
+[ req ]
+default_bits = 4096
+distinguished_name = req_dn
+string_mask = utf8only
+
+# s/sha512/sha256/, according to Jimmy (isrg uses sha256)
+default_md = sha256
+
+x509_extensions = extensions
+
+[ req_dn ]
+commonName = Common Name
+countryName = Country Name (2 letter code)
+# For simplicity
+#stateOrProvinceName = State or Province Name
+#localityName = Locality Name
+#0.organizationName = Organization Name
+# CAB Baseline (BR) v2.0.0
+# OU name must not present
+# Email address is not recommended (as per Jimmy)
+#organizationalUnitName = Organizational Unit Name
+#emailAddress = Email Address
+
+commonName_default = Test Root CA
+countryName_default = CA
+#stateOrProvinceName_default = British Columbia
+#localityName_default = Vancouver
+#0.organizationName_default = Yuuta Home
+#organizationalUnitName_default = IT
+#emailAddress_default = yuuta@yuuta.moe
+
+[ extensions ]
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+# Seems like it is completely unnecessary to put CRL and AIA in RootCA
+# because they point to the issuer's info.
+# crlDistributionPoints = crldp
+# Because I don't have a real OID
+#certificatePolicies = @polset
+# Seems like it is unnecessary.
+#authorityInfoAccess = caIssuers;URI:http://home.yuuta.moe/pki/rootca.crt
+
+[ extensions_sub ]
+subjectKeyIdentifier = hash
+authorityKeyIdentifier = keyid:always,issuer
+basicConstraints = critical, CA:true, pathlen: 0
+keyUsage = critical, digitalSignature, cRLSign, keyCertSign
+crlDistributionPoints = crldp
+authorityInfoAccess = caIssuers;URI:http://home.yuuta.moe/pki/rootca.crt
+
+#[ polset ]
+#policyIdentifier = 1.3.6.1.4.1.191981.5.1.1
+#CPS.1 = "http://home.yuuta.moe/pki/policy"
+#userNotice.1 = @polset_notice
+#
+#[ polset_notice ]
+#explicitText = "This certificate authority is for internal use only."
+
+[ crldp ]
+fullname = URI:http://home.yuuta.moe/pki/rootca.crl
+
+[ crl_ext ]
+authorityKeyIdentifier = keyid:always
diff --git a/tests/leaf.csr.cnf b/tests/leaf.csr.cnf
new file mode 100644
index 0000000..d2b88cd
--- /dev/null
+++ b/tests/leaf.csr.cnf
@@ -0,0 +1,9 @@
+[req]
+distinguished_name = req_distinguished_name
+
+# https://github.com/openssl/openssl/issues/3536#issuecomment-306520579
+prompt = no
+
+[req_distinguished_name]
+countryName = CA
+commonName = Test Leaf