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/CertificationAuthority.java | |
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/CertificationAuthority.java')
-rw-r--r-- | src/main/model/ca/CertificationAuthority.java | 480 |
1 files changed, 480 insertions, 0 deletions
diff --git a/src/main/model/ca/CertificationAuthority.java b/src/main/model/ca/CertificationAuthority.java new file mode 100644 index 0000000..feb557c --- /dev/null +++ b/src/main/model/ca/CertificationAuthority.java @@ -0,0 +1,480 @@ +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; +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.InvalidKeySpecException; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.*; +import java.util.stream.Stream; + +/** + * Holds a CA private key, its certificate, signed / revoked list, template list, and logs list. + */ +public class CertificationAuthority { + /** + * The RSA2048 private key. + */ + private RSAPrivateKey key; + + /** + * The public key. + */ + private RSAPublicKey publicKey; + + /** + * The signed certificate. + */ + private Certificate certificate; + + /** + * Signed certificates. + */ + private final List<Certificate> signed; + + /** + * The next serial number. + */ + private int serial; + + /** + * Revoked certs. + */ + 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, revoked template, and log list, serial at 1, and + * user "yuuta". + */ + 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. 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); + final KeyPair pair = gen.generateKeyPair(); + this.key = (RSAPrivateKey) pair.getPrivate(); + this.publicKey = (RSAPublicKey) pair.getPublic(); + log("Generated CA private key."); + } + + /** + * 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 + */ + 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) 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, + 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 = getPublicKey(); + 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. + * MODIFIES: this (This action will be logged) + */ + public CertificationRequest signCSR() + throws ParseException, NoSuchAlgorithmException, SignatureException, InvalidKeyException { + final CertificationRequestInfo info = generateCSR(); + 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; + } + + /** + * 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); + log("Signed a cert with serial number " + cert.getCertificate().getSerialNumber()); + 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); + 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 + * MODIFIES: this + */ + 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. 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 { + 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])); + 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 List.copyOf(signed); + } + + /** + * EFFECT: Get a read-only view of the revoked certificates. + */ + public List<RevokedCertificate> getRevoked() { + 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; + } +} |