From 0bcc057e741af3fbc108f42b75f9d42f48f6a51e Mon Sep 17 00:00:00 2001 From: Yuuta Liang Date: Sat, 14 Oct 2023 05:12:06 +0800 Subject: Implement the CA Signed-off-by: Yuuta Liang --- src/main/model/asn1/ASN1Object.java | 2 +- src/main/model/asn1/Int.java | 13 +- src/main/model/ca/AuditLogEntry.java | 39 +++ src/main/model/ca/CACertificate.java | 279 +++++++++++++++++++++ src/main/model/ca/Template.java | 104 ++++++++ src/main/model/pki/cert/TbsCertificate.java | 15 ++ src/main/model/pki/crl/CertificateListContent.java | 2 +- src/main/ui/IssueScreen.java | 138 ++++++++++ src/main/ui/JCA.java | 203 +++++++++++++++ src/main/ui/Main.java | 4 +- src/main/ui/MainScreen.java | 200 +++++++++++++++ src/main/ui/MgmtScreen.java | 170 +++++++++++++ src/main/ui/Screen.java | 31 +++ src/main/ui/TemplateSetScreen.java | 131 ++++++++++ src/main/ui/TemplatesScreen.java | 108 ++++++++ src/main/ui/UIHandler.java | 45 ++++ src/main/ui/Utils.java | 44 ++-- 17 files changed, 1499 insertions(+), 29 deletions(-) create mode 100644 src/main/model/ca/AuditLogEntry.java create mode 100644 src/main/model/ca/CACertificate.java create mode 100644 src/main/model/ca/Template.java create mode 100644 src/main/ui/IssueScreen.java create mode 100644 src/main/ui/JCA.java create mode 100644 src/main/ui/MainScreen.java create mode 100644 src/main/ui/MgmtScreen.java create mode 100644 src/main/ui/Screen.java create mode 100644 src/main/ui/TemplateSetScreen.java create mode 100644 src/main/ui/TemplatesScreen.java create mode 100644 src/main/ui/UIHandler.java (limited to 'src/main') 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 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 signed; + + /** + * The next serial number. + */ + private int serial; + + /** + * Revoked certs. + */ + private List 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 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 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.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 "); + 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