]> WPIA git - gigi.git/commitdiff
add: message while reporting private key compromise
authorFelix Dörre <felix@dogcraft.de>
Fri, 25 Aug 2017 22:52:48 +0000 (00:52 +0200)
committerFelix Dörre <felix@dogcraft.de>
Thu, 31 Aug 2017 22:54:33 +0000 (00:54 +0200)
Change-Id: I164ed07804c65e9e9396166d61e3cba645ae308e

src/club/wpia/gigi/pages/main/KeyCompromiseForm.java
src/club/wpia/gigi/pages/main/KeyCompromiseForm.templ
src/club/wpia/gigi/pages/main/KeyCompromisePage.java
src/club/wpia/gigi/pages/main/RevocationNotice.templ [new file with mode: 0644]
tests/club/wpia/gigi/pages/main/KeyCompromiseTestMessage.java [new file with mode: 0644]

index 321059b8d5fe226dedd50fd44ec54b45705e084a..ee0110f4fc4acbf4ca1a33379f6217cffc4ca3b3 100644 (file)
@@ -15,6 +15,8 @@ import java.security.interfaces.RSAPrivateKey;
 import java.security.spec.PKCS8EncodedKeySpec;
 import java.util.Arrays;
 import java.util.Base64;
 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;
 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.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.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.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.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 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
     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");
 
 
     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(16);
     public KeyCompromiseForm(HttpServletRequest hsr) {
         super(hsr);
         challenge = RandomToken.generateToken(16);
@@ -139,7 +150,50 @@ public class KeyCompromiseForm extends Form {
         } catch (UnsupportedEncodingException e) {
             throw new RuntimeException(e);
         }
         } 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 + "\n" + message;
+            }
+            if (message.contains("---")) {
+                throw new GigiApiException("Your message may not contain '---'.");
+            }
+            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<String, Object> 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."));
         }
         if ( !j.waitFor(60000)) {
             throw new PermamentFormException(new GigiApiException("Revocation timed out."));
         }
index 2c083bd44d01ad54914c9a80a098232badc4d96c..bacc8f77ee87a631c0ce4067b1fe07db31977a19 100644 (file)
@@ -38,6 +38,17 @@ echo -n "This private key has been compromised. Challenge: <span id="signChallen
       <input type="text" class="form-control" name="signature" placeholder="Signature">
     </td>
   </tr>
       <input type="text" class="form-control" name="signature" placeholder="Signature">
     </td>
   </tr>
+  <tr>
+    <td colspan="4">
+      <?=_You may provide information on how the private key was compromised to help the certificate owner prevent further key compromises.?>
+      <?=_You can indicate that this information should not be sent to the certificate owner, but only be visible to ${appName} staff, by checking the checkbox below.?>
+      <p>
+      <label for="confidential"><?=_Don't send the message to the certificate owner?></label>
+      <input type='checkbox' name='confidential' id='confidential'>
+      </p>
+      <textarea class="form-control" name="message" rows="3" cols="40"></textarea>
+    </td>
+  </tr>
   <tr>
     <td colspan="4"><input class="btn btn-primary" type="submit" name="process" value="<?=_Next?>"></td>
   </tr>
   <tr>
     <td colspan="4"><input class="btn btn-primary" type="submit" name="process" value="<?=_Next?>"></td>
   </tr>
index c36b176301027dfcbad94527fee8745011757cd0..695f725ee216dba74c20a67d04e28ad143650c99 100644 (file)
@@ -1,7 +1,6 @@
 package club.wpia.gigi.pages.main;
 
 import java.io.IOException;
 package club.wpia.gigi.pages.main;
 
 import java.io.IOException;
-import java.util.HashMap;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 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 {
 
     @Override
     public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
-        new KeyCompromiseForm(req).output(resp.getWriter(), getLanguage(req), new HashMap<String, Object>());
+        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 (file)
index 0000000..c204f11
--- /dev/null
@@ -0,0 +1,17 @@
+Subject: <?=_Certification Revocation Notice due to Key Compromise?>
+
+<?=_The private key for your certificate ${serial} was reported to be compromised. The certificate has therefore been revoked.?>
+
+<?=_The reporting user was able to sign a system-generated challenge and the signature verifies successfully with your public key.?>
+<?=_This means that they somehow have the ability to sign with your private key. This way your certificate could be used by someone else and thereby looses its value. This might have several causes:?>
+
+- <?=_Your private key became publicly accessible.?>
+- <?=_Your key pair was so weak that it could be broken by another individual.?>
+- <?=_Someone with authorized access to your private key (e.g. you) has initiated the revocation.?>
+
+<? if($message) { ?>
+The user supplied a message to help you resolve your security issue, to prevent further key compromises:
+---
+<?=$message?>
+---
+<? } ?>
\ No newline at end of file
diff --git a/tests/club/wpia/gigi/pages/main/KeyCompromiseTestMessage.java b/tests/club/wpia/gigi/pages/main/KeyCompromiseTestMessage.java
new file mode 100644 (file)
index 0000000..63a0108
--- /dev/null
@@ -0,0 +1,108 @@
+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 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;
+    }
+}