1 package club.wpia.gigi.pages.main;
3 import java.io.IOException;
4 import java.io.PrintWriter;
5 import java.io.UnsupportedEncodingException;
6 import java.security.GeneralSecurityException;
7 import java.security.KeyFactory;
8 import java.security.PrivateKey;
9 import java.security.Signature;
10 import java.security.cert.X509Certificate;
11 import java.security.interfaces.RSAPrivateKey;
12 import java.security.spec.PKCS8EncodedKeySpec;
13 import java.util.Base64;
14 import java.util.HashMap;
15 import java.util.Locale;
18 import javax.servlet.http.HttpServletRequest;
20 import club.wpia.gigi.GigiApiException;
21 import club.wpia.gigi.dbObjects.Certificate;
22 import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
23 import club.wpia.gigi.dbObjects.CertificateOwner;
24 import club.wpia.gigi.dbObjects.Job;
25 import club.wpia.gigi.dbObjects.Organisation;
26 import club.wpia.gigi.dbObjects.User;
27 import club.wpia.gigi.localisation.Language;
28 import club.wpia.gigi.output.template.Form;
29 import club.wpia.gigi.output.template.MailTemplate;
30 import club.wpia.gigi.output.template.Template;
31 import club.wpia.gigi.output.template.TranslateCommand;
32 import club.wpia.gigi.util.PEM;
33 import club.wpia.gigi.util.RandomToken;
34 import club.wpia.gigi.util.RateLimit;
35 import club.wpia.gigi.util.RateLimit.RateLimitException;
36 import club.wpia.gigi.util.ServerConstants;
38 public class KeyCompromiseForm extends Form {
40 public static final String CONFIDENTIAL_MARKER = "*CONFIDENTIAL*";
42 private static final Template t = new Template(KeyCompromiseForm.class.getResource("KeyCompromiseForm.templ"));
45 public static final RateLimit RATE_LIMIT = new RateLimit(50, 5 * 60 * 1000);
47 private final String challenge;
49 public static final String CHALLENGE_PREFIX = "This private key has been compromised. Challenge: ";
51 public static final TranslateCommand NOT_FOUND = new TranslateCommand("Certificate to revoke not found");
53 private static final MailTemplate revocationNotice = new MailTemplate(KeyCompromiseForm.class.getResource("RevocationNotice.templ"));
55 public KeyCompromiseForm(HttpServletRequest hsr) {
57 challenge = RandomToken.generateToken(32);
61 public SubmissionResult submit(HttpServletRequest req) throws GigiApiException {
62 if (RATE_LIMIT.isLimitExceeded(req.getRemoteAddr())) {
63 throw new RateLimitException();
67 c = Certificate.locateCertificate(req.getParameter("serial"), req.getParameter("cert"));
69 throw new GigiApiException(NOT_FOUND);
71 } catch (GigiApiException e) {
72 throw new PermamentFormException(e);
78 } catch (IOException | GeneralSecurityException e) {
79 throw new PermamentFormException(new GigiApiException(Certificate.NOT_LOADED));
82 if (c.getStatus() == CertificateStatus.REVOKED) {
83 return new SuccessMessageResult(new TranslateCommand("Certificate had already been revoked"));
85 String inSig = req.getParameter("signature");
86 byte[] signature = null;
87 if (inSig != null && !inSig.isEmpty()) {
89 signature = Base64.getDecoder().decode(inSig);
90 } catch (IllegalArgumentException e) {
91 throw new PermamentFormException(new GigiApiException("Signature is malformed"));
94 String priv = req.getParameter("priv");
95 if (signature == null && priv != null && !priv.isEmpty()) {
97 PKCS8EncodedKeySpec k = new PKCS8EncodedKeySpec(PEM.decode("PRIVATE KEY", priv));
98 RSAPrivateKey pk = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(k);
99 signature = sign(pk, challenge);
100 } catch (IllegalArgumentException e) {
101 throw new PermamentFormException(new GigiApiException("Private Key is malformed"));
102 } catch (GeneralSecurityException e) {
103 throw new PermamentFormException(new GigiApiException("Private Key is malformed"));
106 if (signature == null) {
107 throw new PermamentFormException(new GigiApiException("No verification provided."));
111 Signature sig = Signature.getInstance("SHA256withRSA");
112 sig.initVerify(cert.getPublicKey());
113 sig.update(CHALLENGE_PREFIX.getBytes("UTF-8"));
114 sig.update(challenge.getBytes("UTF-8"));
115 if ( !sig.verify(signature)) {
116 throw new PermamentFormException(new GigiApiException("Verification does not match."));
118 } catch (GeneralSecurityException e) {
119 throw new PermamentFormException(new GigiApiException("Wasn't able to generate signature."));
120 } catch (UnsupportedEncodingException e) {
121 throw new RuntimeException(e);
123 String message = req.getParameter("message");
124 if (message != null && message.isEmpty()) {
127 if (message != null) {
128 if (message.startsWith(CONFIDENTIAL_MARKER)) {
129 message = " " + message;
131 String confidential = req.getParameter("confidential");
132 if (confidential != null && !confidential.isEmpty()) {
133 message = CONFIDENTIAL_MARKER + "\r\n" + message;
135 if (message.contains("---")) {
136 throw new GigiApiException("Your message may not contain '---'.");
138 // convert all line endings to CRLF
139 message = message.replace("\r\n", "\n").replace("\r", "\n").replace("\n", "\r\n");
140 if ( !message.matches("[ -~\r\n\t]*")) {
141 throw new GigiApiException("Your message may only contain printable ASCII characters, tab, newline and space.");
144 CertificateOwner co = c.getOwner();
146 Language l = Language.getInstance(Locale.ENGLISH);
147 if (co instanceof User) {
148 primaryEmail = ((User) co).getEmail();
149 l = Language.getInstance(((User) co).getPreferredLocale());
150 } else if (co instanceof Organisation) {
151 primaryEmail = ((Organisation) co).getContactEmail();
153 throw new IllegalArgumentException("certificate owner of unknown type");
155 HashMap<String, Object> vars = new HashMap<>();
156 vars.put("appName", ServerConstants.getAppName());
157 if (message != null && !message.startsWith(CONFIDENTIAL_MARKER)) {
158 vars.put("message", message);
160 vars.put("message", null);
162 vars.put("serial", c.getSerial());
164 revocationNotice.sendMail(l, vars, primaryEmail);
165 } catch (IOException e) {
166 throw new GigiApiException("Sending the notification mail failed.");
168 Job j = c.revoke(challenge, Base64.getEncoder().encodeToString(signature), message);
169 if ( !j.waitFor(60000)) {
170 throw new PermamentFormException(new GigiApiException("Revocation timed out."));
172 if (c.getStatus() != CertificateStatus.REVOKED) {
173 throw new PermamentFormException(new GigiApiException("Revocation failed."));
175 return new SuccessMessageResult(new TranslateCommand("Certificate is revoked."));
178 public static byte[] sign(PrivateKey pk, String challenge) throws GeneralSecurityException {
180 Signature sig = Signature.getInstance("SHA256withRSA");
183 sig.update(CHALLENGE_PREFIX.getBytes("UTF-8"));
184 sig.update(challenge.getBytes("UTF-8"));
185 } catch (UnsupportedEncodingException e) {
186 throw new RuntimeException(e);
188 signature = sig.sign();
193 protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
194 vars.put("challenge", challenge);
195 vars.put("challengePrefix", CHALLENGE_PREFIX);
196 t.output(out, l, vars);