From 0b0af7389db0efd9cc72f74fb69f4a2a304563ae Mon Sep 17 00:00:00 2001 From: =?utf8?q?Felix=20D=C3=B6rre?= Date: Sat, 23 Aug 2014 13:20:40 +0200 Subject: [PATCH] Implement fixed-time signing. --- doc/tableStructure.sql | 2 + src/org/cacert/gigi/Certificate.java | 24 +- src/org/cacert/gigi/Launcher.java | 2 + .../output/CertificateValiditySelector.java | 120 ++++++---- .../pages/account/CertificateIssueForm.java | 2 +- src/org/cacert/gigi/util/Job.java | 19 +- tests/org/cacert/gigi/TestCertificate.java | 16 +- .../cacert/gigi/TestSeparateSessionScope.java | 4 +- util/org/cacert/gigi/util/SimpleSigner.java | 225 +++++++++++------- 9 files changed, 264 insertions(+), 150 deletions(-) diff --git a/doc/tableStructure.sql b/doc/tableStructure.sql index 69f85347..3fe84bb0 100644 --- a/doc/tableStructure.sql +++ b/doc/tableStructure.sql @@ -173,6 +173,8 @@ CREATE TABLE `jobs` ( `task` enum('sign','revoke') NOT NULL, `state` enum('open', 'done', 'error') NOT NULL DEFAULT 'open', `warning` int(2) NOT NULL DEFAULT '0', + `executeFrom` DATE, + `executeTo` VARCHAR(11), PRIMARY KEY (`id`), KEY `state` (`state`) ) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1; diff --git a/src/org/cacert/gigi/Certificate.java b/src/org/cacert/gigi/Certificate.java index 433e60e4..8094419b 100644 --- a/src/org/cacert/gigi/Certificate.java +++ b/src/org/cacert/gigi/Certificate.java @@ -8,6 +8,7 @@ import java.io.InputStream; import java.security.GeneralSecurityException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; @@ -18,7 +19,6 @@ import java.util.List; import org.cacert.gigi.database.DatabaseConnection; import org.cacert.gigi.util.Job; -import org.cacert.gigi.util.Job.JobType; import org.cacert.gigi.util.KeyStorage; import org.cacert.gigi.util.Notary; @@ -219,7 +219,23 @@ public class Certificate { return CertificateStatus.REVOKED; } - public Job issue() throws IOException, SQLException { + /** + * @param start + * the date from which on the certificate should be valid. (or + * null if it should be valid instantly) + * @param period + * the period for which the date should be valid. (a + * yyyy-mm-dd or a "2y" (2 calendar years), "6m" (6 + * months) + * @return A job which can be used to monitor the progress of this task. + * @throws IOException + * for problems with writing the CSR/SPKAC + * @throws SQLException + * for problems with writing to the DB + * @throws GigiApiException + * if the period is bogus + */ + public Job issue(Date start, String period) throws IOException, SQLException, GigiApiException { if (getStatus() != CertificateStatus.DRAFT) { throw new IllegalStateException(); } @@ -252,7 +268,7 @@ public class Certificate { updater.setString(1, csrName); updater.setInt(2, id); updater.execute(); - return Job.submit(this, JobType.SIGN); + return Job.sign(this, start, period); } @@ -260,7 +276,7 @@ public class Certificate { if (getStatus() != CertificateStatus.ISSUED) { throw new IllegalStateException(); } - return Job.submit(this, JobType.REVOKE); + return Job.revoke(this); } diff --git a/src/org/cacert/gigi/Launcher.java b/src/org/cacert/gigi/Launcher.java index f056be96..10488f05 100644 --- a/src/org/cacert/gigi/Launcher.java +++ b/src/org/cacert/gigi/Launcher.java @@ -10,6 +10,7 @@ import java.security.UnrecoverableKeyException; import java.security.cert.Certificate; import java.util.List; import java.util.Properties; +import java.util.TimeZone; import javax.net.ssl.ExtendedSSLSession; import javax.net.ssl.SNIHostName; @@ -46,6 +47,7 @@ import org.eclipse.jetty.util.ssl.SslContextFactory; public class Launcher { public static void main(String[] args) throws Exception { + TimeZone.setDefault(TimeZone.getTimeZone("UTC")); GigiConfig conf = GigiConfig.parse(System.in); ServerConstants.init(conf.getMainProps()); initEmails(conf); diff --git a/src/org/cacert/gigi/output/CertificateValiditySelector.java b/src/org/cacert/gigi/output/CertificateValiditySelector.java index 95eaa09e..a58ca873 100644 --- a/src/org/cacert/gigi/output/CertificateValiditySelector.java +++ b/src/org/cacert/gigi/output/CertificateValiditySelector.java @@ -3,31 +3,40 @@ package org.cacert.gigi.output; import java.io.PrintWriter; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; +import java.sql.Date; import java.util.Map; import java.util.TimeZone; import javax.servlet.http.HttpServletRequest; +import org.cacert.gigi.GigiApiException; import org.cacert.gigi.localisation.Language; +import org.cacert.gigi.util.HTMLEncoder; public class CertificateValiditySelector implements Outputable { - SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); + private static ThreadLocal fmt = new ThreadLocal<>(); private static final int DAY = 1000 * 60 * 60 * 24; - Date from; + private Date from; - String val; + private String val = "2y"; public CertificateValiditySelector() { - fmt.setTimeZone(TimeZone.getTimeZone("UTC")); } + public static SimpleDateFormat getDateFormat() { + SimpleDateFormat local = fmt.get(); + if (local == null) { + local = new SimpleDateFormat("yyyy-MM-dd"); + local.setTimeZone(TimeZone.getTimeZone("UTC")); + fmt.set(local); + } + return local; + } + @Override public void output(PrintWriter out, Language l, Map vars) { out.print(""); - out.print(""); - - out.print(""); - - out.print(""); - out.println(""); + out.print(""); if (from == null) { return; } - // debug dummy output - Calendar c = GregorianCalendar.getInstance(); - c.setTime(from); - if ("6m".equals(val)) { - c.add(Calendar.MONTH, 6); - } else if ("1y".equals(val)) { - c.add(Calendar.YEAR, 1); - } else if ("2y".equals(val)) { - c.add(Calendar.YEAR, 2); - } - out.println("From: " + fmt.format(from)); - out.println("To: " + fmt.format(c.getTime())); + } private long getCurrentDayBase() { @@ -97,19 +79,71 @@ public class CertificateValiditySelector implements Outputable { return base; } - public void update(HttpServletRequest r) { + public void update(HttpServletRequest r) throws GigiApiException { String from = r.getParameter("validFrom"); + + GigiApiException gae = new GigiApiException(); + try { + saveStartDate(from); + } catch (GigiApiException e) { + gae.mergeInto(e); + } + try { + String validity = r.getParameter("validity"); + if (validity != null) { + checkValidityLength(validity); + val = validity; + } + } catch (GigiApiException e) { + gae.mergeInto(e); + } + if ( !gae.isEmpty()) { + throw gae; + } + + } + + public static void checkValidityLength(String newval) throws GigiApiException { + if (newval.endsWith("y") || newval.endsWith("m")) { + if (newval.length() > 10) { // for database + throw new GigiApiException("The validity interval entered is invalid."); + } + String num = newval.substring(0, newval.length() - 1); + try { + int len = Integer.parseInt(num); + if (len <= 0) { + throw new GigiApiException("The validity interval entered is invalid."); + } + } catch (NumberFormatException e) { + throw new GigiApiException("The validity interval entered is invalid."); + } + } else { + try { + getDateFormat().parse(newval); + } catch (ParseException e) { + throw new GigiApiException("The validity interval entered is invalid."); + } + } + } + + private void saveStartDate(String from) throws GigiApiException { if (from == null || "now".equals(from)) { this.from = null; } else { try { - this.from = fmt.parse(from); + this.from = new Date(getDateFormat().parse(from).getTime()); } catch (ParseException e) { - e.printStackTrace(); + throw new GigiApiException("The validity start date entered is invalid."); } } - val = r.getParameter("validity"); + } + + public Date getFrom() { + return from; + } + public String getTo() { + return val; } } diff --git a/src/org/cacert/gigi/pages/account/CertificateIssueForm.java b/src/org/cacert/gigi/pages/account/CertificateIssueForm.java index 4c56dbdf..086d51a3 100644 --- a/src/org/cacert/gigi/pages/account/CertificateIssueForm.java +++ b/src/org/cacert/gigi/pages/account/CertificateIssueForm.java @@ -300,7 +300,7 @@ public class CertificateIssueForm extends Form { result = new Certificate(LoginPage.getUser(req).getId(), subject.toString(), selectedDigest.toString(), // this.csr, this.csrType, profile, SANs.toArray(new SubjectAlternateName[SANs.size()])); - result.issue().waitFor(60000); + result.issue(issueDate.getFrom(), issueDate.getTo()).waitFor(60000); return true; } } catch (IOException e) { diff --git a/src/org/cacert/gigi/util/Job.java b/src/org/cacert/gigi/util/Job.java index 13e6c7e0..959c14f5 100644 --- a/src/org/cacert/gigi/util/Job.java +++ b/src/org/cacert/gigi/util/Job.java @@ -1,11 +1,14 @@ package org.cacert.gigi.util; +import java.sql.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.cacert.gigi.Certificate; +import org.cacert.gigi.GigiApiException; import org.cacert.gigi.database.DatabaseConnection; +import org.cacert.gigi.output.CertificateValiditySelector; public class Job { @@ -29,10 +32,22 @@ public class Job { } } - public static Job submit(Certificate targetId, JobType type) throws SQLException { + public static Job sign(Certificate targetId, Date start, String period) throws SQLException, GigiApiException { + CertificateValiditySelector.checkValidityLength(period); + PreparedStatement ps = DatabaseConnection.getInstance().prepare("INSERT INTO `jobs` SET targetId=?, task=?, executeFrom=?, executeTo=?"); + ps.setInt(1, targetId.getId()); + ps.setString(2, JobType.SIGN.getName()); + ps.setDate(3, start); + ps.setString(4, period); + ps.execute(); + return new Job(DatabaseConnection.lastInsertId(ps)); + } + + public static Job revoke(Certificate targetId) throws SQLException { + PreparedStatement ps = DatabaseConnection.getInstance().prepare("INSERT INTO `jobs` SET targetId=?, task=?"); ps.setInt(1, targetId.getId()); - ps.setString(2, type.getName()); + ps.setString(2, JobType.REVOKE.getName()); ps.execute(); return new Job(DatabaseConnection.lastInsertId(ps)); } diff --git a/tests/org/cacert/gigi/TestCertificate.java b/tests/org/cacert/gigi/TestCertificate.java index d14d0487..a1972304 100644 --- a/tests/org/cacert/gigi/TestCertificate.java +++ b/tests/org/cacert/gigi/TestCertificate.java @@ -22,25 +22,25 @@ import static org.junit.Assert.*; public class TestCertificate extends ManagedTest { @Test - public void testClientCertLoginStates() throws IOException, GeneralSecurityException, SQLException, InterruptedException { + public void testClientCertLoginStates() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException { KeyPair kp = generateKeypair(); String key1 = generatePEMCSR(kp, "CN=testmail@example.com"); Certificate c = new Certificate(1, "/CN=testmail@example.com", "sha256", key1, CSRType.CSR, CertificateProfile.getById(1)); final PrivateKey pk = kp.getPrivate(); - c.issue().waitFor(60000); + c.issue(null, "2y").waitFor(60000); final X509Certificate ce = c.cert(); assertNotNull(login(pk, ce)); } @Test - public void testSANs() throws IOException, GeneralSecurityException, SQLException, InterruptedException { + public void testSANs() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException { KeyPair kp = generateKeypair(); String key = generatePEMCSR(kp, "CN=testmail@example.com"); Certificate c = new Certificate(1, "/CN=testmail@example.com", "sha256", key, CSRType.CSR, CertificateProfile.getById(1),// new SubjectAlternateName(SANType.EMAIL, "testmail@example.com"), new SubjectAlternateName(SANType.DNS, "testmail.example.com")); testFails(CertificateStatus.DRAFT, c); - c.issue().waitFor(60000); + c.issue(null, "2y").waitFor(60000); X509Certificate cert = c.cert(); Collection> sans = cert.getSubjectAlternativeNames(); assertEquals(2, sans.size()); @@ -84,14 +84,14 @@ public class TestCertificate extends ManagedTest { } @Test - public void testCertLifeCycle() throws IOException, GeneralSecurityException, SQLException, InterruptedException { + public void testCertLifeCycle() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException { KeyPair kp = generateKeypair(); String key = generatePEMCSR(kp, "CN=testmail@example.com"); Certificate c = new Certificate(1, "/CN=testmail@example.com", "sha256", key, CSRType.CSR, CertificateProfile.getById(1)); final PrivateKey pk = kp.getPrivate(); testFails(CertificateStatus.DRAFT, c); - c.issue().waitFor(60000); + c.issue(null, "2y").waitFor(60000); testFails(CertificateStatus.ISSUED, c); X509Certificate cert = c.cert(); @@ -103,7 +103,7 @@ public class TestCertificate extends ManagedTest { } - private void testFails(CertificateStatus status, Certificate c) throws IOException, GeneralSecurityException, SQLException { + private void testFails(CertificateStatus status, Certificate c) throws IOException, GeneralSecurityException, SQLException, GigiApiException { assertEquals(status, c.getStatus()); if (status != CertificateStatus.ISSUED) { try { @@ -115,7 +115,7 @@ public class TestCertificate extends ManagedTest { } if (status != CertificateStatus.DRAFT) { try { - c.issue(); + c.issue(null, "2y"); fail(status + " is in invalid state"); } catch (IllegalStateException ise) { diff --git a/tests/org/cacert/gigi/TestSeparateSessionScope.java b/tests/org/cacert/gigi/TestSeparateSessionScope.java index 840249f4..961b3e32 100644 --- a/tests/org/cacert/gigi/TestSeparateSessionScope.java +++ b/tests/org/cacert/gigi/TestSeparateSessionScope.java @@ -18,7 +18,7 @@ import org.junit.Test; public class TestSeparateSessionScope extends ManagedTest { @Test - public void testSeparateScope() throws IOException, GeneralSecurityException, SQLException, InterruptedException { + public void testSeparateScope() throws IOException, GeneralSecurityException, SQLException, InterruptedException, GigiApiException { String mail = "thisgo" + createUniqueName() + "@example.com"; int user = createAssuranceUser("test", "tugo", mail, TEST_PASSWORD); String cookie = login(mail, TEST_PASSWORD); @@ -26,7 +26,7 @@ public class TestSeparateSessionScope extends ManagedTest { String csr = generatePEMCSR(kp, "CN=felix@dogcraft.de"); Certificate c = new Certificate(user, "/CN=testmail@example.com", "sha256", csr, CSRType.CSR, CertificateProfile.getById(1)); final PrivateKey pk = kp.getPrivate(); - c.issue().waitFor(60000); + c.issue(null, "2y").waitFor(60000); final X509Certificate ce = c.cert(); String scookie = login(pk, ce); diff --git a/util/org/cacert/gigi/util/SimpleSigner.java b/util/org/cacert/gigi/util/SimpleSigner.java index f0c559ac..2f0f6f6b 100644 --- a/util/org/cacert/gigi/util/SimpleSigner.java +++ b/util/org/cacert/gigi/util/SimpleSigner.java @@ -12,14 +12,19 @@ import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; +import java.util.Date; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Arrays; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Properties; +import java.util.TimeZone; import org.cacert.gigi.Certificate.CSRType; import org.cacert.gigi.database.DatabaseConnection; +import org.cacert.gigi.output.CertificateValiditySelector; public class SimpleSigner { @@ -41,6 +46,11 @@ public class SimpleSigner { private static Thread runner; + private static SimpleDateFormat sdf = new SimpleDateFormat("YYMMddHHmmss'Z'"); + static { + sdf.setTimeZone(TimeZone.getTimeZone("UTC")); + } + public static void main(String[] args) throws IOException, SQLException, InterruptedException { Properties p = new Properties(); p.load(new FileReader("config/gigi.properties")); @@ -64,7 +74,7 @@ public class SimpleSigner { throw new IllegalStateException("already running"); } running = true; - readyCerts = DatabaseConnection.getInstance().prepare("SELECT certs.id AS id, certs.csr_name, certs.subject, jobs.id AS jobid, csr_type, md, keyUsage, extendedKeyUsage FROM jobs " + // + readyCerts = DatabaseConnection.getInstance().prepare("SELECT certs.id AS id, certs.csr_name, certs.subject, jobs.id AS jobid, csr_type, md, keyUsage, extendedKeyUsage, executeFrom, executeTo FROM jobs " + // "INNER JOIN certs ON certs.id=jobs.targetId " + // "INNER JOIN profiles ON profiles.id=certs.profile " + // "WHERE jobs.state='open' "// @@ -174,103 +184,138 @@ public class SimpleSigner { private static int counter = 0; - private static void signCertificates() throws SQLException, IOException, InterruptedException { + private static void signCertificates() throws SQLException { ResultSet rs = readyCerts.executeQuery(); while (rs.next()) { String csrname = rs.getString("csr_name"); - System.out.println("sign: " + csrname); int id = rs.getInt("id"); - String csrType = rs.getString("csr_type"); - CSRType ct = CSRType.valueOf(csrType); - File crt = KeyStorage.locateCrt(id); - - String keyUsage = rs.getString("keyUsage"); - String ekeyUsage = rs.getString("extendedKeyUsage"); - getSANSs.setInt(1, id); - ResultSet san = getSANSs.executeQuery(); - - File f = new File("keys", "SANFile" + System.currentTimeMillis() + (counter++) + ".cfg"); - PrintWriter cfg = new PrintWriter(f); - boolean first = true; - while (san.next()) { - if ( !first) { - cfg.print(", "); + System.out.println("sign: " + csrname); + try { + String csrType = rs.getString("csr_type"); + CSRType ct = CSRType.valueOf(csrType); + File crt = KeyStorage.locateCrt(id); + + String keyUsage = rs.getString("keyUsage"); + String ekeyUsage = rs.getString("extendedKeyUsage"); + java.sql.Date from = rs.getDate("executeFrom"); + String length = rs.getString("executeTo"); + Date fromDate; + Date toDate; + if (from == null) { + fromDate = new Date(System.currentTimeMillis()); } else { - cfg.print("subjectAltName="); + fromDate = new Date(from.getTime()); } - first = false; - cfg.print(san.getString("type")); - cfg.print(":"); - cfg.print(san.getString("contents")); - } - cfg.println(); - cfg.println("keyUsage=" + keyUsage); - cfg.println("extendedKeyUsage=" + ekeyUsage); - cfg.close(); - - String[] call = new String[] { - "openssl", "ca",// - "-in", - "../../" + csrname,// - "-cert", - "../unassured.crt",// - "-keyfile", - "../unassured.key",// - "-out", - "../../" + crt.getPath(),// - "-utf8", - "-days", - "356",// - "-batch",// - "-md", - rs.getString("md"),// - "-extfile", - "../" + f.getName(),// - - "-subj", - rs.getString("subject"),// - "-config", - "../selfsign.config"// - - }; - if (ct == CSRType.SPKAC) { - call[2] = "-spkac"; - } - Process p1 = Runtime.getRuntime().exec(call, null, new File("keys/unassured.ca")); - - int waitFor = p1.waitFor(); - f.delete(); - if (waitFor == 0) { - try (InputStream is = new FileInputStream(crt)) { - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - X509Certificate crtp = (X509Certificate) cf.generateCertificate(is); - BigInteger serial = crtp.getSerialNumber(); - updateMail.setString(1, crt.getPath()); - updateMail.setString(2, serial.toString(16)); - updateMail.setInt(3, id); - updateMail.execute(); - - finishJob.setInt(1, rs.getInt("jobid")); - finishJob.execute(); - System.out.println("signed: " + id); - continue; - } catch (GeneralSecurityException e) { - e.printStackTrace(); + if (length.endsWith("m") || length.endsWith("y")) { + String num = length.substring(0, length.length() - 1); + int inter = Integer.parseInt(num); + Calendar c = Calendar.getInstance(); + c.setTimeZone(TimeZone.getTimeZone("UTC")); + c.setTime(fromDate); + if (length.endsWith("m")) { + c.add(Calendar.MONTH, inter); + } else { + c.add(Calendar.YEAR, inter); + } + toDate = c.getTime(); + } else { + toDate = CertificateValiditySelector.getDateFormat().parse(length); } - System.out.println("ERROR Afterwards: " + id); - warnMail.setInt(1, rs.getInt("jobid")); - warnMail.execute(); - } else { - BufferedReader br = new BufferedReader(new InputStreamReader(p1.getErrorStream())); - String s; - while ((s = br.readLine()) != null) { - System.out.println(s); + System.out.println(from); + System.out.println(sdf.format(fromDate)); + + getSANSs.setInt(1, id); + ResultSet san = getSANSs.executeQuery(); + + File f = new File("keys", "SANFile" + System.currentTimeMillis() + (counter++) + ".cfg"); + PrintWriter cfg = new PrintWriter(f); + boolean first = true; + while (san.next()) { + if ( !first) { + cfg.print(", "); + } else { + cfg.print("subjectAltName="); + } + first = false; + cfg.print(san.getString("type")); + cfg.print(":"); + cfg.print(san.getString("contents")); + } + cfg.println(); + cfg.println("keyUsage=" + keyUsage); + cfg.println("extendedKeyUsage=" + ekeyUsage); + cfg.close(); + + String[] call = new String[] { + "openssl", "ca",// + "-in", + "../../" + csrname,// + "-cert", + "../unassured.crt",// + "-keyfile", + "../unassured.key",// + "-out", + "../../" + crt.getPath(),// + "-utf8", + "-startdate", + sdf.format(fromDate),// + "-enddate", + sdf.format(toDate),// + "-batch",// + "-md", + rs.getString("md"),// + "-extfile", + "../" + f.getName(),// + + "-subj", + rs.getString("subject"),// + "-config", + "../selfsign.config"// + + }; + if (ct == CSRType.SPKAC) { + call[2] = "-spkac"; } - System.out.println(Arrays.toString(call)); - System.out.println("ERROR: " + id); - warnMail.setInt(1, rs.getInt("jobid")); - warnMail.execute(); + Process p1 = Runtime.getRuntime().exec(call, null, new File("keys/unassured.ca")); + + int waitFor = p1.waitFor(); + f.delete(); + if (waitFor == 0) { + try (InputStream is = new FileInputStream(crt)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + X509Certificate crtp = (X509Certificate) cf.generateCertificate(is); + BigInteger serial = crtp.getSerialNumber(); + updateMail.setString(1, crt.getPath()); + updateMail.setString(2, serial.toString(16)); + updateMail.setInt(3, id); + updateMail.execute(); + + finishJob.setInt(1, rs.getInt("jobid")); + finishJob.execute(); + System.out.println("signed: " + id); + continue; + } + } else { + BufferedReader br = new BufferedReader(new InputStreamReader(p1.getErrorStream())); + String s; + while ((s = br.readLine()) != null) { + System.out.println(s); + } + } + } catch (GeneralSecurityException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (SQLException e) { + e.printStackTrace(); + } catch (ParseException e) { + e.printStackTrace(); + } catch (InterruptedException e1) { + e1.printStackTrace(); } + System.out.println("Error with: " + id); + warnMail.setInt(1, rs.getInt("jobid")); + warnMail.execute(); } rs.close(); -- 2.39.2