This commit moves the information about certificate revocation into an
own table. All locations accessing this information have been adapted.
Old Behavior:
Table "certs" has a column "revoked" that is "NULL" for all non-revoked
certificates and contains the revocation time (from cassiopeia) for all
revoked certificates.
on revoke: gigi creates a "job" indicating the row in "certs" to revoke
on revoke execution: cassiopeia marks the "job" done and inserts a
revocation date in "certs"
Possible States:
1. revoked is NULL, no revocation job exists
2. revoked is NULL, revocation job exists.
(2b. this transition is not a transaction in cassiopeia so an in-between
state might be observed)
3. revoked is not NULL, revocation job is marked as done
New Behavior:
Table "certs" doesn't have a "revoked" column instead there is a new
table.
on revoke: gigi inserts the "revocation"-row without a revocation date
filled in, but with the reason and other information about the
revocation. Additionally a "job" is created to trigger cassiopeia.
on revoke execution: cassiopeia marks the job as done and updates the
revocation date in the revocation column.
Possible States:
1. no revocation entry and not revocation job exist.
(1b. again strictly speaking not a transaction here)
2. revocation entry without date and revocation job exist
(2b. also still no transaction)
3. date in revocation entry is filled and revocation job is marked done
Change-Id: Ie2a51a16eed420b284f9fd5660e057da1069b740
13 files changed:
import javax.servlet.http.HttpServletResponse;
import club.wpia.gigi.dbObjects.Certificate;
import javax.servlet.http.HttpServletResponse;
import club.wpia.gigi.dbObjects.Certificate;
+import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
+import club.wpia.gigi.dbObjects.Certificate.RevocationType;
import club.wpia.gigi.dbObjects.Job;
import club.wpia.gigi.dbObjects.User;
import club.wpia.gigi.dbObjects.Job;
import club.wpia.gigi.dbObjects.User;
-import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
public class RevokeCertificate extends APIPoint {
public class RevokeCertificate extends APIPoint {
+ Job job = c.revoke(RevocationType.USER);
job.waitFor(60000);
if (c.getStatus() != CertificateStatus.REVOKED) {
resp.sendError(510, "Error, issuing timed out");
job.waitFor(60000);
if (c.getStatus() != CertificateStatus.REVOKED) {
resp.sendError(510, "Error, issuing timed out");
- public static final int CURRENT_SCHEMA_VERSION = 28;
+ public static final int CURRENT_SCHEMA_VERSION = 29;
public static final int CONNECTION_TIMEOUT = 24 * 60 * 60;
public static final int CONNECTION_TIMEOUT = 24 * 60 * 60;
"crt_name" varchar(255) NOT NULL DEFAULT '',
"created" timestamp NULL DEFAULT NULL,
"modified" timestamp NULL DEFAULT NULL,
"crt_name" varchar(255) NOT NULL DEFAULT '',
"created" timestamp NULL DEFAULT NULL,
"modified" timestamp NULL DEFAULT NULL,
- "revoked" timestamp NULL DEFAULT NULL,
"expire" timestamp NULL DEFAULT NULL,
"renewed" boolean NOT NULL DEFAULT 'false',
"pkhash" char(40) DEFAULT NULL,
"expire" timestamp NULL DEFAULT NULL,
"renewed" boolean NOT NULL DEFAULT 'false',
"pkhash" char(40) DEFAULT NULL,
PRIMARY KEY ("id")
);
CREATE INDEX ON "certs" ("pkhash");
PRIMARY KEY ("id")
);
CREATE INDEX ON "certs" ("pkhash");
-CREATE INDEX ON "certs" ("revoked");
CREATE INDEX ON "certs" ("created");
CREATE INDEX ON "certs" ("memid");
CREATE INDEX ON "certs" ("serial");
CREATE INDEX ON "certs" ("expire");
CREATE INDEX ON "certs" ("crt_name");
CREATE INDEX ON "certs" ("created");
CREATE INDEX ON "certs" ("memid");
CREATE INDEX ON "certs" ("serial");
CREATE INDEX ON "certs" ("expire");
CREATE INDEX ON "certs" ("crt_name");
+DROP TABLE IF EXISTS "certsRevoked";
+DROP TYPE IF EXISTS "revocationType";
+CREATE TYPE "revocationType" AS ENUM('user', 'support', 'ping_timeout');
+CREATE TABLE "certsRevoked" (
+ "id" int NOT NULL,
+ -- the time when the certificate was revoked by cassiopeia (and that is stored in the CRL)
+ -- NULL indicated the revocation is pending
+ "revoked" timestamp NULL,
+ "type" "revocationType" NOT NULL,
+ PRIMARY KEY ("id")
+);
DROP TABLE IF EXISTS "certAvas";
DROP TABLE IF EXISTS "certAvas";
"version" smallint NOT NULL,
PRIMARY KEY ("version")
);
"version" smallint NOT NULL,
PRIMARY KEY ("version")
);
-INSERT INTO "schemeVersion" (version) VALUES(28);
+INSERT INTO "schemeVersion" (version) VALUES(29);
DROP TABLE IF EXISTS `passwordResetTickets`;
CREATE TABLE `passwordResetTickets` (
DROP TABLE IF EXISTS `passwordResetTickets`;
CREATE TABLE `passwordResetTickets` (
--- /dev/null
+CREATE TYPE "revocationType" AS ENUM('user', 'support', 'ping_timeout');
+CREATE TABLE "certsRevoked" (
+ "id" int NOT NULL,
+ "revoked" timestamp NULL,
+ "type" "revocationType" NOT NULL,
+ PRIMARY KEY ("id")
+);
+
+INSERT INTO "certsRevoked" (SELECT "id", "revoked", 'user' AS "type" FROM "certs" WHERE "revoked" IS NOT NULL);
+INSERT INTO "certsRevoked" (SELECT "targetId" AS "id", NULL AS "revoked", 'user' AS "type" FROM "jobs" WHERE "task" = 'revoke' AND "state" != 'done');
+
+ALTER TABLE "certs" DROP COLUMN "revoked";
public class Certificate implements IdCachable {
public class Certificate implements IdCachable {
+ public enum RevocationType implements DBEnum {
+ USER("user"), SUPPORT("support"), PING_TIMEOUT("ping_timeout");
+
+ private final String dbName;
+
+ private RevocationType(String dbName) {
+ this.dbName = dbName;
+ }
+
+ @Override
+ public String getDBName() {
+ return dbName;
+ }
+ }
+
public enum SANType implements DBEnum {
EMAIL("email"), DNS("DNS");
public enum SANType implements DBEnum {
EMAIL("email"), DNS("DNS");
}
public synchronized CertificateStatus getStatus() {
}
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 crt_name, created, `revoked`, serial, caid FROM certs LEFT JOIN `certsRevoked` ON `certs`.`id` = `certsRevoked`.`id` WHERE `certs`.id=?")) {
searcher.setInt(1, id);
GigiResultSet rs = searcher.executeQuery();
if ( !rs.next()) {
searcher.setInt(1, id);
GigiResultSet rs = searcher.executeQuery();
if ( !rs.next()) {
+ public Job revoke(RevocationType type) {
if (getStatus() != CertificateStatus.ISSUED) {
throw new IllegalStateException();
}
if (getStatus() != CertificateStatus.ISSUED) {
throw new IllegalStateException();
}
- return Job.revoke(this);
+ return Job.revoke(this, type);
public java.util.Date getRevocationDate() {
if (getStatus() == CertificateStatus.REVOKED) {
public java.util.Date getRevocationDate() {
if (getStatus() == CertificateStatus.REVOKED) {
- try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT revoked FROM certs WHERE id=?")) {
+ try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT revoked FROM `certsRevoked` WHERE id=?")) {
prep.setInt(1, getId());
GigiResultSet res = prep.executeQuery();
if (res.next()) {
prep.setInt(1, getId());
GigiResultSet res = prep.executeQuery();
if (res.next()) {
}
public Certificate[] getCertificates(boolean includeRevoked) {
}
public Certificate[] getCertificates(boolean includeRevoked) {
- try (GigiPreparedStatement ps = new GigiPreparedStatement(includeRevoked ? "SELECT id FROM certs WHERE memid=? ORDER BY id DESC" : "SELECT id FROM certs WHERE memid=? AND revoked IS NULL ORDER BY id DESC")) {
+ try (GigiPreparedStatement ps = new GigiPreparedStatement(includeRevoked ? "SELECT id FROM certs WHERE memid=? ORDER BY id DESC" : "SELECT id FROM certs WHERE memid=? AND NOT EXISTS (SELECT 1 FROM `certsRevoked` WHERE `certsRevoked`.`id` = `certs`.`id` AND `certsRevoked`.`revoked` IS NOT NULL) ORDER BY id DESC")) {
ps.setInt(1, getId());
GigiResultSet rs = ps.executeQuery();
ps.setInt(1, getId());
GigiResultSet rs = ps.executeQuery();
}
public static CertificateOwner getByEnabledSerial(String serial) {
}
public static CertificateOwner getByEnabledSerial(String serial) {
- try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT `memid` FROM `certs` INNER JOIN `logincerts` ON `logincerts`.`id`=`certs`.`id` WHERE serial=? AND `revoked` is NULL")) {
+ try (GigiPreparedStatement prep = new GigiPreparedStatement("SELECT `memid` FROM `certs` INNER JOIN `logincerts` ON `logincerts`.`id`=`certs`.`id` WHERE serial=? AND NOT EXISTS (SELECT 1 FROM `certsRevoked` WHERE `certsRevoked`.`id` = `certs`.`id` AND `certsRevoked`.`revoked` IS NOT NULL)")) {
prep.setString(1, serial);
GigiResultSet res = prep.executeQuery();
if (res.next()) {
prep.setString(1, serial);
GigiResultSet res = prep.executeQuery();
if (res.next()) {
import club.wpia.gigi.database.DBEnum;
import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.database.GigiResultSet;
import club.wpia.gigi.database.DBEnum;
import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.database.GigiResultSet;
+import club.wpia.gigi.dbObjects.Certificate.RevocationType;
import club.wpia.gigi.output.CertificateValiditySelector;
public class Job implements IdCachable {
import club.wpia.gigi.output.CertificateValiditySelector;
public class Job implements IdCachable {
- public synchronized static Job sign(Certificate targetId, Date start, String period) throws GigiApiException {
+ protected synchronized static Job sign(Certificate targetId, Date start, String period) throws GigiApiException {
CertificateValiditySelector.checkValidityLength(period);
try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `jobs` SET targetId=?, task=?::`jobType`, executeFrom=?, executeTo=?")) {
ps.setInt(1, targetId.getId());
CertificateValiditySelector.checkValidityLength(period);
try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `jobs` SET targetId=?, task=?::`jobType`, executeFrom=?, executeTo=?")) {
ps.setInt(1, targetId.getId());
- public synchronized static Job revoke(Certificate targetId) {
+ protected synchronized static Job revoke(Certificate targetId, RevocationType type) {
+ try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `certsRevoked` SET id=?, type=?::`revocationType`")) {
+ ps.setInt(1, targetId.getId());
+ ps.setEnum(2, type);
+ ps.execute();
+ }
try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `jobs` SET targetId=?, task=?::`jobType`")) {
ps.setInt(1, targetId.getId());
try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `jobs` SET targetId=?, task=?::`jobType`")) {
ps.setInt(1, targetId.getId());
import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.database.GigiResultSet;
import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.database.GigiResultSet;
import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
+import club.wpia.gigi.dbObjects.Certificate.RevocationType;
import club.wpia.gigi.dbObjects.Country.CountryCodeType;
import club.wpia.gigi.dbObjects.wrappers.DataContainer;
import club.wpia.gigi.dbObjects.Country.CountryCodeType;
import club.wpia.gigi.dbObjects.wrappers.DataContainer;
}
for (Certificate cert : getCertificates(false)) {
if (cert.getStatus() == CertificateStatus.ISSUED) {
}
for (Certificate cert : getCertificates(false)) {
if (cert.getStatus() == CertificateStatus.ISSUED) {
+ cert.revoke(RevocationType.USER);
}
}
try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `organisations` SET `name`=?, `country`=?, `province`=?, `city`=? WHERE `id`=?")) {
}
}
try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `organisations` SET `name`=?, `country`=?, `province`=?, `city`=? WHERE `id`=?")) {
import club.wpia.gigi.GigiApiException;
import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
import club.wpia.gigi.GigiApiException;
import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
+import club.wpia.gigi.dbObjects.Certificate.RevocationType;
import club.wpia.gigi.localisation.Language;
import club.wpia.gigi.output.template.MailTemplate;
import club.wpia.gigi.output.template.Outputable;
import club.wpia.gigi.localisation.Language;
import club.wpia.gigi.output.template.MailTemplate;
import club.wpia.gigi.output.template.Outputable;
// TODO Check for open jobs!
for (int i = 0; i < certs.length; i++) {
if (certs[i].getStatus() == CertificateStatus.ISSUED) {
// TODO Check for open jobs!
for (int i = 0; i < certs.length; i++) {
if (certs[i].getStatus() == CertificateStatus.ISSUED) {
+ certs[i].revoke(RevocationType.SUPPORT);
}
}
// send notification to support
}
}
// send notification to support
// TODO Check for open jobs!
if (cert.getStatus() == CertificateStatus.ISSUED) {
writeSELog("SE Revoke certificate");
// TODO Check for open jobs!
if (cert.getStatus() == CertificateStatus.ISSUED) {
writeSELog("SE Revoke certificate");
- cert.revoke().waitFor(60000);
+ cert.revoke(RevocationType.SUPPORT).waitFor(60000);
// send notification to support
String subject = "Revoke certificate";
Outputable message = SprintfCommand.createSimple("Certificate with serial number {0} for {1} <{2}> has been revoked.", cert.getSerial(), target.getPreferredName().toString(), target.getEmail());
// send notification to support
String subject = "Revoke certificate";
Outputable message = SprintfCommand.createSimple("Certificate with serial number {0} for {1} <{2}> has been revoked.", cert.getSerial(), target.getPreferredName().toString(), target.getEmail());
import club.wpia.gigi.GigiApiException;
import club.wpia.gigi.dbObjects.Certificate;
import club.wpia.gigi.GigiApiException;
import club.wpia.gigi.dbObjects.Certificate;
+import club.wpia.gigi.dbObjects.Certificate.RevocationType;
import club.wpia.gigi.dbObjects.CertificateOwner;
import club.wpia.gigi.dbObjects.Job;
import club.wpia.gigi.localisation.Language;
import club.wpia.gigi.dbObjects.CertificateOwner;
import club.wpia.gigi.dbObjects.Job;
import club.wpia.gigi.localisation.Language;
if (c == null || c.getOwner() != target) {
continue;
}
if (c == null || c.getOwner() != target) {
continue;
}
- revokes.add(c.revoke());
+ revokes.add(c.revoke(RevocationType.SUPPORT));
}
long start = System.currentTimeMillis();
for (Job job : revokes) {
}
long start = System.currentTimeMillis();
for (Job job : revokes) {
import club.wpia.gigi.GigiApiException;
import club.wpia.gigi.dbObjects.Certificate;
import club.wpia.gigi.GigiApiException;
import club.wpia.gigi.dbObjects.Certificate;
+import club.wpia.gigi.dbObjects.Certificate.RevocationType;
import club.wpia.gigi.dbObjects.SupportedUser;
import club.wpia.gigi.localisation.Language;
import club.wpia.gigi.output.template.Form;
import club.wpia.gigi.dbObjects.SupportedUser;
import club.wpia.gigi.localisation.Language;
import club.wpia.gigi.output.template.Form;
if (target != null) {
target.revokeCertificate(c);
} else {
if (target != null) {
target.revokeCertificate(c);
} else {
- c.revoke().waitFor(60000);
+ c.revoke(RevocationType.USER).waitFor(60000);
}
return new RedirectResult(req.getPathInfo());
}
}
return new RedirectResult(req.getPathInfo());
}
import club.wpia.gigi.dbObjects.Certificate;
import club.wpia.gigi.dbObjects.Certificate.CSRType;
import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
import club.wpia.gigi.dbObjects.Certificate;
import club.wpia.gigi.dbObjects.Certificate.CSRType;
import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
+import club.wpia.gigi.dbObjects.Certificate.RevocationType;
import club.wpia.gigi.dbObjects.Certificate.SANType;
import club.wpia.gigi.dbObjects.Certificate.SubjectAlternateName;
import club.wpia.gigi.dbObjects.Digest;
import club.wpia.gigi.dbObjects.Certificate.SANType;
import club.wpia.gigi.dbObjects.Certificate.SubjectAlternateName;
import club.wpia.gigi.dbObjects.Digest;
assertNotNull(login(pk, cert));
assertEquals(1, countRegex(IOUtils.readURL(get(cookie, Certificates.PATH)), "<td>(?:REVOKED|ISSUED)</td>"));
assertEquals(1, countRegex(IOUtils.readURL(get(cookie, Certificates.PATH + "?withRevoked")), "<td>(?:REVOKED|ISSUED)</td>"));
assertNotNull(login(pk, cert));
assertEquals(1, countRegex(IOUtils.readURL(get(cookie, Certificates.PATH)), "<td>(?:REVOKED|ISSUED)</td>"));
assertEquals(1, countRegex(IOUtils.readURL(get(cookie, Certificates.PATH + "?withRevoked")), "<td>(?:REVOKED|ISSUED)</td>"));
+ await(c.revoke(RevocationType.USER));
testFails(CertificateStatus.REVOKED, c);
assertNull(login(pk, cert));
testFails(CertificateStatus.REVOKED, c);
assertNull(login(pk, cert));
assertEquals(status, c.getStatus());
if (status != CertificateStatus.ISSUED) {
try {
assertEquals(status, c.getStatus());
if (status != CertificateStatus.ISSUED) {
try {
+ c.revoke(RevocationType.USER);
fail(status + " is in invalid state");
} catch (IllegalStateException ise) {
fail(status + " is in invalid state");
} catch (IllegalStateException ise) {
warnMail = new GigiPreparedStatement("UPDATE jobs SET warning=warning+1, state=CASE WHEN warning<3 THEN 'open'::`jobState` ELSE 'error'::`jobState` END WHERE id=?");
revoke = new GigiPreparedStatement("SELECT certs.id, certs.csr_name,jobs.id FROM jobs INNER JOIN certs ON jobs.`targetId`=certs.id" + " WHERE jobs.state='open' AND task='revoke'");
warnMail = new GigiPreparedStatement("UPDATE jobs SET warning=warning+1, state=CASE WHEN warning<3 THEN 'open'::`jobState` ELSE 'error'::`jobState` END WHERE id=?");
revoke = new GigiPreparedStatement("SELECT certs.id, certs.csr_name,jobs.id FROM jobs INNER JOIN certs ON jobs.`targetId`=certs.id" + " WHERE jobs.state='open' AND task='revoke'");
- revokeCompleted = new GigiPreparedStatement("UPDATE certs SET revoked=NOW() WHERE id=?");
+ revokeCompleted = new GigiPreparedStatement("UPDATE `certsRevoked` SET revoked=NOW() WHERE id=?");
finishJob = new GigiPreparedStatement("UPDATE jobs SET state='done' WHERE id=?");
finishJob = new GigiPreparedStatement("UPDATE jobs SET state='done' WHERE id=?");
worked = true;
System.out.println("Revoke faked: " + id);
revokeCompleted.setInt(1, id);
worked = true;
System.out.println("Revoke faked: " + id);
revokeCompleted.setInt(1, id);
- revokeCompleted.execute();
+ revokeCompleted.executeUpdate();
finishJob.setInt(1, rs.getInt(3));
finishJob.setInt(1, rs.getInt(3));
+ finishJob.executeUpdate();
}
if (worked) {
gencrl();
}
if (worked) {
gencrl();
updateMail.setInt(3, caRs.getInt("id"));
updateMail.setTimestamp(4, new Timestamp(toDate.getTime()));
updateMail.setInt(5, id);
updateMail.setInt(3, caRs.getInt("id"));
updateMail.setTimestamp(4, new Timestamp(toDate.getTime()));
updateMail.setInt(5, id);
+ updateMail.executeUpdate();
finishJob.setInt(1, rs.getInt("jobid"));
finishJob.setInt(1, rs.getInt("jobid"));
+ finishJob.executeUpdate();
System.out.println("signed: " + id);
continue;
}
System.out.println("signed: " + id);
continue;
}
}
System.out.println("Error with: " + id);
warnMail.setInt(1, rs.getInt("jobid"));
}
System.out.println("Error with: " + id);
warnMail.setInt(1, rs.getInt("jobid"));
+ warnMail.executeUpdate();