aboutsummaryrefslogtreecommitdiff
path: root/src/main
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 /src/main
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>
Diffstat (limited to 'src/main')
-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
17 files changed, 1499 insertions, 29 deletions
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();
+ }
}