aboutsummaryrefslogtreecommitdiff
path: root/src/main/model/ca/CertificationAuthority.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/model/ca/CertificationAuthority.java')
-rw-r--r--src/main/model/ca/CertificationAuthority.java480
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;
+ }
+}