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 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
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);
} 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<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."));
}
<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>
package club.wpia.gigi.pages.main;
import java.io.IOException;
-import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@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));
}
}
--- /dev/null
+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 to impersonate the identity represented by the certificate. 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 reporting this compromised key provided the following message to aid you to resolve this security issue and help you to prevent further security incidents.
+---
+<?=$message?>
+---
+<? } ?>
\ No newline at end of file
@RunWith(Parameterized.class)
public class KeyCompromiseTest extends ClientTest {
+ private static final String NOT_FOUND = "not found";
+
private static class TestParameters {
private final String query;
}
public String getError() {
+ if (NOT_FOUND.equals(error)) {
+ return KeyCompromiseForm.NOT_FOUND.getRaw();
+ }
return error;
}
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"),
--- /dev/null
+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;
+ }
+}