aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-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
24 files changed, 1814 insertions, 31 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();
+ }
}
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"));
+ }
+}