1 package club.wpia.gigi.pages.account;
3 import static org.hamcrest.CoreMatchers.*;
4 import static org.hamcrest.MatcherAssert.assertThat;
5 import static org.junit.Assert.*;
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;
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.Base64;
27 import java.util.Calendar;
28 import java.util.Date;
29 import java.util.TimeZone;
30 import java.util.Vector;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
34 import org.junit.Test;
36 import club.wpia.gigi.crypto.SPKAC;
37 import club.wpia.gigi.dbObjects.CertificateOwner;
38 import club.wpia.gigi.dbObjects.Digest;
39 import club.wpia.gigi.pages.account.certs.CertificateAdd;
40 import club.wpia.gigi.pages.account.certs.CertificateRequest;
41 import club.wpia.gigi.pages.account.certs.Certificates;
42 import club.wpia.gigi.testUtils.ClientTest;
43 import club.wpia.gigi.testUtils.IOUtils;
44 import club.wpia.gigi.util.PEM;
45 import club.wpia.gigi.util.RandomToken;
46 import sun.security.pkcs.PKCS7;
47 import sun.security.pkcs.PKCS9Attribute;
48 import sun.security.pkcs10.PKCS10Attribute;
49 import sun.security.pkcs10.PKCS10Attributes;
50 import sun.security.util.ObjectIdentifier;
51 import sun.security.x509.CertificateExtensions;
52 import sun.security.x509.DNSName;
53 import sun.security.x509.ExtendedKeyUsageExtension;
54 import sun.security.x509.GeneralName;
55 import sun.security.x509.GeneralNameInterface;
56 import sun.security.x509.GeneralNames;
57 import sun.security.x509.RFC822Name;
58 import sun.security.x509.SubjectAlternativeNameExtension;
59 import sun.security.x509.X509Key;
61 public class TestCertificateAdd extends ClientTest {
63 private static class OnPageError extends Error {
65 private static final long serialVersionUID = 1L;
67 public OnPageError(String page) {
72 KeyPair kp = generateKeypair();
75 * This KeyPair is used for testing the KeyCheck for proper rejection of
76 * invalid keys. The generated keys suffers from small factors.
78 KeyPair kpBroken = generateBrokenKeypair();
82 public TestCertificateAdd() throws GeneralSecurityException, IOException {
83 TestDomain.addDomain(cookie, uniq + ".tld");
88 public void testSimpleServer() throws IOException, GeneralSecurityException {
89 PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
90 CertificateRequest.OID_KEY_USAGE_SSL_SERVER
91 }, new DNSName(uniq + ".tld"));
93 String pem = generatePEMCSR(kp, "CN=a." + uniq + ".tld", atts);
94 String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
95 assertArrayEquals(new String[] {
96 "server", CertificateRequest.DEFAULT_CN, "dns:a." + uniq + ".tld\ndns:" + uniq + ".tld\n", Digest.SHA512.toString()
101 public void testSimpleMail() throws IOException, GeneralSecurityException {
102 PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
103 CertificateRequest.OID_KEY_USAGE_EMAIL_PROTECTION
104 }, new DNSName("a." + uniq + ".tld"), new DNSName("b." + uniq + ".tld"), new RFC822Name(email));
106 String pem = generatePEMCSR(kp, "CN=a b", atts, "SHA384WithRSA");
108 String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
109 assertArrayEquals(new String[] {
110 "mail", "a b", "email:" + email + "\ndns:a." + uniq + ".tld\ndns:b." + uniq + ".tld\n", Digest.SHA384.toString()
115 public void testSimpleClient() throws IOException, GeneralSecurityException {
116 PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
117 CertificateRequest.OID_KEY_USAGE_SSL_CLIENT
118 }, new RFC822Name(email));
120 String pem = generatePEMCSR(kp, "CN=a b,email=" + email, atts, "SHA256WithRSA");
122 String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
123 assertArrayEquals(new String[] {
124 "client", "a b", "email:" + email + "\n", Digest.SHA256.toString()
129 public void testSPKAC() throws GeneralSecurityException, IOException {
135 public void testIssue() throws IOException, GeneralSecurityException {
136 HttpURLConnection huc = sendCertificateForm("description");
137 URLConnection uc = authenticate(new URL(huc.getHeaderField("Location") + ".crt"));
138 String crt = IOUtils.readURL(new InputStreamReader(uc.getInputStream(), "UTF-8"));
140 uc = authenticate(new URL(huc.getHeaderField("Location") + ".cer"));
141 byte[] cer = IOUtils.readURL(uc.getInputStream());
142 assertArrayEquals(cer, PEM.decode("CERTIFICATE", crt));
144 uc = authenticate(new URL(huc.getHeaderField("Location") + ".cer?install&chain"));
145 byte[] pkcs7 = IOUtils.readURL(uc.getInputStream());
146 PKCS7 p7 = new PKCS7(pkcs7);
147 byte[] sub = verifyChain(p7.getCertificates());
148 assertArrayEquals(cer, sub);
149 assertEquals("application/x-x509-user-cert", uc.getHeaderField("Content-type"));
151 uc = authenticate(new URL(huc.getHeaderField("Location")));
152 String gui = IOUtils.readURL(uc);
153 Pattern p = Pattern.compile("-----BEGIN CERTIFICATE-----[^-]+-----END CERTIFICATE-----");
154 Matcher m = p.matcher(gui);
155 assertTrue(m.find());
156 byte[] cert = PEM.decode("CERTIFICATE", m.group(0));
157 Certificate c = CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(cert));
159 assertThat(gui, containsString("clientAuth"));
160 assertThat(gui, containsString("CN=" + CertificateRequest.DEFAULT_CN));
161 assertThat(gui, containsString("SHA512withRSA"));
162 assertThat(gui, containsString("RFC822Name: " + email));
166 public void testIssueWithDescription() throws IOException, GeneralSecurityException {
167 String description = "Just a new comment." + RandomToken.generateToken(32);
168 HttpURLConnection huc = sendCertificateForm(description);
169 assertEquals(302, huc.getResponseCode());
171 URLConnection uc = get(Certificates.PATH);
172 assertThat(IOUtils.readURL(uc), containsString(description));
174 description = "Just a new comment." + RandomToken.generateToken(100);
175 huc = sendCertificateForm(description);
176 assertThat(fetchStartErrorMessage(IOUtils.readURL(huc)), containsString("Submitted description is longer than 100 characters."));
179 private HttpURLConnection sendCertificateForm(String description) throws IOException, GeneralSecurityException {
180 HttpURLConnection huc = openCertificateForm();
181 OutputStream out = huc.getOutputStream();
182 out.write(("csrf=" + URLEncoder.encode(csrf, "UTF-8")).getBytes("UTF-8"));
183 out.write(("&CN=" + URLEncoder.encode(CertificateRequest.DEFAULT_CN, "UTF-8") + "&profile=client&SANs=" + URLEncoder.encode("email:" + email + "\n", "UTF-8")).getBytes("UTF-8"));
184 out.write(("&hash_alg=SHA512").getBytes("UTF-8"));
185 out.write(("&description=" + URLEncoder.encode(description, "UTF-8")).getBytes("UTF-8"));
189 private HttpURLConnection openCertificateForm() throws IOException, GeneralSecurityException, UnsupportedEncodingException {
190 PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
191 CertificateRequest.OID_KEY_USAGE_SSL_CLIENT
192 }, new RFC822Name(email));
194 String pem = generatePEMCSR(kp, "CN=a b,email=" + email, atts, "SHA512WithRSA");
196 String[] res = fillOutForm("CSR=" + URLEncoder.encode(pem, "UTF-8"));
197 assertArrayEquals(new String[] {
198 "client", "a b", "email:" + email + "\n", Digest.SHA512.toString()
201 HttpURLConnection huc = (HttpURLConnection) ncert.openConnection();
202 huc.setRequestProperty("Cookie", cookie);
203 huc.setDoOutput(true);
207 private byte[] verifyChain(X509Certificate[] x509Certificates) throws GeneralSecurityException {
208 X509Certificate current = null;
211 for (int i = 0; i < x509Certificates.length; i++) {
212 X509Certificate cert = x509Certificates[i];
213 if (current == null) {
214 if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
219 if (cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
222 if (current.getSubjectX500Principal().equals(cert.getIssuerX500Principal())) {
223 Signature s = Signature.getInstance(cert.getSigAlgName());
224 s.initVerify(current.getPublicKey());
225 s.update(cert.getTBSCertificate());
226 assertTrue(s.verify(cert.getSignature()));
232 assertNotNull(current);
233 return current.getEncoded();
238 public void testValidityPeriodCalendar() throws IOException, GeneralSecurityException {
239 testCertificateValidityRelative(Calendar.YEAR, 2, "2y", true);
240 testCertificateValidityRelative(Calendar.YEAR, 1, "1y", true);
241 testCertificateValidityRelative(Calendar.MONTH, 3, "3m", true);
242 testCertificateValidityRelative(Calendar.MONTH, 7, "7m", true);
243 testCertificateValidityRelative(Calendar.MONTH, 13, "13m", true);
245 testCertificateValidityRelative(Calendar.MONTH, 13, "-1m", false);
249 public void testValidityPeriodWhishStart() throws IOException, GeneralSecurityException {
250 long now = System.currentTimeMillis();
251 final long MS_PER_DAY = 24 * 60 * 60 * 1000;
252 now -= now % MS_PER_DAY;
254 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
255 sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
257 Date start = new Date(now);
258 Date end = new Date(now + MS_PER_DAY * 10);
259 String validity = "&validFrom=" + sdf.format(start) + "&validity=" + sdf.format(end);
260 X509Certificate res = createCertWithValidity(validity, false);
261 assertNotNull(validity, res);
262 assertEquals(start, res.getNotBefore());
263 assertEquals(end, res.getNotAfter());
266 private void testCertificateValidityRelative(int field, int amount, String length, boolean shouldsucceed) throws IOException, GeneralSecurityException, UnsupportedEncodingException, MalformedURLException, CertificateException {
267 X509Certificate parsed = createCertWithValidity("&validFrom=now&validity=" + length, false);
268 if (parsed == null) {
269 assertTrue( !shouldsucceed);
272 assertTrue(shouldsucceed);
275 long now = System.currentTimeMillis();
276 Date start = parsed.getNotBefore();
277 Date end = parsed.getNotAfter();
278 Calendar c = Calendar.getInstance();
279 c.setTimeZone(TimeZone.getTimeZone("UTC"));
281 c.add(field, amount);
282 assertTrue(Math.abs(start.getTime() - now) < 10000);
283 assertEquals(c.getTime(), end);
286 private X509Certificate createCertWithValidity(String validity, boolean login) throws IOException, GeneralSecurityException, UnsupportedEncodingException, MalformedURLException, CertificateException {
287 HttpURLConnection huc = openCertificateForm();
288 OutputStream out = huc.getOutputStream();
289 out.write(("csrf=" + URLEncoder.encode(csrf, "UTF-8")).getBytes("UTF-8"));
290 out.write(("&profile=client&CN=" + CertificateRequest.DEFAULT_CN + "&SANs=" + URLEncoder.encode("email:" + email + "\n", "UTF-8")).getBytes("UTF-8"));
291 out.write(("&hash_alg=SHA512&").getBytes("UTF-8"));
293 out.write(("login=1&").getBytes("UTF-8"));
295 out.write(validity.getBytes("UTF-8"));
297 String certurl = huc.getHeaderField("Location");
298 if (certurl == null) {
301 URLConnection uc = authenticate(new URL(certurl + ".crt"));
302 String crt = IOUtils.readURL(new InputStreamReader(uc.getInputStream(), "UTF-8"));
304 CertificateFactory cf = CertificateFactory.getInstance("X.509");
305 X509Certificate parsed = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(crt.getBytes("UTF-8")));
309 private URLConnection authenticate(URL url) throws IOException {
310 URLConnection uc = url.openConnection();
311 uc.setRequestProperty("Cookie", cookie);
315 protected String testSPKAC(boolean correctChallenge) throws GeneralSecurityException, IOException {
316 HttpURLConnection uc = (HttpURLConnection) ncert.openConnection();
317 uc.setRequestProperty("Cookie", cookie);
318 String s = IOUtils.readURL(uc);
320 csrf = extractPattern(s, Pattern.compile("<input [^>]*name='csrf' [^>]*value='([^']*)'>"));
321 String challenge = extractPattern(s, Pattern.compile("<keygen [^>]*name=\"SPKAC\" [^>]*challenge=\"([^\"]*)\"/>"));
323 SPKAC spk = new SPKAC((X509Key) kp.getPublic(), challenge + (correctChallenge ? "" : "b"));
324 Signature sign = Signature.getInstance("SHA512WithRSA");
325 sign.initSign(kp.getPrivate());
327 String[] res = fillOutFormDirect("SPKAC=" + URLEncoder.encode(Base64.getEncoder().encodeToString(spk.getEncoded(sign)), "UTF-8"));
328 if ( !correctChallenge) {
329 fail("Should not succeed with wrong challenge.");
331 assertArrayEquals(new String[] {
332 "client", CertificateRequest.DEFAULT_CN, "", Digest.SHA512.toString()
334 } catch (OnPageError e) {
335 String error = fetchStartErrorMessage(e.getMessage());
336 assertTrue(error, error.startsWith("<p>Challenge mismatch"));
341 private PKCS10Attributes buildAtts(ObjectIdentifier[] ekuOIDs, GeneralNameInterface... SANs) throws IOException {
342 CertificateExtensions attributeValue = new CertificateExtensions();
343 GeneralNames names = new GeneralNames();
345 for (GeneralNameInterface name : SANs) {
346 names.add(new GeneralName(name));
348 attributeValue.set("SANs", new SubjectAlternativeNameExtension(names));
349 PKCS10Attributes atts = new PKCS10Attributes(new PKCS10Attribute[] {
350 new PKCS10Attribute(PKCS9Attribute.EXTENSION_REQUEST_OID, attributeValue)
352 ExtendedKeyUsageExtension eku = new ExtendedKeyUsageExtension(//
353 new Vector<>(Arrays.<ObjectIdentifier>asList(ekuOIDs)));
354 attributeValue.set("eku", eku);
358 private final URL ncert = new URL("https://" + getServerName() + CertificateAdd.PATH);
360 private String[] fillOutForm(String pem) throws IOException {
361 HttpURLConnection uc = (HttpURLConnection) ncert.openConnection();
362 uc.setRequestProperty("Cookie", cookie);
364 return fillOutFormDirect(pem);
368 private String[] fillOutFormDirect(String pem) throws IOException {
370 HttpURLConnection uc = (HttpURLConnection) ncert.openConnection();
371 uc.setRequestProperty("Cookie", cookie);
372 uc.setDoOutput(true);
373 uc.getOutputStream().write(("csrf=" + URLEncoder.encode(csrf, "UTF-8") + "&" + pem).getBytes("UTF-8"));
374 uc.getOutputStream().flush();
376 return extractFormData(uc);
379 private String[] extractFormData(HttpURLConnection uc) throws IOException, Error {
380 String result = IOUtils.readURL(uc);
381 if (hasError().matches(result)) {
382 throw new OnPageError(result);
385 String profileKey = extractPattern(result, Pattern.compile("<option value=\"([^\"]*)\" selected>"));
386 String resultingCN = extractPattern(result, Pattern.compile("<input [^>]*name='CN' [^>]*value='([^']*)'/>"));
387 String txt = extractPattern(result, Pattern.compile("<textarea [^>]*name='SANs' [^>]*>([^<]*)</textarea>"));
388 String md = extractPattern(result, Pattern.compile("<input type=\"radio\" [^>]*name=\"hash_alg\" value=\"([^\"]*)\" checked='checked'/>"));
389 return new String[] {
390 profileKey, resultingCN, txt, md
394 private String extractPattern(String result, Pattern p) {
395 Matcher m = p.matcher(result);
396 assertTrue(m.find());
397 String resultingCN = m.group(1);
402 public void testSetLoginEnabled() throws IOException, GeneralSecurityException {
403 X509Certificate parsedLoginNotEnabled = createCertWithValidity("&validFrom=now&validity=1m", false);
404 assertNull(CertificateOwner.getByEnabledSerial(parsedLoginNotEnabled.getSerialNumber()));
406 X509Certificate parsedLoginEnabled = createCertWithValidity("&validFrom=now&validity=1m", true);
407 assertEquals(u, CertificateOwner.getByEnabledSerial(parsedLoginEnabled.getSerialNumber()));
411 public void testInvalidKeyInCSR() throws IOException, GeneralSecurityException {
412 PKCS10Attributes atts = buildAtts(new ObjectIdentifier[] {
413 CertificateRequest.OID_KEY_USAGE_SSL_SERVER
414 }, new DNSName(uniq + ".tld"));
416 String pem = generatePEMCSR(kpBroken, "CN=a." + uniq + ".tld", atts);
418 HttpURLConnection huc = post(CertificateAdd.PATH, "CSR=" + URLEncoder.encode(pem, "UTF-8"));
419 assertThat(IOUtils.readURL(huc), hasError());