Merge "upd: remove 'browser install'"
[gigi.git] / tests / club / wpia / gigi / pages / account / TestCertificateAdd.java
1 package club.wpia.gigi.pages.account;
2
3 import static org.hamcrest.CoreMatchers.*;
4 import static org.hamcrest.MatcherAssert.assertThat;
5 import static org.junit.Assert.*;
6
7 import java.io.ByteArrayInputStream;
8 import java.io.IOException;
9 import java.io.InputStreamReader;
10 import java.io.OutputStream;
11 import java.io.UnsupportedEncodingException;
12 import java.net.HttpURLConnection;
13 import java.net.MalformedURLException;
14 import java.net.URL;
15 import java.net.URLConnection;
16 import java.net.URLEncoder;
17 import java.security.GeneralSecurityException;
18 import java.security.KeyPair;
19 import java.security.Signature;
20 import java.security.cert.Certificate;
21 import java.security.cert.CertificateException;
22 import java.security.cert.CertificateFactory;
23 import java.security.cert.X509Certificate;
24 import java.text.SimpleDateFormat;
25 import java.util.Arrays;
26 import java.util.Calendar;
27 import java.util.Date;
28 import java.util.TimeZone;
29 import java.util.Vector;
30 import java.util.regex.Matcher;
31 import java.util.regex.Pattern;
32
33 import org.junit.Test;
34
35 import club.wpia.gigi.dbObjects.CertificateOwner;
36 import club.wpia.gigi.dbObjects.Digest;
37 import club.wpia.gigi.pages.account.certs.CertificateAdd;
38 import club.wpia.gigi.pages.account.certs.CertificateRequest;
39 import club.wpia.gigi.pages.account.certs.Certificates;
40 import club.wpia.gigi.testUtils.ClientTest;
41 import club.wpia.gigi.testUtils.IOUtils;
42 import club.wpia.gigi.util.PEM;
43 import club.wpia.gigi.util.RandomToken;
44 import sun.security.pkcs.PKCS7;
45 import sun.security.pkcs.PKCS9Attribute;
46 import sun.security.pkcs10.PKCS10Attribute;
47 import sun.security.pkcs10.PKCS10Attributes;
48 import sun.security.util.ObjectIdentifier;
49 import sun.security.x509.CertificateExtensions;
50 import sun.security.x509.DNSName;
51 import sun.security.x509.ExtendedKeyUsageExtension;
52 import sun.security.x509.GeneralName;
53 import sun.security.x509.GeneralNameInterface;
54 import sun.security.x509.GeneralNames;
55 import sun.security.x509.RFC822Name;
56 import sun.security.x509.SubjectAlternativeNameExtension;
57
58 public class TestCertificateAdd extends ClientTest {
59
60     private static class OnPageError extends Error {
61
62         private static final long serialVersionUID = 1L;
63
64         public OnPageError(String page) {
65             super(page);
66         }
67     }
68
69     KeyPair kp = generateKeypair();
70
71     /**
72      * This KeyPair is used for testing the KeyCheck for proper rejection of
73      * invalid keys. The generated keys suffers from small factors.
74      */
75     KeyPair kpBroken = generateBrokenKeypair();
76
77     String csrf;
78
79     public TestCertificateAdd() throws GeneralSecurityException, IOException {
80         TestDomain.addDomain(cookie, uniq + ".tld");
81
82     }
83
84     @Test
85     public void testSimpleServer() throws IOException, GeneralSecurityException {
86         PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
87                 CertificateRequest.OID_KEY_USAGE_SSL_SERVER
88         }, new DNSName(uniq + ".tld"));
89
90         String pem = generatePEMCSR(kp, "CN=a." + uniq + ".tld", atts);
91         String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
92         assertArrayEquals(new String[] {
93                 "server", CertificateRequest.DEFAULT_CN, "dns:a." + uniq + ".tld\ndns:" + uniq + ".tld\n", Digest.SHA512.toString()
94         }, res);
95     }
96
97     @Test
98     public void testSimpleMail() throws IOException, GeneralSecurityException {
99         PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
100                 CertificateRequest.OID_KEY_USAGE_EMAIL_PROTECTION
101         }, new DNSName("a." + uniq + ".tld"), new DNSName("b." + uniq + ".tld"), new RFC822Name(email));
102
103         String pem = generatePEMCSR(kp, "CN=a b", atts, "SHA384WithRSA");
104
105         String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
106         assertArrayEquals(new String[] {
107                 "mail", "a b", "email:" + email + "\ndns:a." + uniq + ".tld\ndns:b." + uniq + ".tld\n", Digest.SHA384.toString()
108         }, res);
109     }
110
111     @Test
112     public void testSimpleClient() throws IOException, GeneralSecurityException {
113         PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
114                 CertificateRequest.OID_KEY_USAGE_SSL_CLIENT
115         }, new RFC822Name(email));
116
117         String pem = generatePEMCSR(kp, "CN=a b,email=" + email, atts, "SHA256WithRSA");
118
119         String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
120         assertArrayEquals(new String[] {
121                 "client", "a b", "email:" + email + "\n", Digest.SHA256.toString()
122         }, res);
123     }
124
125     @Test
126     public void testIssue() throws IOException, GeneralSecurityException {
127         HttpURLConnection huc = sendCertificateForm("description");
128         URLConnection uc = authenticate(new URL(huc.getHeaderField("Location") + ".crt"));
129         String crt = IOUtils.readURL(new InputStreamReader(uc.getInputStream(), "UTF-8"));
130
131         uc = authenticate(new URL(huc.getHeaderField("Location") + ".cer"));
132         byte[] cer = IOUtils.readURL(uc.getInputStream());
133         assertArrayEquals(cer, PEM.decode("CERTIFICATE", crt));
134
135         uc = authenticate(new URL(huc.getHeaderField("Location") + ".cer?chain"));
136         byte[] pkcs7 = IOUtils.readURL(uc.getInputStream());
137         PKCS7 p7 = new PKCS7(pkcs7);
138         byte[] sub = verifyChain(p7.getCertificates());
139         assertArrayEquals(cer, sub);
140         assertEquals("application/pkix-cert", uc.getHeaderField("Content-type"));
141
142         uc = authenticate(new URL(huc.getHeaderField("Location")));
143         String gui = IOUtils.readURL(uc);
144         Pattern p = Pattern.compile("-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----");
145         Matcher m = p.matcher(gui);
146         assertTrue(m.find());
147         byte[] cert = PEM.decode("CERTIFICATE", m.group(0));
148         Certificate c = CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(cert));
149         gui = c.toString();
150         assertThat(gui, containsString("clientAuth"));
151         assertThat(gui, containsString("CN=" + CertificateRequest.DEFAULT_CN));
152         assertThat(gui, containsString("SHA512withRSA"));
153         assertThat(gui, containsString("RFC822Name: " + email));
154     }
155
156     @Test
157     public void testIssueWithDescription() throws IOException, GeneralSecurityException {
158         String description = "Just a new comment." + RandomToken.generateToken(32);
159         HttpURLConnection huc = sendCertificateForm(description);
160         assertEquals(302, huc.getResponseCode());
161
162         URLConnection uc = get(Certificates.PATH);
163         assertThat(IOUtils.readURL(uc), containsString(description));
164
165         description = "Just a new comment." + RandomToken.generateToken(100);
166         huc = sendCertificateForm(description);
167         assertThat(fetchStartErrorMessage(IOUtils.readURL(huc)), containsString("Submitted description is longer than 100 characters."));
168     }
169
170     private HttpURLConnection sendCertificateForm(String description) throws IOException, GeneralSecurityException {
171         HttpURLConnection huc = openCertificateForm();
172         OutputStream out = huc.getOutputStream();
173         out.write(("csrf=" + URLEncoder.encode(csrf, "UTF-8")).getBytes("UTF-8"));
174         out.write(("&CN=" + URLEncoder.encode(CertificateRequest.DEFAULT_CN, "UTF-8") + "&profile=client&SANs=" + URLEncoder.encode("email:" + email + "\n", "UTF-8")).getBytes("UTF-8"));
175         out.write(("&hash_alg=SHA512").getBytes("UTF-8"));
176         out.write(("&description=" + URLEncoder.encode(description, "UTF-8")).getBytes("UTF-8"));
177         return huc;
178     }
179
180     private HttpURLConnection openCertificateForm() throws IOException, GeneralSecurityException, UnsupportedEncodingException {
181         PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
182                 CertificateRequest.OID_KEY_USAGE_SSL_CLIENT
183         }, new RFC822Name(email));
184
185         String pem = generatePEMCSR(kp, "CN=a b,email=" + email, atts, "SHA512WithRSA");
186
187         String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
188         assertArrayEquals(new String[] {
189                 "client", "a b", "email:" + email + "\n", Digest.SHA512.toString()
190         }, res);
191
192         HttpURLConnection huc = (HttpURLConnection) ncert.openConnection();
193         huc.setRequestProperty("Cookie", cookie);
194         huc.setDoOutput(true);
195         return huc;
196     }
197
198     private byte[] verifyChain(X509Certificate[] x509Certificates) throws GeneralSecurityException {
199         X509Certificate current = null;
200         nextCert:
201         while (true) {
202             for (int i = 0; i < x509Certificates.length; i++) {
203                 X509Certificate cert = x509Certificates[i];
204                 if (current == null) {
205                     if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
206                         current = cert;
207                         continue nextCert;
208                     }
209                 } else {
210                     if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
211                         continue;
212                     }
213                     if (current.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
214                         Signature s = Signature.getInstance(cert.getSigAlgName());
215                         s.initVerify(current.getPublicKey());
216                         s.update(cert.getTBSCertificate());
217                         assertTrue(s.verify(cert.getSignature()));
218                         current = cert;
219                         continue nextCert;
220                     }
221                 }
222             }
223             assertNotNull(current);
224             return current.getEncoded();
225         }
226     }
227
228     @Test
229     public void testValidityPeriodCalendar() throws IOException, GeneralSecurityException {
230         testCertificateValidityRelative(Calendar.YEAR, 2, "2y", true);
231         testCertificateValidityRelative(Calendar.YEAR, 1, "1y", true);
232         testCertificateValidityRelative(Calendar.MONTH, 3, "3m", true);
233         testCertificateValidityRelative(Calendar.MONTH, 7, "7m", true);
234         testCertificateValidityRelative(Calendar.MONTH, 13, "13m", true);
235
236         testCertificateValidityRelative(Calendar.MONTH, 13, "-1m", false);
237     }
238
239     @Test
240     public void testValidityPeriodWhishStart() throws IOException, GeneralSecurityException {
241         long now = System.currentTimeMillis();
242         final long MS_PER_DAY = 24 * 60 * 60 * 1000;
243         now -= now % MS_PER_DAY;
244         now += MS_PER_DAY;
245         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
246         sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
247
248         Date start = new Date(now);
249         Date end = new Date(now + MS_PER_DAY * 10);
250         String validity = "&validFrom=" + sdf.format(start) + "&validity=" + sdf.format(end);
251         X509Certificate res = createCertWithValidity(validity, false);
252         assertNotNull(validity, res);
253         assertEquals(start, res.getNotBefore());
254         assertEquals(end, res.getNotAfter());
255     }
256
257     private void testCertificateValidityRelative(int field, int amount, String length, boolean shouldsucceed) throws IOException, GeneralSecurityException, UnsupportedEncodingException, MalformedURLException, CertificateException {
258         X509Certificate parsed = createCertWithValidity("&validFrom=now&validity=" + length, false);
259         if (parsed == null) {
260             assertTrue( !shouldsucceed);
261             return;
262         } else {
263             assertTrue(shouldsucceed);
264         }
265
266         long now = System.currentTimeMillis();
267         Date start = parsed.getNotBefore();
268         Date end = parsed.getNotAfter();
269         Calendar c = Calendar.getInstance();
270         c.setTimeZone(TimeZone.getTimeZone("UTC"));
271         c.setTime(start);
272         c.add(field, amount);
273         assertTrue(Math.abs(start.getTime() - now) < 10000);
274         assertEquals(c.getTime(), end);
275     }
276
277     private X509Certificate createCertWithValidity(String validity, boolean login) throws IOException, GeneralSecurityException, UnsupportedEncodingException, MalformedURLException, CertificateException {
278         HttpURLConnection huc = openCertificateForm();
279         OutputStream out = huc.getOutputStream();
280         out.write(("csrf=" + URLEncoder.encode(csrf, "UTF-8")).getBytes("UTF-8"));
281         out.write(("&profile=client&CN=" + CertificateRequest.DEFAULT_CN + "&SANs=" + URLEncoder.encode("email:" + email + "\n", "UTF-8")).getBytes("UTF-8"));
282         out.write(("&hash_alg=SHA512&").getBytes("UTF-8"));
283         if (login) {
284             out.write(("login=1&").getBytes("UTF-8"));
285         }
286         out.write(validity.getBytes("UTF-8"));
287
288         String certurl = huc.getHeaderField("Location");
289         if (certurl == null) {
290             return null;
291         }
292         URLConnection uc = authenticate(new URL(certurl + ".crt"));
293         String crt = IOUtils.readURL(new InputStreamReader(uc.getInputStream(), "UTF-8"));
294
295         CertificateFactory cf = CertificateFactory.getInstance("X.509");
296         X509Certificate parsed = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(crt.getBytes("UTF-8")));
297         return parsed;
298     }
299
300     private URLConnection authenticate(URL url) throws IOException {
301         URLConnection uc = url.openConnection();
302         uc.setRequestProperty("Cookie", cookie);
303         return uc;
304     }
305
306     private PKCS10Attributes buildAtts(ObjectIdentifier[] ekuOIDs, GeneralNameInterface... SANs) throws IOException {
307         CertificateExtensions attributeValue = new CertificateExtensions();
308         GeneralNames names = new GeneralNames();
309
310         for (GeneralNameInterface name : SANs) {
311             names.add(new GeneralName(name));
312         }
313         attributeValue.set("SANs", new SubjectAlternativeNameExtension(names));
314         PKCS10Attributes atts = new PKCS10Attributes(new PKCS10Attribute[] {
315                 new PKCS10Attribute(PKCS9Attribute.EXTENSION_REQUEST_OID, attributeValue)
316         });
317         ExtendedKeyUsageExtension eku = new ExtendedKeyUsageExtension(//
318                 new Vector<>(Arrays.<ObjectIdentifier>asList(ekuOIDs)));
319         attributeValue.set("eku", eku);
320         return atts;
321     }
322
323     private final URL ncert = new URL("https://" + getServerName() + CertificateAdd.PATH);
324
325     private String[] fillOutForm(String pem) throws IOException {
326         HttpURLConnection uc = (HttpURLConnection) ncert.openConnection();
327         uc.setRequestProperty("Cookie", cookie);
328         csrf = getCSRF(uc);
329         return fillOutFormDirect(pem);
330
331     }
332
333     private String[] fillOutFormDirect(String pem) throws IOException {
334
335         HttpURLConnection uc = (HttpURLConnection) ncert.openConnection();
336         uc.setRequestProperty("Cookie", cookie);
337         uc.setDoOutput(true);
338         uc.getOutputStream().write(("csrf=" + URLEncoder.encode(csrf, "UTF-8") + "&" + pem).getBytes("UTF-8"));
339         uc.getOutputStream().flush();
340
341         return extractFormData(uc);
342     }
343
344     private String[] extractFormData(HttpURLConnection uc) throws IOException, Error {
345         String result = IOUtils.readURL(uc);
346         if (hasError().matches(result)) {
347             throw new OnPageError(result);
348         }
349
350         String profileKey = extractPattern(result, Pattern.compile("<option value=\"([^\"]*)\" selected>"));
351         String resultingCN = extractPattern(result, Pattern.compile("<input [^>]*name='CN' [^>]*value='([^']*)'/>"));
352         String txt = extractPattern(result, Pattern.compile("<textarea [^>]*name='SANs' [^>]*>([^<]*)</textarea>"));
353         String md = extractPattern(result, Pattern.compile("<input type=\"radio\" [^>]*name=\"hash_alg\" value=\"([^\"]*)\" checked='checked'/>"));
354         return new String[] {
355                 profileKey, resultingCN, txt, md
356         };
357     }
358
359     private String extractPattern(String result, Pattern p) {
360         Matcher m = p.matcher(result);
361         assertTrue(m.find());
362         String resultingCN = m.group(1);
363         return resultingCN;
364     }
365
366     @Test
367     public void testSetLoginEnabled() throws IOException, GeneralSecurityException {
368         X509Certificate parsedLoginNotEnabled = createCertWithValidity("&validFrom=now&validity=1m", false);
369         assertNull(CertificateOwner.getByEnabledSerial(parsedLoginNotEnabled.getSerialNumber()));
370
371         X509Certificate parsedLoginEnabled = createCertWithValidity("&validFrom=now&validity=1m", true);
372         assertEquals(u, CertificateOwner.getByEnabledSerial(parsedLoginEnabled.getSerialNumber()));
373     }
374
375     @Test
376     public void testInvalidKeyInCSR() throws IOException, GeneralSecurityException {
377         PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
378                 CertificateRequest.OID_KEY_USAGE_SSL_SERVER
379         }, new DNSName(uniq + ".tld"));
380
381         String pem = generatePEMCSR(kpBroken, "CN=a." + uniq + ".tld", atts);
382
383         HttpURLConnection huc = post(CertificateAdd.PATH, "CSR=" + URLEncoder.encode(pem, "UTF-8"));
384         assertThat(IOUtils.readURL(huc), hasError());
385     }
386
387 }