From 65ea6c17a0c1348aa9ef4e158102ddf173936882 Mon Sep 17 00:00:00 2001 From: Yuuta Liang Date: Thu, 23 Nov 2023 08:09:01 +0800 Subject: Add GUI Signed-off-by: Yuuta Liang --- README.md | 26 + src/main/model/GroupObserver.java | 48 ++ src/main/model/ObservedData.java | 47 ++ src/main/model/Observer.java | 33 + src/main/model/asn1/ObjectIdentifier.java | 16 + src/main/model/ca/CertificationAuthority.java | 94 ++- src/main/model/ca/Template.java | 2 +- src/main/model/x501/AttributeTypeAndValue.java | 9 +- src/main/model/x501/Name.java | 107 +++- src/main/ui/IssueDialog.java | 110 ++++ src/main/ui/Main.java | 4 +- src/main/ui/MainUI.form | 111 ---- src/main/ui/MainUI.java | 690 ++++++++++++++++++++- src/main/ui/RevokeDialog.java | 186 ++++++ src/main/ui/TemplateEditDialog.java | 82 +++ src/main/ui/widgets/CertEditDialog.java | 108 ++++ src/main/ui/widgets/CertTableModel.java | 129 ++++ src/main/ui/widgets/GCBuilder.java | 213 +++++++ src/main/ui/widgets/LogTableModel.java | 79 +++ src/main/ui/widgets/TemplateTableModel.java | 105 ++++ src/main/ui/widgets/UIUtils.java | 168 +++++ src/resources/deletetest.png | Bin 0 -> 1464 bytes src/resources/disable.png | Bin 0 -> 831 bytes src/resources/enable.png | Bin 0 -> 639 bytes src/resources/export.png | Bin 0 -> 691 bytes src/resources/new.png | Bin 0 -> 497 bytes src/resources/open.png | Bin 0 -> 508 bytes src/resources/publisher.png | Bin 0 -> 969 bytes src/resources/saveall.png | Bin 0 -> 368 bytes src/resources/verified.png | Bin 0 -> 1139 bytes src/test/model/GroupObserverTest.java | 65 ++ src/test/model/MockObserver.java | 41 ++ src/test/model/ObservedDataTest.java | 44 ++ src/test/model/asn1/ObjectIdentifierTest.java | 17 + src/test/model/ca/CertificationAuthorityTest.java | 79 ++- src/test/model/ca/TemplateTest.java | 3 + src/test/model/x501/AttributeTypeAndValueTest.java | 4 + src/test/model/x501/NameTest.java | 34 + src/test/persistence/DecoderTest.java | 3 + tests/Makefile | 3 +- 40 files changed, 2494 insertions(+), 166 deletions(-) create mode 100644 src/main/model/GroupObserver.java create mode 100644 src/main/model/ObservedData.java create mode 100644 src/main/model/Observer.java create mode 100644 src/main/ui/IssueDialog.java delete mode 100644 src/main/ui/MainUI.form create mode 100644 src/main/ui/RevokeDialog.java create mode 100644 src/main/ui/TemplateEditDialog.java create mode 100644 src/main/ui/widgets/CertEditDialog.java create mode 100644 src/main/ui/widgets/CertTableModel.java create mode 100644 src/main/ui/widgets/GCBuilder.java create mode 100644 src/main/ui/widgets/LogTableModel.java create mode 100644 src/main/ui/widgets/TemplateTableModel.java create mode 100644 src/main/ui/widgets/UIUtils.java create mode 100644 src/resources/deletetest.png create mode 100644 src/resources/disable.png create mode 100644 src/resources/enable.png create mode 100644 src/resources/export.png create mode 100644 src/resources/new.png create mode 100644 src/resources/open.png create mode 100644 src/resources/publisher.png create mode 100644 src/resources/saveall.png create mode 100644 src/resources/verified.png create mode 100644 src/test/model/GroupObserverTest.java create mode 100644 src/test/model/MockObserver.java create mode 100644 src/test/model/ObservedDataTest.java diff --git a/README.md b/README.md index 9b4f359..1040e4b 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,32 @@ As a user, I want to be able to: For example, the user can add arbitrary number of `Cert`'s into a `CA` container, upon each successful sign. +## Instructions + +1. Load / store to `./data/ca.json` by clicking on the Load / Store button on the top left. Signing certificates, + revoking certificates, publishing CRLs will automatically save the database. The unsaved changes are visible at the + bottom status line. +2. The main operations are at the upper panel, from the toolbar until the audit logs pane. The lower panel shows audit + logs. +3. Start by generating a RSA2048 private key, by clicking on the "Generate" button. Then, sign a CSR to be sent to the + upper level CA. Finally, get the signed certificate back and install it by clicking on the "Install" button. +4. Define a template. Navigate to the "Templates" tab and click on "New". Set template name, subject (optional), and + validity period (in days). These settings will be applied when signing certificates. You can add arbitrary number of + templates to the database, don't forget to save. +5. Before a template can be used, it must be enabled. Select a template and click on the "Enable" button on the top. + Optionally, disable or delete the templates no longer needed. +6. Sign certificates. Navigate to the "Certs" tab and click on "Sign" button in the toolbar. Select the CSR. In the popup + dialog, you can select the template and apply certificate properties. When you are ready, click on the "Issue" button. + The CA will sign this certificate and add it to the database. The database will be automatically saved. You can view + your certificate from the list and export it. +7. If a certificate has to be revoked ahead of validity period, select it from the list and click on the "Revoke" button + in the toolbar. From the dialog, confirm certificate subject and serial number, and select an appropriate reason. You + may also customize the revocation time, but it must be typed in ISO-8601 format with offset. After revocation, the list + automatically updates, and you can see the certificate you just revoked now has a red cross icon. Note that the database + automatically saves after revoking. +8. Periodically publish CRLs. On the "CA" tab, click on the "CRL" button on the top right, and select a path for the CRL. + The database automatically saves after issuing a new CRL. + ## Author Yuuta Liang diff --git a/src/main/model/GroupObserver.java b/src/main/model/GroupObserver.java new file mode 100644 index 0000000..41a9aac --- /dev/null +++ b/src/main/model/GroupObserver.java @@ -0,0 +1,48 @@ +package model; + +import java.util.HashMap; +import java.util.Map; + +/** + * A group of observers with different types registered. + */ +public class GroupObserver implements Observer { + /** + * The map. Because Java doesn't have dependent maps, they are left here as unchecked. + */ + private final Map map = new HashMap<>(); + + /** + * EFFECTS: Register a data type with an observer. Override existing registrations with the same type. + * REQUIRES: cls != null, obs != null. + */ + public void register(Class cls, Observer obs) { + map.put(cls, obs); + } + + /** + * EFFECTS: Notify the registered observer based on the type of 'o' with given arguments. + * Does nothing if no registration found. Subclasses are supported as long as a parent class observer is registered. + * Specific type match takes priority. Non-specific type match is not guaranteed to be the closest. + * REQUIRES: o != null, direction be DIRECTION_*, index >= 0 or == INDEX_NOT_IN_LIST. + */ + @Override + public void accept(Object o, int direction, int i) { + Observer obs = map.get(o.getClass()); + if (obs == null) { + Class supertype = map.keySet().stream().filter(clz -> clz.isInstance(o)).findFirst().orElse(null); + if (supertype == null) { + return; + } + obs = map.get(supertype); + } + obs.accept(o, direction, i); + } + + /** + * EFFECTS: Count the observers. + */ + public int getRegisteredObserverCount() { + return map.size(); + } +} diff --git a/src/main/model/ObservedData.java b/src/main/model/ObservedData.java new file mode 100644 index 0000000..5ad91a3 --- /dev/null +++ b/src/main/model/ObservedData.java @@ -0,0 +1,47 @@ +package model; + +/** + * A single observed data that notifies the observer once changed. + */ +public class ObservedData { + /** + * The data. + */ + private T data; + + /** + * The observer. + */ + private Observer acceptor; + + /** + * EFFECTS: Init with the given initial value and observer. + * REQUIRES: acceptor != null + */ + public ObservedData(T initialValue, Observer acceptor) { + this.data = initialValue; + this.acceptor = acceptor; + } + + /** + * EFFECTS: Set the data and notify the observer with the new data and DIRECTION_CHANGE + INDEX_NOT_IN_LIST. + */ + public void set(T data) { + this.data = data; + acceptor.accept(data, Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST); + } + + /** + * EFFECTS: Get the data. + */ + public T get() { + return data; + } + + /** + * EFFECTS: Get the observer. + */ + public Observer getAcceptor() { + return acceptor; + } +} diff --git a/src/main/model/Observer.java b/src/main/model/Observer.java new file mode 100644 index 0000000..df9cbc2 --- /dev/null +++ b/src/main/model/Observer.java @@ -0,0 +1,33 @@ +package model; + +/** + * Data observers for GUI data binding. + */ +@FunctionalInterface +public interface Observer { + /** + * The data is just added to a list. Index will be its index. + */ + int DIRECTION_ADD = 0; + + /** + * The data is just removed from a list. Index will be its previous index. + */ + int DIRECTION_REMOVE = 1; + + /** + * The data is modified, either or not in a list. Index will be either INDEX_NOT_IN_LIST or its index. + */ + int DIRECTION_CHANGE = 2; + + /** + * A special index representing that the data is not in a list. + */ + int INDEX_NOT_IN_LIST = -1; + + /** + * EFFECTS: Handle data change. + * REQUIRES: data != null, direction be DIRECTION_*, index >= 0 or == INDEX_NOT_IN_LIST. + */ + void accept(T data, int direction, int index); +} diff --git a/src/main/model/asn1/ObjectIdentifier.java b/src/main/model/asn1/ObjectIdentifier.java index f6e850a..64ade8e 100644 --- a/src/main/model/asn1/ObjectIdentifier.java +++ b/src/main/model/asn1/ObjectIdentifier.java @@ -104,6 +104,22 @@ public class ObjectIdentifier extends ASN1Object { this.ints = nums.toArray(new Integer[0]); } + /** + * EFFECTS: Get OID from a known part, case insensitive. Currently known: C CN OU O L DC. + * Throws {@link ParseException} if the name is unsupported. + */ + public static Integer[] getKnown(String name) throws ParseException { + switch (name.toUpperCase()) { + case "C": return OID_C; + case "CN": return OID_CN; + case "OU": return OID_OU; + case "O": return OID_O; + case "L": return OID_L; + case "DC": return OID_DC; + default: throw new ParseException("Unsupported DN part: " + name); + } + } + /** * EFFECTS: Parse input OID bytes. * REQUIRES: raw.length >= 1 diff --git a/src/main/model/ca/CertificationAuthority.java b/src/main/model/ca/CertificationAuthority.java index 038d209..5181f1a 100644 --- a/src/main/model/ca/CertificationAuthority.java +++ b/src/main/model/ca/CertificationAuthority.java @@ -1,5 +1,6 @@ package model.ca; +import model.Observer; import model.asn1.*; import model.asn1.exceptions.InvalidCAException; import model.asn1.exceptions.ParseException; @@ -28,11 +29,12 @@ import java.security.spec.RSAPrivateKeySpec; import java.security.spec.RSAPublicKeySpec; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Stream; /** - * Holds a CA private key, its certificate, signed / revoked list, template list, and logs list. + * Holds a CA private key, its certificate, signed / revoked list, template list, and logs list. Data can be observed. */ public class CertificationAuthority { public static final int SERIAL_DEFAULT = 1; @@ -83,7 +85,12 @@ public class CertificationAuthority { private final String user; /** - * EFFECT: Init with the given parameters and user "yuuta". + * Data observers. + */ + private final List observers; + + /** + * EFFECT: Init with the given parameters, user "yuuta", and no observers. * Throws {@link NoSuchAlgorithmException} if the key is specified but RSA is not supported. * Throws {@link InvalidKeySpecException} if the key specified is invalid. * Throws {@link InvalidCAException} or {@link ParseException} if the CA specified is invalid. @@ -111,11 +118,12 @@ public class CertificationAuthority { this.templates = new ArrayList<>(templates); this.logs = new ArrayList<>(logs); this.user = "yuuta"; + this.observers = new ArrayList<>(); } /** * EFFECT: Init with a null key and null certificate, empty signed, revoked template, and log list, - * serial at SERIAL_DEFAULT, and user "yuuta". + * serial at SERIAL_DEFAULT, user "yuuta", and no observers. */ public CertificationAuthority() { this.key = null; @@ -127,10 +135,13 @@ public class CertificationAuthority { this.templates = new ArrayList<>(); this.logs = new ArrayList<>(); this.user = "yuuta"; + this.observers = new ArrayList<>(); } /** * EFFECTS: Generate a new RSA2048 private key. This action will be logged. + * Observers will be notified for (RSAPublicKey.class, DIRECTION_CHANGE, INDEX_NOT_IN_LIST). + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * REQUIRES: getPublicKey() is null (i.e., no private key had been installed) * MODIFIES: this */ @@ -140,6 +151,7 @@ public class CertificationAuthority { final KeyPair pair = gen.generateKeyPair(); this.key = (RSAPrivateKey) pair.getPrivate(); this.publicKey = (RSAPublicKey) pair.getPublic(); + notif(getPublicKey(), Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST); log("Generated CA private key."); } @@ -159,6 +171,8 @@ public class CertificationAuthority { /** * EFFECTS: Load the RSA private and public exponents. This action will be logged. + * Observers will be notified for (RSAPublicKey.class, DIRECTION_CHANGE, INDEX_NOT_IN_LIST). + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * Throws {@link NoSuchAlgorithmException} if RSA is not available on the platform. * Throws {@link InvalidKeySpecException} if the input is invalid. * REQUIRES: getPublicKey() is null (i.e., no private key had been installed) @@ -167,6 +181,7 @@ public class CertificationAuthority { public void loadKey(BigInteger n, BigInteger p, BigInteger e) throws NoSuchAlgorithmException, InvalidKeySpecException { setKey(n, p, e); + notif(getPublicKey(), Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST); log("Installed CA private key."); } @@ -252,6 +267,8 @@ public class CertificationAuthority { * - The new certificate must have the same algorithm and public key as getPublicKey() * - It must have basicConstraints { cA = TRUE } * - It must contain key usage Digital Signature, Certificate Sign, CRL Sign + * Observers will be notified for (Certificate.class, DIRECTION_CHANGE, INDEX_NOT_IN_LIST). + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * Throws {@link ParseException} if the cert has invalid extension values. * This action will be logged. * REQUIRES: @@ -261,6 +278,7 @@ public class CertificationAuthority { public void installCertificate(Certificate certificate) throws InvalidCAException, ParseException { validateCertificate(certificate); this.certificate = certificate; + notif(certificate, Observer.DIRECTION_CHANGE, Observer.INDEX_NOT_IN_LIST); log("CA certificate is installed."); } @@ -325,6 +343,7 @@ public class CertificationAuthority { /** * EFFECT: Generate CSR and sign it, so the CA can request itself a certificate. + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * REQUIRES: The CA cert must not be installed. * MODIFIES: this (This action will be logged) */ @@ -351,6 +370,8 @@ public class CertificationAuthority { /** * EFFECTS: Sign the CSR based on the template. + * Observers will be notified for (Certificate.class, DIRECTION_ADD, i). + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * REQUIRES: The CA cert must be installed first, req must have a subject, template must be enabled. * MODIFIES: this */ @@ -363,7 +384,8 @@ public class CertificationAuthority { new BitString(BitString.TAG, null, 0, signBytes(newCert.encodeValueDER()))); this.signed.add(cert); - log("Signed a cert with serial number " + cert.getCertificate().getSerialNumber()); + notif(cert, Observer.DIRECTION_ADD, this.signed.size() - 1); + log("Signed a cert with serial number " + cert.getCertificate().getSerialNumber().getLong()); return cert; } @@ -406,17 +428,22 @@ public class CertificationAuthority { /** * EFFECTS: Add the revocation info to revoked list. This action will be logged. + * Observers will be notified for (RevokedCertificate.class, DIRECTION_ADD, i). + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * 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); - log("Certificate " + rev.getSerialNumber().getLong() + " is revoked with reason " + rev.getReason()); + notif(rev, Observer.DIRECTION_ADD, revoked.size() - 1); + log("Certificate " + rev.getSerialNumber().getLong() + " is revoked with reason " + rev.getReason() + + " at " + rev.getRevocationDate().getTimestamp().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)); } /** * 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. + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * REQUIRES: The CA cert must be installed first. * MODIFIES: this (This action will be logged) */ @@ -439,10 +466,29 @@ public class CertificationAuthority { /** * EFFECTS: Log the action with the current date and user. + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * MODIFIES: this */ private void log(String message) { - this.logs.add(new AuditLogEntry(user, ZonedDateTime.now(), message)); + final AuditLogEntry i = new AuditLogEntry(user, ZonedDateTime.now(), message); + this.logs.add(i); + notif(i, Observer.DIRECTION_ADD, logs.size() - 1); + } + + /** + * EFFECTS: Register the given observer, so it will be called upon changes. + * MODIFIES: this + */ + public void registerObserver(final Observer observer) { + this.observers.add(observer); + } + + /** + * EFFECTS: Notify the observers. + * REQUIRES: direction must be valid Observer constants, i must be either >= 0 or Observer.INDEX_NOT_IN_LIST. + */ + private void notif(Object o, int direction, int i) { + observers.forEach(e -> e.accept(o, direction, i)); } /** @@ -460,33 +506,43 @@ public class CertificationAuthority { /** * EFFECTS: Install the new template. This action will be logged. + * Observers will be notified for (Template.class, DIRECTION_ADD, i). + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * REQUIRES: findTemplate(template.getName(), false) == null * MODIFIES: this */ public void addTemplate(Template template) { this.templates.add(template); + notif(template, Observer.DIRECTION_ADD, templates.size() - 1); log("Added a new template: " + template.getName()); } /** - * EFFECTS: Set the given template to enabled / disabled. This action will be logged. + * EFFECTS: Set the given template to enabled / disabled, order will be kept. This action will be logged. + * Observers will be notified for (Template.class, DIRECTION_CHANGE, i). + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * REQUIRES: the template is valid (findTemplate does not return null) * MODIFIES: this */ public void setTemplateEnable(Template template, boolean enable) { final Template t = findTemplate(template.getName(), false); - templates.remove(t); - templates.add(new Template(t.getName(), enable, t.getSubject(), t.getValidity())); + int i = templates.indexOf(t); + templates.set(i, new Template(t.getName(), enable, t.getSubject(), t.getValidity())); + notif(template, Observer.DIRECTION_CHANGE, i); log("Template " + template.getName() + " has been " + (enable ? "enabled" : "disabled")); } /** * EFFECTS: Remove the given template. This action will be logged. + * Observers will be notified for (Template.class, DIRECTION_REMOVE, i). + * Observers will be notified for (AuditLogEntry.class, DIRECTION_ADD, i). * REQUIRES: the template is valid (findTemplate does not return null) * MODIFIES: this */ public void removeTemplate(Template template) { + int i = templates.indexOf(template); templates.remove(findTemplate(template.getName(), false)); + notif(template, Observer.DIRECTION_REMOVE, i); log("Template " + template.getName() + " is removed"); } @@ -496,40 +552,28 @@ public class CertificationAuthority { return certificate; } - /** - * EFFECT: Get a read-only view of the signed certificates. - */ public List getSigned() { - return List.copyOf(signed); + return signed; } - /** - * EFFECT: Get a read-only view of the revoked certificates. - */ public List getRevoked() { - return List.copyOf(revoked); + return revoked; } public int getSerial() { return serial; } - /** - * EFFECT: Get a read-only view of the templates. - */ public List