1 package club.wpia.gigi.pages.main;
3 import java.io.ByteArrayInputStream;
4 import java.io.IOException;
5 import java.io.PrintWriter;
6 import java.io.UnsupportedEncodingException;
7 import java.security.GeneralSecurityException;
8 import java.security.KeyFactory;
9 import java.security.PrivateKey;
10 import java.security.Signature;
11 import java.security.cert.CertificateException;
12 import java.security.cert.CertificateFactory;
13 import java.security.cert.X509Certificate;
14 import java.security.interfaces.RSAPrivateKey;
15 import java.security.spec.PKCS8EncodedKeySpec;
16 import java.util.Arrays;
17 import java.util.Base64;
18 import java.util.HashMap;
19 import java.util.Locale;
22 import javax.servlet.http.HttpServletRequest;
24 import club.wpia.gigi.GigiApiException;
25 import club.wpia.gigi.dbObjects.Certificate;
26 import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
27 import club.wpia.gigi.dbObjects.CertificateOwner;
28 import club.wpia.gigi.dbObjects.Job;
29 import club.wpia.gigi.dbObjects.Organisation;
30 import club.wpia.gigi.dbObjects.User;
31 import club.wpia.gigi.localisation.Language;
32 import club.wpia.gigi.output.template.Form;
33 import club.wpia.gigi.output.template.MailTemplate;
34 import club.wpia.gigi.output.template.Template;
35 import club.wpia.gigi.output.template.TranslateCommand;
36 import club.wpia.gigi.util.PEM;
37 import club.wpia.gigi.util.RandomToken;
38 import club.wpia.gigi.util.RateLimit;
39 import club.wpia.gigi.util.RateLimit.RateLimitException;
40 import club.wpia.gigi.util.ServerConstants;
42 public class KeyCompromiseForm extends Form {
44 public static final String CONFIDENTIAL_MARKER = "*CONFIDENTIAL*";
46 private static final Template t = new Template(KeyCompromiseForm.class.getResource("KeyCompromiseForm.templ"));
49 public static final RateLimit RATE_LIMIT = new RateLimit(50, 5 * 60 * 1000);
51 private final String challenge;
53 public static final String CHALLENGE_PREFIX = "This private key has been compromised. Challenge: ";
55 public static final TranslateCommand NOT_LOADED = new TranslateCommand("Certificate could not be loaded");
57 public static final TranslateCommand NOT_FOUND = new TranslateCommand("Certificate to revoke not found");
59 private static final MailTemplate revocationNotice = new MailTemplate(KeyCompromiseForm.class.getResource("RevocationNotice.templ"));
61 public KeyCompromiseForm(HttpServletRequest hsr) {
63 challenge = RandomToken.generateToken(16);
67 public SubmissionResult submit(HttpServletRequest req) throws GigiApiException {
68 if (RATE_LIMIT.isLimitExceeded(req.getRemoteAddr())) {
69 throw new RateLimitException();
72 X509Certificate cert = null;
73 String serial = req.getParameter("serial");
74 String certData = req.getParameter("cert");
75 if (serial != null && !serial.isEmpty()) {
76 c = fetchCertificate(serial);
79 } catch (IOException e) {
80 throw new PermamentFormException(new GigiApiException(NOT_LOADED));
81 } catch (GeneralSecurityException e) {
82 throw new PermamentFormException(new GigiApiException(NOT_LOADED));
85 if (certData != null && !certData.isEmpty()) {
89 supplied = PEM.decode("CERTIFICATE", certData);
90 c0 = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(supplied));
91 } catch (IllegalArgumentException e1) {
92 throw new PermamentFormException(new GigiApiException("Your certificate could not be parsed"));
93 } catch (CertificateException e1) {
94 throw new PermamentFormException(new GigiApiException("Your certificate could not be parsed"));
97 String ser = c0.getSerialNumber().toString(16);
98 c = fetchCertificate(ser);
100 if ( !Arrays.equals(supplied, cert.getEncoded())) {
101 throw new PermamentFormException(new GigiApiException(NOT_FOUND));
103 } catch (IOException e) {
104 throw new PermamentFormException(new GigiApiException(NOT_LOADED));
105 } catch (GeneralSecurityException e) {
106 throw new PermamentFormException(new GigiApiException(NOT_LOADED));
110 throw new PermamentFormException(new GigiApiException("No certificate identification information provided"));
112 if (c.getStatus() == CertificateStatus.REVOKED) {
113 return new SuccessMessageResult(new TranslateCommand("Certificate already had been revoked"));
115 String inSig = req.getParameter("signature");
116 byte[] signature = null;
117 if (inSig != null && !inSig.isEmpty()) {
119 signature = Base64.getDecoder().decode(inSig);
120 } catch (IllegalArgumentException e) {
121 throw new PermamentFormException(new GigiApiException("Signature is malformed"));
124 String priv = req.getParameter("priv");
125 if (signature == null && priv != null && !priv.isEmpty()) {
127 PKCS8EncodedKeySpec k = new PKCS8EncodedKeySpec(PEM.decode("PRIVATE KEY", priv));
128 RSAPrivateKey pk = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(k);
129 signature = sign(pk, challenge);
130 } catch (IllegalArgumentException e) {
131 throw new PermamentFormException(new GigiApiException("Private Key is malformed"));
132 } catch (GeneralSecurityException e) {
133 throw new PermamentFormException(new GigiApiException("Private Key is malformed"));
136 if (signature == null) {
137 throw new PermamentFormException(new GigiApiException("No verification provided."));
141 Signature sig = Signature.getInstance("SHA256withRSA");
142 sig.initVerify(cert.getPublicKey());
143 sig.update(CHALLENGE_PREFIX.getBytes("UTF-8"));
144 sig.update(challenge.getBytes("UTF-8"));
145 if ( !sig.verify(signature)) {
146 throw new PermamentFormException(new GigiApiException("Verification does not match."));
148 } catch (GeneralSecurityException e) {
149 throw new PermamentFormException(new GigiApiException("Wasn't able to generate signature."));
150 } catch (UnsupportedEncodingException e) {
151 throw new RuntimeException(e);
153 String message = req.getParameter("message");
154 if (message != null && message.isEmpty()) {
157 if (message != null) {
158 if (message.startsWith(CONFIDENTIAL_MARKER)) {
159 message = " " + message;
161 String confidential = req.getParameter("confidential");
162 if (confidential != null && !confidential.isEmpty()) {
163 message = CONFIDENTIAL_MARKER + "\n" + message;
165 if (message.contains("---")) {
166 throw new GigiApiException("Your message may not contain '---'.");
168 if ( !message.matches("[ -~\r\n\t]*")) {
169 throw new GigiApiException("Your message may only contain printable ASCII characters, tab, newline and space.");
172 CertificateOwner co = c.getOwner();
174 Language l = Language.getInstance(Locale.ENGLISH);
175 if (co instanceof User) {
176 primaryEmail = ((User) co).getEmail();
177 l = Language.getInstance(((User) co).getPreferredLocale());
178 } else if (co instanceof Organisation) {
179 primaryEmail = ((Organisation) co).getContactEmail();
181 throw new IllegalArgumentException("certificate owner of unknown type");
183 HashMap<String, Object> vars = new HashMap<>();
184 vars.put("appName", ServerConstants.getAppName());
185 if (message != null && !message.startsWith(CONFIDENTIAL_MARKER)) {
186 vars.put("message", message);
188 vars.put("message", null);
190 vars.put("serial", c.getSerial());
192 revocationNotice.sendMail(l, vars, primaryEmail);
193 } catch (IOException e) {
194 throw new GigiApiException("Sending the notification mail failed.");
196 Job j = c.revoke(challenge, Base64.getEncoder().encodeToString(signature), message);
197 if ( !j.waitFor(60000)) {
198 throw new PermamentFormException(new GigiApiException("Revocation timed out."));
200 if (c.getStatus() != CertificateStatus.REVOKED) {
201 throw new PermamentFormException(new GigiApiException("Revocation failed."));
203 return new SuccessMessageResult(new TranslateCommand("Certificate is revoked."));
206 public static byte[] sign(PrivateKey pk, String challenge) throws GeneralSecurityException {
208 Signature sig = Signature.getInstance("SHA256withRSA");
211 sig.update(CHALLENGE_PREFIX.getBytes("UTF-8"));
212 sig.update(challenge.getBytes("UTF-8"));
213 } catch (UnsupportedEncodingException e) {
214 throw new RuntimeException(e);
216 signature = sig.sign();
220 private Certificate fetchCertificate(String serial) {
222 serial = serial.trim().toLowerCase();
224 while (idx < serial.length() && serial.charAt(idx) == '0') {
227 serial = serial.substring(idx);
228 c = Certificate.getBySerial(serial);
230 throw new PermamentFormException(new GigiApiException(NOT_FOUND));
236 protected void outputContent(PrintWriter out, Language l, Map<String, Object> vars) {
237 vars.put("challenge", challenge);
238 t.output(out, l, vars);