diff options
author | Yuuta Liang <yuutaw@students.cs.ubc.ca> | 2023-10-14 05:12:06 +0800 |
---|---|---|
committer | Yuuta Liang <yuutaw@students.cs.ubc.ca> | 2023-10-14 05:12:06 +0800 |
commit | 0bcc057e741af3fbc108f42b75f9d42f48f6a51e (patch) | |
tree | d638c81c0778554a8902efc59000e61db74060be /src | |
parent | f369da34cf9aca151df0150d90e651e6a88ee700 (diff) | |
download | jca-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')
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")); + } +} |