X-Git-Url: https://code.wpia.club/?p=gigi.git;a=blobdiff_plain;f=src%2Fclub%2Fwpia%2Fgigi%2FdbObjects%2FCertificate.java;h=c46b1b477b90438386d96977b4eee9c8f0cf972f;hp=72cd3c19d57ad77c7adf8efc7f2e3adfc54c033c;hb=265f081ad3db3fd6028a1c5573462e5a2eea973a;hpb=bccd4cc0dba0f89aa045b113bac46eb8cc1dab4e diff --git a/src/club/wpia/gigi/dbObjects/Certificate.java b/src/club/wpia/gigi/dbObjects/Certificate.java index 72cd3c19..c46b1b47 100644 --- a/src/club/wpia/gigi/dbObjects/Certificate.java +++ b/src/club/wpia/gigi/dbObjects/Certificate.java @@ -1,11 +1,13 @@ package club.wpia.gigi.dbObjects; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; +import java.io.ByteArrayInputStream; import java.io.IOException; -import java.io.InputStream; +import java.math.BigInteger; import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.sql.Date; @@ -14,8 +16,11 @@ import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map.Entry; +import javax.xml.bind.DatatypeConverter; + import club.wpia.gigi.GigiApiException; import club.wpia.gigi.database.DBEnum; import club.wpia.gigi.database.GigiPreparedStatement; @@ -23,10 +28,38 @@ import club.wpia.gigi.database.GigiResultSet; import club.wpia.gigi.output.template.Outputable; import club.wpia.gigi.output.template.TranslateCommand; import club.wpia.gigi.pages.account.certs.CertificateRequest; -import club.wpia.gigi.util.KeyStorage; +import club.wpia.gigi.util.PEM; public class Certificate implements IdCachable { + public enum RevocationType implements DBEnum { + USER("user"), SUPPORT("support"), PING_TIMEOUT("ping_timeout"), KEY_COMPROMISE("key_compromise"); + + private final String dbName; + + private RevocationType(String dbName) { + this.dbName = dbName; + } + + @Override + public String getDBName() { + return dbName; + } + + public static RevocationType fromString(String s) { + return valueOf(s.toUpperCase(Locale.ENGLISH)); + } + } + + public enum AttachmentType implements DBEnum { + CSR, CRT; + + @Override + public String getDBName() { + return toString(); + } + } + public enum SANType implements DBEnum { EMAIL("email"), DNS("DNS"); @@ -122,10 +155,6 @@ public class Certificate implements IdCachable { private Digest md; - private String csrName; - - private String crtName; - private String csr = null; private CSRType csrType; @@ -140,6 +169,14 @@ public class Certificate implements IdCachable { private CACertificate ca; + private String description = ""; + + private User actor; + + public static final TranslateCommand NOT_LOADED = new TranslateCommand("Certificate could not be loaded"); + + public static final TranslateCommand NOT_PARSED = new TranslateCommand("Certificate could not be parsed"); + /** * Creates a new Certificate. WARNING: this is an internal API. Creating * certificates for users must be done using the {@link CertificateRequest} @@ -182,13 +219,15 @@ public class Certificate implements IdCachable { this.csrType = csrType; this.profile = profile; this.sans = Arrays.asList(sans); + this.actor = actor; synchronized (Certificate.class) { - try (GigiPreparedStatement inserter = new GigiPreparedStatement("INSERT INTO certs SET md=?::`mdType`, csr_type=?::`csrType`, crt_name='', memid=?, profile=?")) { + try (GigiPreparedStatement inserter = new GigiPreparedStatement("INSERT INTO certs SET md=?::`mdType`, csr_type=?::`csrType`, memid=?, profile=?, actorid=?")) { inserter.setString(1, md.toString().toLowerCase()); inserter.setString(2, this.csrType.toString()); inserter.setInt(3, owner.getId()); inserter.setInt(4, profile.getId()); + inserter.setInt(5, this.actor.getId()); inserter.execute(); id = inserter.lastInsertId(); } @@ -210,17 +249,7 @@ public class Certificate implements IdCachable { insertAVA.execute(); } } - File csrFile = KeyStorage.locateCsr(id); - csrName = csrFile.getPath(); - try (FileOutputStream fos = new FileOutputStream(csrFile)) { - fos.write(this.csr.getBytes("UTF-8")); - } - try (GigiPreparedStatement updater = new GigiPreparedStatement("UPDATE `certs` SET `csr_name`=? WHERE id=?")) { - updater.setString(1, csrName); - updater.setInt(2, id); - updater.execute(); - } - + addAttachment(AttachmentType.CSR, csr); cache.put(this); } } @@ -229,11 +258,11 @@ public class Certificate implements IdCachable { this.id = rs.getInt("id"); dnString = rs.getString("subject"); md = Digest.valueOf(rs.getString("md").toUpperCase()); - csrName = rs.getString("csr_name"); - crtName = rs.getString("crt_name"); owner = CertificateOwner.getById(rs.getInt("memid")); profile = CertificateProfile.getById(rs.getInt("profile")); this.serial = rs.getString("serial"); + this.description = rs.getString("description"); + this.actor = User.getById(rs.getInt("actorid")); try (GigiPreparedStatement ps2 = new GigiPreparedStatement("SELECT `contents`, `type` FROM `subjectAlternativeNames` WHERE `certId`=?")) { ps2.setInt(1, id); @@ -282,20 +311,19 @@ public class Certificate implements IdCachable { } public synchronized CertificateStatus getStatus() { - try (GigiPreparedStatement searcher = new GigiPreparedStatement("SELECT crt_name, created, revoked, serial, caid FROM certs WHERE id=?")) { + try (GigiPreparedStatement searcher = new GigiPreparedStatement("SELECT created, revoked, serial, caid FROM certs WHERE id=?")) { searcher.setInt(1, id); GigiResultSet rs = searcher.executeQuery(); if ( !rs.next()) { throw new IllegalStateException("Certificate not in Database"); } - crtName = rs.getString(1); - serial = rs.getString(4); - if (rs.getTimestamp(2) == null) { + serial = rs.getString(3); + if (rs.getTimestamp(1) == null) { return CertificateStatus.DRAFT; } ca = CACertificate.getById(rs.getInt("caid")); - if (rs.getTimestamp(2) != null && rs.getTimestamp(3) == null) { + if (rs.getTimestamp(1) != null && rs.getTimestamp(2) == null) { return CertificateStatus.ISSUED; } return CertificateStatus.REVOKED; @@ -325,12 +353,18 @@ public class Certificate implements IdCachable { } - public Job revoke() { + public Job revoke(RevocationType type) { if (getStatus() != CertificateStatus.ISSUED) { throw new IllegalStateException(); } - return Job.revoke(this); + return Job.revoke(this, type); + } + public Job revoke(String challenge, String signature, String message) { + if (getStatus() != CertificateStatus.ISSUED) { + throw new IllegalStateException(); + } + return Job.revoke(this, challenge, signature, message); } public CACertificate getParent() { @@ -341,23 +375,16 @@ public class Certificate implements IdCachable { return ca; } - public X509Certificate cert() throws IOException, GeneralSecurityException { + public X509Certificate cert() throws IOException, GeneralSecurityException, GigiApiException { CertificateStatus status = getStatus(); if (status != CertificateStatus.REVOKED && status != CertificateStatus.ISSUED) { throw new IllegalStateException(status + " is not wanted here."); } - InputStream is = null; - X509Certificate crt = null; - try { - is = new FileInputStream(crtName); + String crtS = getAttachment(AttachmentType.CRT); + try (ByteArrayInputStream bais = new ByteArrayInputStream(PEM.decode("CERTIFICATE", crtS))) { CertificateFactory cf = CertificateFactory.getInstance("X.509"); - crt = (X509Certificate) cf.generateCertificate(is); - } finally { - if (is != null) { - is.close(); - } + return (X509Certificate) cf.generateCertificate(bais); } - return crt; } public Certificate renew() { @@ -394,13 +421,14 @@ public class Certificate implements IdCachable { return profile; } - public synchronized static Certificate getBySerial(String serial) { - if (serial == null || "".equals(serial)) { + private static final String CONCAT = "string_agg(concat('/', `name`, '=', REPLACE(REPLACE(value, '\\\\', '\\\\\\\\'), '/', '\\\\/')), '')"; + + public synchronized static Certificate getBySerial(BigInteger serial) { + if (serial == null) { return null; } - String concat = "string_agg(concat('/', `name`, '=', REPLACE(REPLACE(value, '\\\\', '\\\\\\\\'), '/', '\\\\/')), '')"; - try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT certs.id, " + concat + " as `subject`, `md`, `csr_name`, `crt_name`,`memid`, `profile`, `certs`.`serial` FROM `certs` LEFT JOIN `certAvas` ON `certAvas`.`certId`=`certs`.`id` WHERE `serial`=? GROUP BY `certs`.`id`")) { - ps.setString(1, serial); + try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT certs.id, " + CONCAT + " as `subject`, `md`,`memid`, `profile`, `certs`.`serial`, `certs`.`description`, `certs`.`actorid` FROM `certs` LEFT JOIN `certAvas` ON `certAvas`.`certId`=`certs`.`id` WHERE `serial`=? GROUP BY `certs`.`id`")) { + ps.setString(1, serial.toString(16)); GigiResultSet rs = ps.executeQuery(); if ( !rs.next()) { return null; @@ -425,8 +453,7 @@ public class Certificate implements IdCachable { } try { - String concat = "string_agg(concat('/', `name`, '=', REPLACE(REPLACE(value, '\\\\', '\\\\\\\\'), '/', '\\\\/')), '')"; - try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT certs.id, " + concat + " as subject, md, csr_name, crt_name,memid, profile, certs.serial FROM `certs` LEFT JOIN `certAvas` ON `certAvas`.`certId`=certs.id WHERE certs.id=? GROUP BY certs.id")) { + try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT certs.id, " + CONCAT + " as subject, md, memid, profile, certs.serial, description, actorid FROM `certs` LEFT JOIN `certAvas` ON `certAvas`.`certId`=certs.id WHERE certs.id=? GROUP BY certs.id")) { ps.setInt(1, id); GigiResultSet rs = ps.executeQuery(); if ( !rs.next()) { @@ -527,4 +554,118 @@ public class Certificate implements IdCachable { } return certs; } + + public void addAttachment(AttachmentType tp, String data) throws GigiApiException { + if (getAttachment(tp) != null) { + throw new GigiApiException("Cannot override attachment"); + } + if (data == null) { + throw new GigiApiException("Attachment must not be null"); + } + try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `certificateAttachment` SET `certid`=?, `type`=?::`certificateAttachmentType`, `content`=?")) { + ps.setInt(1, getId()); + ps.setEnum(2, tp); + ps.setString(3, data); + ps.execute(); + } + } + + public String getAttachment(AttachmentType tp) throws GigiApiException { + try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `content` FROM `certificateAttachment` WHERE `certid`=? AND `type`=?::`certificateAttachmentType`")) { + ps.setInt(1, getId()); + ps.setEnum(2, tp); + GigiResultSet rs = ps.executeQuery(); + if ( !rs.next()) { + return null; + } + String s = rs.getString(1); + if (rs.next()) { + throw new GigiApiException("Invalid database state"); + } + return s; + } + } + + public void setDescription(String description) { + try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `certs` SET `description`=? WHERE `id`=?")) { + ps.setString(1, description); + ps.setInt(2, id); + ps.execute(); + } + this.description = description; + } + + public String getDescription() { + return description; + } + + public User getActor() { + return actor; + } + + public static Certificate locateCertificate(String serial, String certData) throws GigiApiException { + if (serial != null && !serial.isEmpty()) { + return getBySerial(normalizeSerial(serial)); + } + + if (certData != null && !certData.isEmpty()) { + final byte[] supplied; + final X509Certificate c0; + try { + supplied = PEM.decode("CERTIFICATE", certData); + c0 = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(supplied)); + } catch (IllegalArgumentException e1) { + throw new GigiApiException(NOT_PARSED); + } catch (CertificateException e1) { + throw new GigiApiException(NOT_PARSED); + } + try { + Certificate c = getBySerial(c0.getSerialNumber()); + if (c == null) { + return null; + } + X509Certificate cert = c.cert(); + if ( !Arrays.equals(supplied, cert.getEncoded())) { + return null; + } + return c; + } catch (IOException e) { + throw new GigiApiException(NOT_LOADED); + } catch (GeneralSecurityException e) { + throw new GigiApiException(NOT_LOADED); + } + } + throw new GigiApiException("No information to identify the correct certificate was provided."); + } + + public static BigInteger normalizeSerial(String serial) throws GigiApiException { + serial = serial.replace(" ", ""); + serial = serial.toLowerCase(); + if (serial.matches("[0-9a-f]{2}(:[0-9a-f]{2})*")) { + serial = serial.replace(":", ""); + } + int idx = 0; + while (idx < serial.length() && serial.charAt(idx) == '0') { + idx++; + } + serial = serial.substring(idx); + if ( !serial.matches("[0-9a-f]+")) { + throw new GigiApiException("Malformed serial"); + } + return new BigInteger(serial, 16); + } + + public String getFingerprint(String algorithm) throws IOException, GeneralSecurityException, GigiApiException { + X509Certificate certx = cert(); + return getFingerprint(certx, algorithm); + } + + private static String getFingerprint(X509Certificate cert, String algorithm) throws NoSuchAlgorithmException, CertificateEncodingException { + MessageDigest md = MessageDigest.getInstance(algorithm); + byte[] der = cert.getEncoded(); + md.update(der); + byte[] digest = md.digest(); + String digestHex = DatatypeConverter.printHexBinary(digest); + return digestHex.toLowerCase(); + } }