From: Felix Dörre Date: Fri, 25 Aug 2017 22:52:48 +0000 (+0200) Subject: add: message while reporting private key compromise X-Git-Url: https://code.wpia.club/?p=gigi.git;a=commitdiff_plain;h=079f745cc7a284e6964c568f83e1293929fd899f add: message while reporting private key compromise Change-Id: I164ed07804c65e9e9396166d61e3cba645ae308e --- diff --git a/src/club/wpia/gigi/pages/main/KeyCompromiseForm.java b/src/club/wpia/gigi/pages/main/KeyCompromiseForm.java index 197f24da..84af8c31 100644 --- a/src/club/wpia/gigi/pages/main/KeyCompromiseForm.java +++ b/src/club/wpia/gigi/pages/main/KeyCompromiseForm.java @@ -15,6 +15,8 @@ import java.security.interfaces.RSAPrivateKey; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Arrays; import java.util.Base64; +import java.util.HashMap; +import java.util.Locale; import java.util.Map; import javax.servlet.http.HttpServletRequest; @@ -22,18 +24,25 @@ import javax.servlet.http.HttpServletRequest; import club.wpia.gigi.GigiApiException; import club.wpia.gigi.dbObjects.Certificate; import club.wpia.gigi.dbObjects.Certificate.CertificateStatus; +import club.wpia.gigi.dbObjects.CertificateOwner; import club.wpia.gigi.dbObjects.Job; +import club.wpia.gigi.dbObjects.Organisation; +import club.wpia.gigi.dbObjects.User; import club.wpia.gigi.localisation.Language; import club.wpia.gigi.output.template.Form; +import club.wpia.gigi.output.template.MailTemplate; import club.wpia.gigi.output.template.Template; import club.wpia.gigi.output.template.TranslateCommand; import club.wpia.gigi.util.PEM; import club.wpia.gigi.util.RandomToken; import club.wpia.gigi.util.RateLimit; import club.wpia.gigi.util.RateLimit.RateLimitException; +import club.wpia.gigi.util.ServerConstants; public class KeyCompromiseForm extends Form { + public static final String CONFIDENTIAL_MARKER = "*CONFIDENTIAL*"; + private static final Template t = new Template(KeyCompromiseForm.class.getResource("KeyCompromiseForm.templ")); // 50 per 5 min @@ -47,6 +56,8 @@ public class KeyCompromiseForm extends Form { public static final TranslateCommand NOT_FOUND = new TranslateCommand("Certificate to revoke not found"); + private static final MailTemplate revocationNotice = new MailTemplate(KeyCompromiseForm.class.getResource("RevocationNotice.templ")); + public KeyCompromiseForm(HttpServletRequest hsr) { super(hsr); challenge = RandomToken.generateToken(32); @@ -139,7 +150,52 @@ public class KeyCompromiseForm extends Form { } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } - Job j = c.revoke(challenge, Base64.getEncoder().encodeToString(signature), ""); + String message = req.getParameter("message"); + if (message != null && message.isEmpty()) { + message = null; + } + if (message != null) { + if (message.startsWith(CONFIDENTIAL_MARKER)) { + message = " " + message; + } + String confidential = req.getParameter("confidential"); + if (confidential != null && !confidential.isEmpty()) { + message = CONFIDENTIAL_MARKER + "\r\n" + message; + } + if (message.contains("---")) { + throw new GigiApiException("Your message may not contain '---'."); + } + // convert all line endings to CRLF + message = message.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "\r\n"); + if ( !message.matches("[ -~\r\n\t]*")) { + throw new GigiApiException("Your message may only contain printable ASCII characters, tab, newline and space."); + } + } + CertificateOwner co = c.getOwner(); + String primaryEmail; + Language l = Language.getInstance(Locale.ENGLISH); + if (co instanceof User) { + primaryEmail = ((User) co).getEmail(); + l = Language.getInstance(((User) co).getPreferredLocale()); + } else if (co instanceof Organisation) { + primaryEmail = ((Organisation) co).getContactEmail(); + } else { + throw new IllegalArgumentException("certificate owner of unknown type"); + } + HashMap vars = new HashMap<>(); + vars.put("appName", ServerConstants.getAppName()); + if (message != null && !message.startsWith(CONFIDENTIAL_MARKER)) { + vars.put("message", message); + } else { + vars.put("message", null); + } + vars.put("serial", c.getSerial()); + try { + revocationNotice.sendMail(l, vars, primaryEmail); + } catch (IOException e) { + throw new GigiApiException("Sending the notification mail failed."); + } + Job j = c.revoke(challenge, Base64.getEncoder().encodeToString(signature), message); if ( !j.waitFor(60000)) { throw new PermamentFormException(new GigiApiException("Revocation timed out.")); } diff --git a/src/club/wpia/gigi/pages/main/KeyCompromiseForm.templ b/src/club/wpia/gigi/pages/main/KeyCompromiseForm.templ index e75d2717..9aadd3a0 100644 --- a/src/club/wpia/gigi/pages/main/KeyCompromiseForm.templ +++ b/src/club/wpia/gigi/pages/main/KeyCompromiseForm.templ @@ -38,6 +38,17 @@ printf '%s' '' | openssl dgst -sha256 -sign + + + + +

+ + +

+ + + diff --git a/src/club/wpia/gigi/pages/main/KeyCompromisePage.java b/src/club/wpia/gigi/pages/main/KeyCompromisePage.java index c36b1763..695f725e 100644 --- a/src/club/wpia/gigi/pages/main/KeyCompromisePage.java +++ b/src/club/wpia/gigi/pages/main/KeyCompromisePage.java @@ -1,7 +1,6 @@ package club.wpia.gigi.pages.main; import java.io.IOException; -import java.util.HashMap; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -24,7 +23,7 @@ public class KeyCompromisePage extends ManagedFormPage { @Override public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { - new KeyCompromiseForm(req).output(resp.getWriter(), getLanguage(req), new HashMap()); + new KeyCompromiseForm(req).output(resp.getWriter(), getLanguage(req), getDefaultVars(req)); } } diff --git a/src/club/wpia/gigi/pages/main/RevocationNotice.templ b/src/club/wpia/gigi/pages/main/RevocationNotice.templ new file mode 100644 index 00000000..453ceea4 --- /dev/null +++ b/src/club/wpia/gigi/pages/main/RevocationNotice.templ @@ -0,0 +1,17 @@ +Subject: + + + + + + +- +- +- + + +The user reporting this compromised key provided the following message to aid you to resolve this security issue and help you to prevent further security incidents. +--- + +--- + \ No newline at end of file diff --git a/tests/club/wpia/gigi/pages/main/KeyCompromiseTest.java b/tests/club/wpia/gigi/pages/main/KeyCompromiseTest.java index 1074a519..af9cbb64 100644 --- a/tests/club/wpia/gigi/pages/main/KeyCompromiseTest.java +++ b/tests/club/wpia/gigi/pages/main/KeyCompromiseTest.java @@ -36,6 +36,8 @@ import club.wpia.gigi.util.PEM; @RunWith(Parameterized.class) public class KeyCompromiseTest extends ClientTest { + private static final String NOT_FOUND = "not found"; + private static class TestParameters { private final String query; @@ -48,6 +50,9 @@ public class KeyCompromiseTest extends ClientTest { } public String getError() { + if (NOT_FOUND.equals(error)) { + return KeyCompromiseForm.NOT_FOUND.getRaw(); + } return error; } @@ -91,13 +96,13 @@ public class KeyCompromiseTest extends ClientTest { params("cert=%cert&priv=%priv", null),// cert+key params("serial=%serial&signature=%signature", null), // Zero serial - params("serial=0000&priv=%priv", KeyCompromiseForm.NOT_FOUND.getRaw()), - params("serial=0lkd&priv=%priv", KeyCompromiseForm.NOT_FOUND.getRaw()), + params("serial=0000&priv=%priv", NOT_FOUND), + params("serial=0lkd&priv=%priv", NOT_FOUND), // tampered cert params("cert=%tamperedCert&priv=%priv", "not be parsed"), params("cert=%cert&priv=%tamperedPriv", "Private Key is malformed"), - params("serial=1&priv=%priv", KeyCompromiseForm.NOT_FOUND.getRaw()), - params("serial=1%serial&priv=%priv", KeyCompromiseForm.NOT_FOUND.getRaw()), + params("serial=1&priv=%priv", NOT_FOUND), + params("serial=1%serial&priv=%priv", NOT_FOUND), // missing certificate identification params("serial=&cert=&priv=%priv", "identification"), params("cert=&priv=%priv", "identification"), diff --git a/tests/club/wpia/gigi/pages/main/KeyCompromiseTestMessage.java b/tests/club/wpia/gigi/pages/main/KeyCompromiseTestMessage.java new file mode 100644 index 00000000..06a5ba51 --- /dev/null +++ b/tests/club/wpia/gigi/pages/main/KeyCompromiseTestMessage.java @@ -0,0 +1,126 @@ +package club.wpia.gigi.pages.main; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; + +import org.hamcrest.CoreMatchers; +import org.junit.Test; + +import club.wpia.gigi.GigiApiException; +import club.wpia.gigi.dbObjects.Certificate; +import club.wpia.gigi.dbObjects.Certificate.CertificateStatus; +import club.wpia.gigi.dbObjects.Digest; +import club.wpia.gigi.dbObjects.Job; +import club.wpia.gigi.pages.account.certs.CertificateRequest; +import club.wpia.gigi.testUtils.ClientTest; +import club.wpia.gigi.testUtils.IOUtils; +import club.wpia.gigi.testUtils.TestEmailReceiver.TestMail; +import club.wpia.gigi.util.AuthorizationContext; +import club.wpia.gigi.util.HTMLEncoder; +import club.wpia.gigi.util.PEM; + +public class KeyCompromiseTestMessage extends ClientTest { + + private Certificate cert; + + private PrivateKey priv; + + public KeyCompromiseTestMessage() throws GeneralSecurityException, IOException, GigiApiException, InterruptedException { + KeyPair kp = generateKeypair(); + priv = kp.getPrivate(); + String csr = generatePEMCSR(kp, "CN=test"); + CertificateRequest cr = new CertificateRequest(new AuthorizationContext(u, u), csr); + cr.update(CertificateRequest.DEFAULT_CN, Digest.SHA512.toString(), "client", null, null, "email:" + email + "\n"); + cert = cr.draft(); + Job j = cert.issue(null, "2y", u); + await(j); + } + + @Test + public void testExecution() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + reportCompromiseAndCheck(""); + } + + @Test + public void testNoConfidential() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + TestMail rc = reportCompromiseAndCheck("message=test+message"); + assertThat(rc.getMessage(), CoreMatchers.containsString("test message")); + } + + @Test + public void testNoConfidentialButMarker() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + TestMail rc = reportCompromiseAndCheck("message=" + URLEncoder.encode(KeyCompromiseForm.CONFIDENTIAL_MARKER + "\ntest message", "UTF-8")); + assertThat(rc.getMessage(), CoreMatchers.containsString("test message")); + assertThat(rc.getMessage(), CoreMatchers.containsString(" " + KeyCompromiseForm.CONFIDENTIAL_MARKER)); + } + + @Test + public void testConfidential() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + TestMail rc = reportCompromiseAndCheck("message=test+message&confidential=on"); + assertThat(rc.getMessage(), CoreMatchers.not(CoreMatchers.containsString("test message"))); + } + + @Test + public void testCR() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + TestMail rc = reportCompromiseAndCheck("message=test%0Dmessage&confidential=on"); + assertThat(rc.getMessage(), CoreMatchers.not(CoreMatchers.containsString("test\r\nmessage"))); + } + + @Test + public void testLF() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + TestMail rc = reportCompromiseAndCheck("message=test%0Amessage&confidential=on"); + assertThat(rc.getMessage(), CoreMatchers.not(CoreMatchers.containsString("test\r\nmessage"))); + } + + @Test + public void testCRLF() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + TestMail rc = reportCompromiseAndCheck("message=test%0D%0Amessage&confidential=on"); + assertThat(rc.getMessage(), CoreMatchers.not(CoreMatchers.containsString("test\r\nmessage"))); + } + + @Test + public void testIllegalContent() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + HttpURLConnection rc = reportCompromise("message=test+message+---&confidential=on"); + String data = IOUtils.readURL(rc); + assertThat(data, hasError()); + assertThat(data, CoreMatchers.containsString(HTMLEncoder.encodeHTML("message may not contain '---'"))); + assertNull(getMailReceiver().poll()); + assertEquals(CertificateStatus.ISSUED, cert.getStatus()); + } + + @Test + public void testIllegalChars() throws IOException, InterruptedException, GigiApiException, GeneralSecurityException { + HttpURLConnection rc = reportCompromise("message=" + URLEncoder.encode("§", "UTF-8")); + String data = IOUtils.readURL(rc); + assertThat(data, hasError()); + assertThat(data, CoreMatchers.containsString("may only contain printable ASCII characters")); + assertEquals(CertificateStatus.ISSUED, cert.getStatus()); + } + + private TestMail reportCompromiseAndCheck(String params) throws IOException, UnsupportedEncodingException, CertificateEncodingException, GeneralSecurityException { + HttpURLConnection huc = reportCompromise(params); + assertThat(IOUtils.readURL(huc), hasNoError()); + TestMail rc = getMailReceiver().receive(); + assertEquals(u.getEmail(), rc.getTo()); + assertThat(rc.getMessage(), CoreMatchers.containsString(cert.getSerial())); + assertEquals(CertificateStatus.REVOKED, cert.getStatus()); + return rc; + } + + private HttpURLConnection reportCompromise(String params) throws IOException, UnsupportedEncodingException, CertificateEncodingException, GeneralSecurityException { + if ( !params.isEmpty() && !params.startsWith("&")) { + params = "&" + params; + } + HttpURLConnection huc = post(KeyCompromisePage.PATH, "cert=" + URLEncoder.encode(PEM.encode("CERTIFICATE", cert.cert().getEncoded()), "UTF-8")// + + "&priv=" + URLEncoder.encode(PEM.encode("PRIVATE KEY", priv.getEncoded()), "UTF-8") + params); + return huc; + } +}