From 99ef9ee7f8d4a2332e4f08c7a0b23cc84966f555 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Felix=20D=C3=B6rre?= Date: Fri, 29 Aug 2014 16:59:22 +0200 Subject: [PATCH] [test-config] FIX: the ssl-pinger+ add various tests for that. --- config/test.properties.template | 1 + src/org/cacert/gigi/Gigi.java | 8 +- src/org/cacert/gigi/Launcher.java | 8 +- src/org/cacert/gigi/ping/PingerDaemon.java | 21 +- src/org/cacert/gigi/ping/SSLPinger.java | 41 +++- tests/org/cacert/gigi/ping/TestSSL.java | 224 ++++++++++++++++++ .../cacert/gigi/testUtils/ManagedTest.java | 25 +- tests/org/cacert/gigi/testUtils/PingTest.java | 3 +- 8 files changed, 300 insertions(+), 31 deletions(-) create mode 100644 tests/org/cacert/gigi/ping/TestSSL.java diff --git a/config/test.properties.template b/config/test.properties.template index 19e2645b..46c1d1d2 100644 --- a/config/test.properties.template +++ b/config/test.properties.template @@ -28,3 +28,4 @@ domain.manage=http://you-installation-of-the/index.php domain.http=you-intstallation-for-the-textfiles domain.dnstest=the.dns.zone domain.testns=the.authorativ.ns.for.domain.dnstest +domain.local=a.domain.that.resolves.to.localhost diff --git a/src/org/cacert/gigi/Gigi.java b/src/org/cacert/gigi/Gigi.java index 53685824..3f0cb798 100644 --- a/src/org/cacert/gigi/Gigi.java +++ b/src/org/cacert/gigi/Gigi.java @@ -2,6 +2,7 @@ package org.cacert.gigi; import java.io.IOException; import java.io.PrintWriter; +import java.security.KeyStore; import java.util.Calendar; import java.util.HashMap; import java.util.LinkedList; @@ -63,14 +64,15 @@ public class Gigi extends HttpServlet { private static Gigi instance; - private PingerDaemon pinger = new PingerDaemon(); + private PingerDaemon pinger; - public Gigi(Properties conf) { + public Gigi(Properties conf, KeyStore truststore) { if (instance != null) { throw new IllegalStateException("Multiple Gigi instances!"); } instance = this; DatabaseConnection.init(conf); + pinger = new PingerDaemon(truststore); pinger.start(); } @@ -156,7 +158,7 @@ public class Gigi extends HttpServlet { final Page p = getPage(req.getPathInfo()); if (p != null) { - if (!isSecure && (p.needsLogin() || p instanceof LoginPage || p instanceof RegisterPage)) { + if ( !isSecure && (p.needsLogin() || p instanceof LoginPage || p instanceof RegisterPage)) { resp.sendRedirect("https://" + ServerConstants.getWwwHostNamePortSecure() + req.getPathInfo()); return; } diff --git a/src/org/cacert/gigi/Launcher.java b/src/org/cacert/gigi/Launcher.java index 41ee8ac7..58758ffa 100644 --- a/src/org/cacert/gigi/Launcher.java +++ b/src/org/cacert/gigi/Launcher.java @@ -66,7 +66,7 @@ public class Launcher { HandlerList hl = new HandlerList(); hl.setHandlers(new Handler[] { - generateStaticContext(), generateGigiContexts(conf.getMainProps()), generateAPIContext() + generateStaticContext(), generateGigiContexts(conf.getMainProps(), conf.getTrustStore()), generateAPIContext() }); s.setHandler(hl); s.start(); @@ -86,7 +86,7 @@ public class Launcher { connector = new ServerConnector(s); } connector.setHost(conf.getMainProps().getProperty("host")); - if(doHttps) { + if (doHttps) { connector.setPort(ServerConstants.getSecurePort()); } else { connector.setPort(ServerConstants.getPort()); @@ -163,8 +163,8 @@ public class Launcher { }; } - private static Handler generateGigiContexts(Properties conf) { - ServletHolder webAppServlet = new ServletHolder(new Gigi(conf)); + private static Handler generateGigiContexts(Properties conf, KeyStore trust) { + ServletHolder webAppServlet = new ServletHolder(new Gigi(conf, trust)); ContextHandler ch = generateGigiServletContext(webAppServlet); ch.setVirtualHosts(new String[] { diff --git a/src/org/cacert/gigi/ping/PingerDaemon.java b/src/org/cacert/gigi/ping/PingerDaemon.java index ac61f14c..cd36dc66 100644 --- a/src/org/cacert/gigi/ping/PingerDaemon.java +++ b/src/org/cacert/gigi/ping/PingerDaemon.java @@ -1,13 +1,10 @@ package org.cacert.gigi.ping; -import java.io.FileReader; -import java.io.IOException; +import java.security.KeyStore; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.HashMap; -import java.util.Properties; - import org.cacert.gigi.Domain; import org.cacert.gigi.User; import org.cacert.gigi.database.DatabaseConnection; @@ -21,13 +18,19 @@ public class PingerDaemon extends Thread { private PreparedStatement enterPingResult; + private KeyStore truststore; + + public PingerDaemon(KeyStore truststore) { + this.truststore = truststore; + } + @Override public void run() { try { searchNeededPings = DatabaseConnection.getInstance().prepare("SELECT pingconfig.*, domains.domain, domains.memid FROM pingconfig LEFT JOIN domainPinglog ON domainPinglog.configId=pingconfig.id INNER JOIN domains ON domains.id=pingconfig.domainid WHERE domainPinglog.configId IS NULL "); enterPingResult = DatabaseConnection.getInstance().prepare("INSERT INTO domainPinglog SET configId=?, state=?, result=?, challenge=?"); pingers.put("email", new EmailPinger()); - pingers.put("ssl", new SSLPinger()); + pingers.put("ssl", new SSLPinger(truststore)); pingers.put("http", new HTTPFetch()); pingers.put("dns", new DNSPinger()); } catch (SQLException e) { @@ -69,12 +72,4 @@ public class PingerDaemon extends Thread { } } } - - public static void main(String[] args) throws IOException { - Properties conf = new Properties(); - conf.load(new FileReader("config/gigi.properties")); - DatabaseConnection.init(conf); - new PingerDaemon().run(); - - } } diff --git a/src/org/cacert/gigi/ping/SSLPinger.java b/src/org/cacert/gigi/ping/SSLPinger.java index 81c73937..0b253c5f 100644 --- a/src/org/cacert/gigi/ping/SSLPinger.java +++ b/src/org/cacert/gigi/ping/SSLPinger.java @@ -3,23 +3,30 @@ package org.cacert.gigi.ping; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.math.BigInteger; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.Arrays; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.SSLEngineResult.Status; import javax.net.ssl.SSLException; import javax.net.ssl.SSLEngineResult.HandshakeStatus; import javax.net.ssl.SSLParameters; import javax.security.cert.X509Certificate; +import org.cacert.gigi.Certificate; import org.cacert.gigi.Domain; import org.cacert.gigi.User; @@ -29,10 +36,15 @@ public class SSLPinger extends DomainPinger { "xmpp", "server-xmpp", "smtp", "imap" }; + private KeyStore truststore; + + public SSLPinger(KeyStore truststore) { + this.truststore = truststore; + } + @Override public String ping(Domain domain, String configuration, User u) { - try { - SocketChannel sch = SocketChannel.open(); + try (SocketChannel sch = SocketChannel.open()) { String[] parts = configuration.split(":", 2); sch.connect(new InetSocketAddress(domain.getSuffix(), Integer.parseInt(parts[0]))); if (parts.length == 2) { @@ -52,7 +64,7 @@ public class SSLPinger extends DomainPinger { } } - return test(sch, domain.getSuffix()); + return test(sch, domain.getSuffix(), u); } catch (IOException e) { return "Connecton failed"; } @@ -133,9 +145,18 @@ public class SSLPinger extends DomainPinger { } } - private String test(SocketChannel sch, String domain) { + private String test(SocketChannel sch, String domain, User subject) { try { - SSLContext sc = SSLContext.getDefault(); + SSLContext sc = SSLContext.getInstance("SSL"); + try { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init(truststore); + sc.init(null, tmf.getTrustManagers(), new SecureRandom()); + } catch (KeyManagementException e) { + e.printStackTrace(); + } catch (KeyStoreException e) { + e.printStackTrace(); + } SSLEngine se = sc.createSSLEngine(); ByteBuffer enc_in = ByteBuffer.allocate(se.getSession().getPacketBufferSize()); ByteBuffer enc_out = ByteBuffer.allocate(se.getSession().getPacketBufferSize()); @@ -182,11 +203,13 @@ public class SSLPinger extends DomainPinger { } } - System.out.println("completed"); - System.out.println(se.getSession().getCipherSuite()); X509Certificate[] peerCertificateChain = se.getSession().getPeerCertificateChain(); - for (X509Certificate x509Certificate : peerCertificateChain) { - System.out.println(x509Certificate.getSubjectDN().getName()); + X509Certificate first = peerCertificateChain[0]; + + BigInteger serial = first.getSerialNumber(); + Certificate c = Certificate.getBySerial(serial.toString(16)); + if (c.getOwnerId() != subject.getId()) { + return "Owner mismatch"; } return PING_SUCCEDED; } catch (NoSuchAlgorithmException e) { diff --git a/tests/org/cacert/gigi/ping/TestSSL.java b/tests/org/cacert/gigi/ping/TestSSL.java new file mode 100644 index 00000000..9b0e928d --- /dev/null +++ b/tests/org/cacert/gigi/ping/TestSSL.java @@ -0,0 +1,224 @@ +package org.cacert.gigi.ping; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.Socket; +import java.net.URL; +import java.net.URLEncoder; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.sql.SQLException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import org.cacert.gigi.Certificate; +import org.cacert.gigi.CertificateProfile; +import org.cacert.gigi.Certificate.CSRType; +import org.cacert.gigi.GigiApiException; +import org.cacert.gigi.pages.account.DomainOverview; +import org.cacert.gigi.testUtils.IOUtils; +import org.cacert.gigi.testUtils.PingTest; +import org.cacert.gigi.testUtils.TestEmailReciever.TestMail; +import org.junit.Test; + +public class TestSSL extends PingTest { + + private KeyPair kp; + + private Certificate c; + + @Test + public void sslAndMailSuccess() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException { + testEmailAndSSL(0, 0, true); + } + + @Test + public void sslWongTypeAndMailSuccess() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException { + testEmailAndSSL(1, 0, true); + } + + @Test + public void sslOneMissingAndMailSuccess() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException { + testEmailAndSSL(2, 0, true); + } + + @Test + public void sslBothMissingAndMailSuccess() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException { + testEmailAndSSL(3, 0, true); + } + + @Test + public void sslWrongTypeAndMailFail() throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException { + testEmailAndSSL(1, 1, false); + } + + /** + * @param sslVariant + * + * @param emailVariant + * @param successSSL + * @param successMail + * @throws IOException + * @throws InterruptedException + * @throws SQLException + * @throws GeneralSecurityException + * @throws GigiApiException + */ + + private void testEmailAndSSL(int sslVariant, int emailVariant, boolean successMail) throws IOException, InterruptedException, SQLException, GeneralSecurityException, GigiApiException { + String test = getTestProps().getProperty("domain.local"); + + URL u = new URL("https://" + getServerName() + DomainOverview.PATH); + + initailizeDomainForm(u); + + createCertificate(test, CertificateProfile.getByName(sslVariant == 1 ? "client" : "server")); + SSLServerSocket sss = createSSLServer(kp.getPrivate(), c.cert()); + int port = sss.getLocalPort(); + SSLServerSocket sss2 = createSSLServer(kp.getPrivate(), c.cert()); + int port2 = sss2.getLocalPort(); + if (sslVariant == 3 || sslVariant == 2) { + sss2.close(); + if (sslVariant == 3) { + sss.close(); + } + } + String content = "newdomain=" + URLEncoder.encode(test, "UTF-8") + // + "&emailType=y&email=2&SSLType=y" + // + "&ssl-type-0=direct&ssl-port-0=" + port + // + "&ssl-type-1=direct&ssl-port-1=" + port2 + // + "&ssl-type-2=direct&ssl-port-2=" + // + "&ssl-type-3=direct&ssl-port-3=" + // + "&adddomain&csrf=" + csrf; + URL u2 = sendDomainForm(u, content); + boolean firstSucceeds = sslVariant != 0 && sslVariant != 2; + assertTrue(firstSucceeds ^ acceptSSLServer(sss)); + boolean secondsSucceeds = sslVariant != 0; + assertTrue(secondsSucceeds ^ acceptSSLServer(sss2)); + + TestMail mail = getMailReciever().recieve(); + if (emailVariant == 0) { + String link = mail.extractLink(); + new URL(link).openConnection().getHeaderField(""); + } + waitForPings(3); + + String newcontent = IOUtils.readURL(cookie(u2.openConnection(), cookie)); + Pattern pat = Pattern.compile("ssl\\s*success"); + Matcher matcher = pat.matcher(newcontent); + assertTrue(newcontent, firstSucceeds ^ matcher.find()); + assertTrue(newcontent, secondsSucceeds ^ matcher.find()); + assertFalse(newcontent, matcher.find()); + pat = Pattern.compile("email\\s*success"); + assertTrue(newcontent, !successMail ^ pat.matcher(newcontent).find()); + } + + private void createCertificate(String test, CertificateProfile profile) throws GeneralSecurityException, IOException, SQLException, InterruptedException, GigiApiException { + kp = generateKeypair(); + String csr = generatePEMCSR(kp, "CN=" + test); + c = new Certificate(userid, "/CN=" + test, "sha256", csr, CSRType.CSR, profile); + c.issue(null, "2y").waitFor(60000); + } + + private boolean acceptSSLServer(SSLServerSocket sss) throws IOException { + try { + Socket s = sss.accept(); + s.getOutputStream().write('b'); + s.getOutputStream().close(); + return true; + } catch (IOException e) { + return false; + } + } + + private SSLServerSocket createSSLServer(final PrivateKey priv, final X509Certificate cert) throws Error, IOException { + SSLContext sc; + try { + sc = SSLContext.getInstance("SSL"); + sc.init(new KeyManager[] { + new X509KeyManager() { + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return new String[] { + "server" + }; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + return priv; + } + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + throw new Error(); + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + return new X509Certificate[] { + cert + }; + } + + @Override + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) { + throw new Error(); + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return "server"; + } + + } + }, new TrustManager[] { + new X509TrustManager() { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} + } + }, new SecureRandom()); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + throw new Error(e); + } catch (KeyManagementException e) { + e.printStackTrace(); + throw new Error(e); + } + + SSLServerSocketFactory sssf = sc.getServerSocketFactory(); + return (SSLServerSocket) sssf.createServerSocket(0); + } + +} diff --git a/tests/org/cacert/gigi/testUtils/ManagedTest.java b/tests/org/cacert/gigi/testUtils/ManagedTest.java index ed5f1eec..e9b23e28 100644 --- a/tests/org/cacert/gigi/testUtils/ManagedTest.java +++ b/tests/org/cacert/gigi/testUtils/ManagedTest.java @@ -4,9 +4,13 @@ import static org.junit.Assert.*; import java.io.BufferedReader; import java.io.DataOutputStream; +import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; @@ -426,7 +430,26 @@ public class ManagedTest { public static KeyPair generateKeypair() throws GeneralSecurityException { KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); kpg.initialize(4096); - return kpg.generateKeyPair(); + KeyPair keyPair = null; + File f = new File("testKeypair"); + if (f.exists()) { + try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(f))) { + keyPair = (KeyPair) ois.readObject(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } + } else { + keyPair = kpg.generateKeyPair(); + try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(f))) { + oos.writeObject(keyPair); + oos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + return keyPair; } public static String generatePEMCSR(KeyPair kp, String dn) throws GeneralSecurityException, IOException { diff --git a/tests/org/cacert/gigi/testUtils/PingTest.java b/tests/org/cacert/gigi/testUtils/PingTest.java index 6c57d76d..b8c0d910 100644 --- a/tests/org/cacert/gigi/testUtils/PingTest.java +++ b/tests/org/cacert/gigi/testUtils/PingTest.java @@ -27,7 +27,8 @@ public abstract class PingTest extends ClientTest { protected void waitForPings(int count) throws SQLException, InterruptedException { PreparedStatement ps = DatabaseConnection.getInstance().prepare("SELECT COUNT(*) FROM domainPinglog"); - while (true) { + long start = System.currentTimeMillis(); + while (System.currentTimeMillis() - start < 10000) { ResultSet rs = ps.executeQuery(); rs.next(); if (rs.getInt(1) >= count) { -- 2.39.2