diff options
author | Yuuta Liang <yuutaw@student.cs.ubc.ca> | 2023-10-25 03:30:45 +0800 |
---|---|---|
committer | Yuuta Liang <yuutaw@student.cs.ubc.ca> | 2023-10-25 03:30:45 +0800 |
commit | d7ff9d5e217873609d79efe279f2634e3a3dd8b4 (patch) | |
tree | 704729e5eed658728b521acd407c6ca767f7e865 /src/main/model/ca | |
parent | 55df54e5dbf26e6824123410784d00aa793c3781 (diff) | |
download | jca-d7ff9d5e217873609d79efe279f2634e3a3dd8b4.tar jca-d7ff9d5e217873609d79efe279f2634e3a3dd8b4.tar.gz jca-d7ff9d5e217873609d79efe279f2634e3a3dd8b4.tar.bz2 jca-d7ff9d5e217873609d79efe279f2634e3a3dd8b4.zip |
Refactor: move all logics into CertificationAuthority
Signed-off-by: Yuuta Liang <yuutaw@student.cs.ubc.ca>
Diffstat (limited to 'src/main/model/ca')
-rw-r--r-- | src/main/model/ca/CertificationAuthority.java (renamed from src/main/model/ca/CACertificate.java) | 267 | ||||
-rw-r--r-- | src/main/model/ca/Template.java | 53 |
2 files changed, 255 insertions, 65 deletions
diff --git a/src/main/model/ca/CACertificate.java b/src/main/model/ca/CertificationAuthority.java index 1bd53c9..feb557c 100644 --- a/src/main/model/ca/CACertificate.java +++ b/src/main/model/ca/CertificationAuthority.java @@ -1,11 +1,14 @@ package model.ca; import model.asn1.*; +import model.asn1.exceptions.InvalidCAException; import model.asn1.exceptions.ParseException; +import model.asn1.parsing.BytesReader; import model.csr.*; import model.pki.AlgorithmIdentifier; import model.pki.SubjectPublicKeyInfo; import model.pki.cert.Certificate; +import model.pki.cert.Extension; import model.pki.cert.TbsCertificate; import model.pki.cert.Validity; import model.pki.crl.CertificateList; @@ -18,23 +21,29 @@ import ui.Utils; import java.math.BigInteger; import java.security.*; +import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; +import java.util.*; import java.util.stream.Stream; /** - * Holds a CA private key, its certificate, and signed / revoked list. + * Holds a CA private key, its certificate, signed / revoked list, template list, and logs list. */ -public class CACertificate { +public class CertificationAuthority { /** - * The key pair. + * The RSA2048 private key. */ - private KeyPair key; + private RSAPrivateKey key; + + /** + * The public key. + */ + private RSAPublicKey publicKey; /** * The signed certificate. @@ -44,7 +53,7 @@ public class CACertificate { /** * Signed certificates. */ - private List<Certificate> signed; + private final List<Certificate> signed; /** * The next serial number. @@ -54,45 +63,153 @@ public class CACertificate { /** * Revoked certs. */ - private List<RevokedCertificate> revoked; + private final List<RevokedCertificate> revoked; + + /** + * Certificate templates. + */ + private final List<Template> templates; + + /** + * Audit logs. + */ + private final List<AuditLogEntry> logs; + + /** + * Current operator. + */ + private final String user; /** - * EFFECT: Init with a null key and null certificate, empty signed and revoked list, and serial at 1. + * EFFECT: Init with a null key and null certificate, empty signed, revoked template, and log list, serial at 1, and + * user "yuuta". */ - public CACertificate() { + public CertificationAuthority() { this.key = null; + this.publicKey = null; this.certificate = null; this.serial = 1; this.signed = new ArrayList<>(); this.revoked = new ArrayList<>(); + this.templates = new ArrayList<>(); + this.logs = new ArrayList<>(); + this.user = "yuuta"; } /** - * EFFECTS: Generate a new RSA2048 private key. + * EFFECTS: Generate a new RSA2048 private key. This action will be logged. * REQUIRES: getPublicKey() is null (i.e., no private key had been installed) + * MODIFIES: this */ public void generateKey() throws NoSuchAlgorithmException { final KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); gen.initialize(2048); - this.key = gen.generateKeyPair(); + final KeyPair pair = gen.generateKeyPair(); + this.key = (RSAPrivateKey) pair.getPrivate(); + this.publicKey = (RSAPublicKey) pair.getPublic(); + log("Generated CA private key."); } /** - * EFFECT: Install the CA certificate. + * EFFECTS: Load the RSA private and public exponents. This action will be logged. + * 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) * MODIFIES: this - * REQUIRES: - * - The new certificate must have the same algorithm and public key as getPublicKey(), except for testing purpose + */ + public void loadKey(BigInteger n, BigInteger p, BigInteger e) + throws NoSuchAlgorithmException, InvalidKeySpecException { + this.key = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new RSAPrivateKeySpec(n, p)); + this.publicKey = + (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(n, e)); + log("Installed CA private key."); + } + + /** + * EFFECTS: Throw {@link InvalidCAException} if the incoming cert is not v3. + */ + private void validateCACertificateVersion(Certificate cert) throws InvalidCAException { + if (cert.getCertificate().getVersion() == null + || cert.getCertificate().getVersion().getLong() != TbsCertificate.VERSION_V3) { + throw new InvalidCAException("The input certificate must be V3"); + } + } + + /** + * EFFECTS: Throw {@link InvalidCAException} if the incoming cert does not have the matching public key. + */ + private void validateCACertificatePublicKey(Certificate cert) throws InvalidCAException { + final SubjectPublicKeyInfo expectedPKInfo = 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 InvalidCAException("The input certificate does not have the corresponding public key"); + } + } + + /** + * EFFECTS: Throw {@link InvalidCAException} if the incoming cert does not have cA = true in its basicConstraints. + */ + private void validateCACertificateBasicConstraints(Certificate cert) throws InvalidCAException, ParseException { + final Extension basicConstraints = cert.getCertificate().getExtension(ObjectIdentifier.OID_BASIC_CONSTRAINTS); + if (basicConstraints == null) { + throw new InvalidCAException("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 InvalidCAException("The certificate does not have a valid basicConstraints extension."); + } + final ASN1Object bool = + ASN1Object.parse(new BytesReader(basicConstraintsValue.encodeValueDER()), false); + if (!((Bool) bool).getValue()) { + throw new InvalidCAException("The certificate does not have a valid basicConstraints extension."); + } + } + + /** + * EFFECTS: Throw {@link InvalidCAException} if the incoming cert does not have valid key usages. + */ + private void validateCACertificateKeyUsage(Certificate cert) throws InvalidCAException, ParseException { + final Extension keyUsage = cert.getCertificate().getExtension(ObjectIdentifier.OID_KEY_USAGE); + if (keyUsage == null) { + throw new InvalidCAException("The certificate does not have a valid keyUsage extension."); + } + final ASN1Object keyUsageValue = + ASN1Object.parse(new BytesReader(keyUsage.getExtnValue().getBytes()), false); + final BitSet bitSet = BitSet.valueOf(Utils.byteToByte(((BitString) keyUsageValue).getVal())); + if (!bitSet.get(7) || !bitSet.get(2) || !bitSet.get(1)) { + throw new InvalidCAException("The certificate does not have a valid keyUsage extension."); + } + } + + /** + * EFFECT: Install the CA certificate. Throws {@link InvalidCAException} if any of the + * following are violated: * - It must be a v3 certificate + * - 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 + * Throws {@link ParseException} if the cert has invalid extension values. + * This action will be logged. + * REQUIRES: * - getCertificate() must be null (i.e., no certificate is installed yet). + * MODIFIES: this */ - public void installCertificate(Certificate certificate) { + public void installCertificate(Certificate certificate) throws InvalidCAException, ParseException { + validateCACertificateVersion(certificate); + validateCACertificatePublicKey(certificate); + validateCACertificateBasicConstraints(certificate); + validateCACertificateKeyUsage(certificate); this.certificate = certificate; + log("CA certificate is installed."); } /** * EFFECTS: Generate a CSR based on public key. It will have subject = CN=JCA. + * REQUIRES: + * - getCertificate() must be null (i.e., no certificate is installed yet). */ private CertificationRequestInfo generateCSR() throws ParseException { return new CertificationRequestInfo(ASN1Object.TAG_SEQUENCE, null, @@ -120,7 +237,7 @@ public class CACertificate { } private Byte[] getPubKeyBitStream() { - final RSAPublicKey pub = (RSAPublicKey) key.getPublic(); + final RSAPublicKey pub = getPublicKey(); final BigInteger exponent = pub.getPublicExponent(); byte[] modules = pub.getModulus().toByteArray(); final Int asn1Exponent = new Int(Int.TAG, null, exponent); @@ -151,14 +268,17 @@ public class CACertificate { /** * EFFECT: Generate CSR and sign it, so the CA can request itself a certificate. * REQUIRES: The CA cert must not be installed. + * MODIFIES: this (This action will be logged) */ public CertificationRequest signCSR() throws ParseException, NoSuchAlgorithmException, SignatureException, InvalidKeyException { final CertificationRequestInfo info = generateCSR(); - return new CertificationRequest(ASN1Object.TAG_SEQUENCE, null, + final CertificationRequest csr = new CertificationRequest(ASN1Object.TAG_SEQUENCE, null, info, getSigningAlgorithm(), new BitString(BitString.TAG, null, 0, signBytes(info.encodeDER()))); + log("Signed CA csr"); + return csr; } /** @@ -185,6 +305,7 @@ public class CACertificate { new BitString(BitString.TAG, null, 0, signBytes(newCert.encodeValueDER()))); this.signed.add(cert); + log("Signed a cert with serial number " + cert.getCertificate().getSerialNumber()); return cert; } @@ -194,7 +315,7 @@ public class CACertificate { private Byte[] signBytes(Byte[] message) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { final Signature signature = Signature.getInstance("SHA256withRSA"); - signature.initSign(key.getPrivate()); + signature.initSign(key); signature.update(Utils.byteToByte(message)); return Utils.byteToByte(signature.sign()); } @@ -205,6 +326,7 @@ public class CACertificate { * - Issuer will be set to CA#getCertificate()#getSubject() * - The template will be applied (subject, validity, cdp) * - A serial number will be generated + * MODIFIES: this */ private TbsCertificate generateCert(CertificationRequestInfo req, Template template) { final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC")); @@ -225,57 +347,134 @@ public class CACertificate { } /** - * EFFECTS: Add the revocation info to revoked list. + * EFFECTS: Add the revocation info to revoked list. This action will be logged. * 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()); } /** * 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. + * MODIFIES: this (This action will be logged) */ public CertificateList signCRL() throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { - CertificateListContent content = new CertificateListContent(ASN1Object.TAG_SEQUENCE, null, + final 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, + final CertificateList crl = new CertificateList(ASN1Object.TAG_SEQUENCE, null, content, getSigningAlgorithm(), new BitString(BitString.TAG, null, 0, signBytes(content.encodeValueDER()))); + log("Signed CRL with " + revoked.size() + " revoked certs."); + return crl; + } + + /** + * EFFECTS: Log the action with the current date and user. + * MODIFIES: this + */ + private void log(String message) { + this.logs.add(new AuditLogEntry(user, ZonedDateTime.now(), message)); + } + + /** + * 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); + } + + /** + * EFFECTS: Install the new template. This action will be logged. + * REQUIRES: findTemplate(template.getName(), false) == null + * MODIFIES: this + */ + public void addTemplate(Template template) { + this.templates.add(template); + log("Added a new template: " + template.getName()); + } + + /** + * EFFECTS: Set the given template to enabled / disabled. This action will be logged. + * 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())); + log("Template " + template.getName() + " has been " + (enable ? "enabled" : "disabled")); + } + + /** + * EFFECTS: Remove the given template. This action will be logged. + * REQUIRES: the template is valid (findTemplate does not return null) + * MODIFIES: this + */ + public void removeTemplate(Template template) { + templates.remove(findTemplate(template.getName(), false)); + log("Template " + template.getName() + " is removed"); } + // Getters + public Certificate getCertificate() { return certificate; } + /** + * EFFECT: Get a read-only view of the signed certificates. + */ public List<Certificate> getSigned() { - return signed; + return List.copyOf(signed); } /** - * EFFECTS: Get the public key, or null if no private key is installed. + * EFFECT: Get a read-only view of the revoked certificates. */ - public PublicKey getPublicKey() { - if (key == null) { - return null; - } - return key.getPublic(); - } - public List<RevokedCertificate> getRevoked() { - return revoked; + return List.copyOf(revoked); } public int getSerial() { return serial; } + + /** + * EFFECT: Get a read-only view of the templates. + */ + public List<Template> getTemplates() { + return List.copyOf(templates); + } + + public String getUser() { + return user; + } + + /** + * EFFECT: Get a read-only view of the logs. + */ + public List<AuditLogEntry> getLogs() { + return List.copyOf(logs); + } + + public RSAPublicKey getPublicKey() { + return publicKey; + } } diff --git a/src/main/model/ca/Template.java b/src/main/model/ca/Template.java index af751dc..84e639e 100644 --- a/src/main/model/ca/Template.java +++ b/src/main/model/ca/Template.java @@ -16,22 +16,33 @@ public class Template { /** * Name of the template. */ - private String name; + private final String name; /** * Whether the template is usable or not. */ - private boolean enabled; + private final boolean enabled; /** * Subject of the issued certs. Null -> unspecified */ - private Name subject; + private final Name subject; /** * Length of validity in days since the point of issue. */ - private long validity; + private final long validity; + + /** + * EFFECTS: Init with all given parameters, and commonName will be converted into CN=commonName,C=CA. + * Throws {@link ParseException} if the commonName is invalid. + */ + public Template(String name, + boolean enabled, + String commonName, + long validity) throws ParseException { + this(name, enabled, parseString(commonName), validity); + } /** * EFFECTS: Init with all given parameters. @@ -51,36 +62,24 @@ public class Template { 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; + public long getValidity() { + return validity; } /** - * EFFECTS: Set the subject to CN=commonName,C=CA - * Throws {@link ParseException} if commonName is not a valid PrintableString + * EFFECTS: Convert the given commonName to RDN of CN=commonName,C=CA + * Throws {@link ParseException} if the given commonName is invalid. */ - public void setSubject(String commonName) throws ParseException { - if (commonName == null) { - this.subject = null; - return; - } - setSubject(new Name(ASN1Object.TAG_SEQUENCE, null, new RelativeDistinguishedName[]{ + private static Name parseString(String commonName) throws ParseException { + return 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, @@ -90,14 +89,6 @@ public class Template { 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; + new PrintableString(PrintableString.TAG, null, "CA"))})}); } } |