]> WPIA git - gigi.git/blob - src/club/wpia/gigi/pages/main/KeyCompromiseForm.java
Merge "upd: remove 'browser install'"
[gigi.git] / src / club / wpia / gigi / pages / main / KeyCompromiseForm.java
1 package club.wpia.gigi.pages.main;
2
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;
16 import java.util.Map;
17
18 import javax.servlet.http.HttpServletRequest;
19
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;
37
38 public class KeyCompromiseForm extends Form {
39
40     public static final String CONFIDENTIAL_MARKER = "*CONFIDENTIAL*";
41
42     private static final Template t = new Template(KeyCompromiseForm.class.getResource("KeyCompromiseForm.templ"));
43
44     // 50 per 5 min
45     public static final RateLimit RATE_LIMIT = new RateLimit(50, 5 * 60 * 1000);
46
47     private final String challenge;
48
49     public static final String CHALLENGE_PREFIX = "This private key has been compromised. Challenge: ";
50
51     public static final TranslateCommand NOT_FOUND = new TranslateCommand("Certificate to revoke not found");
52
53     private static final MailTemplate revocationNotice = new MailTemplate(KeyCompromiseForm.class.getResource("RevocationNotice.templ"));
54
55     public KeyCompromiseForm(HttpServletRequest hsr) {
56         super(hsr);
57         challenge = RandomToken.generateToken(32);
58     }
59
60     @Override
61     public SubmissionResult submit(HttpServletRequest req) throws GigiApiException {
62         if (RATE_LIMIT.isLimitExceeded(req.getRemoteAddr())) {
63             throw new RateLimitException();
64         }
65         Certificate c;
66         try {
67             c = Certificate.locateCertificate(req.getParameter("serial"), req.getParameter("cert"));
68             if (c == null) {
69                 throw new GigiApiException(NOT_FOUND);
70             }
71         } catch (GigiApiException e) {
72             throw new PermamentFormException(e);
73         }
74
75         X509Certificate cert;
76         try {
77             cert = c.cert();
78         } catch (IOException | GeneralSecurityException e) {
79             throw new PermamentFormException(new GigiApiException(Certificate.NOT_LOADED));
80         }
81
82         if (c.getStatus() == CertificateStatus.REVOKED) {
83             return new SuccessMessageResult(new TranslateCommand("Certificate had already been revoked"));
84         }
85         String inSig = req.getParameter("signature");
86         byte[] signature = null;
87         if (inSig != null && !inSig.isEmpty()) {
88             try {
89                 signature = Base64.getDecoder().decode(inSig);
90             } catch (IllegalArgumentException e) {
91                 throw new PermamentFormException(new GigiApiException("Signature is malformed"));
92             }
93         }
94         String priv = req.getParameter("priv");
95         if (signature == null && priv != null && !priv.isEmpty()) {
96             try {
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"));
104             }
105         }
106         if (signature == null) {
107             throw new PermamentFormException(new GigiApiException("No verification provided."));
108         }
109
110         try {
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."));
117             }
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);
122         }
123         String message = req.getParameter("message");
124         if (message != null && message.isEmpty()) {
125             message = null;
126         }
127         if (message != null) {
128             if (message.startsWith(CONFIDENTIAL_MARKER)) {
129                 message = " " + message;
130             }
131             String confidential = req.getParameter("confidential");
132             if (confidential != null && !confidential.isEmpty()) {
133                 message = CONFIDENTIAL_MARKER + "\r\n" + message;
134             }
135             if (message.contains("---")) {
136                 throw new GigiApiException("Your message may not contain '---'.");
137             }
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.");
142             }
143         }
144         CertificateOwner co = c.getOwner();
145         String primaryEmail;
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();
152         } else {
153             throw new IllegalArgumentException("certificate owner of unknown type");
154         }
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);
159         } else {
160             vars.put("message", null);
161         }
162         vars.put("serial", c.getSerial());
163         try {
164             revocationNotice.sendMail(l, vars, primaryEmail);
165         } catch (IOException e) {
166             throw new GigiApiException("Sending the notification mail failed.");
167         }
168         Job j = c.revoke(challenge, Base64.getEncoder().encodeToString(signature), message);
169         if ( !j.waitFor(Job.WAIT_MIN)) {
170             throw new PermamentFormException(new GigiApiException("Revocation timed out."));
171         }
172         if (c.getStatus() != CertificateStatus.REVOKED) {
173             throw new PermamentFormException(new GigiApiException("Revocation failed."));
174         }
175         return new SuccessMessageResult(new TranslateCommand("Certificate is revoked."));
176     }
177
178     public static byte[] sign(PrivateKey pk, String challenge) throws GeneralSecurityException {
179         byte[] signature;
180         Signature sig = Signature.getInstance("SHA256withRSA");
181         sig.initSign(pk);
182         try {
183             sig.update(CHALLENGE_PREFIX.getBytes("UTF-8"));
184             sig.update(challenge.getBytes("UTF-8"));
185         } catch (UnsupportedEncodingException e) {
186             throw new RuntimeException(e);
187         }
188         signature = sig.sign();
189         return signature;
190     }
191
192     @Override
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);
197     }
198
199 }