package org.jolokia.jvmagent; /* * Copyright 2009-2014 Roland Huss * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.io.*; import java.lang.reflect.Field; import java.net.*; import java.security.*; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.net.ssl.*; import com.sun.net.httpserver.HttpServer; import org.jolokia.Version; import org.jolokia.jvmagent.security.KeyStoreUtil; import org.jolokia.test.util.EnvTestUtil; import org.jolokia.util.Base64Util; import org.testng.annotations.Test; import static org.testng.Assert.*; /** * @author roland * @author nevenr * @since 31.08.11 */ public class JolokiaServerTest { @Test public void http() throws Exception { String configs[] = { null, "executor=fixed,threadNr=5", "executor=cached", "executor=single" }; for (String c : configs) { roundtrip(c, true); } } @Test(expectedExceptions = IOException.class,expectedExceptionsMessageRegExp = ".*401.*") public void httpWithAuthenticationRejected() throws Exception { Map config = new HashMap(); config.put("user", "roland"); config.put("password", "s!cr!t"); config.put("port", "0"); roundtrip(config, true); } @Test public void serverPicksThePort() throws Exception { roundtrip("host=localhost,port=0", true); } // SSL Checks ======================================================================================== /* Test Scenarios ============== - 1 no client auth: - 11 https only (no certs) - 12 with keystore - 13 with PEM server cert - 131 without CA validation - 132 with CA validation (positive) - 2 with client auth: - 21 self-signed client cert --> fail - 22 properly signed client cert --> ok - 23 with 'extended key usage check' - 231 with extended key usage == client --> ok - 232 with extended key usage == server --> fail - 233 with no extended key usage: - 2331 with 'extendedClientCheck' options == true --> fail - 2332 with 'extendedClientCheck' option == false --> ok - 24 with 'clientPrincipal' given - 241 matching clientPrincipal --> ok - 241 non-matching clientPrincipal --> fail - 25 no CA given to verify against --> fail - 26 with clientPrincipal and basic auth */ @Test public void t_11_https_only() throws Exception { httpsRoundtrip("agentId=test", false); } @Test public void t_12_with_keystore() throws Exception { httpsRoundtrip("keystore=" + getResourcePath("/keystore") + ",keystorePassword=jetty7", false); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = ".*without.*key.*") public void serverCertWithoutKey() throws Exception { httpsRoundtrip("serverCert=" + getCertPath("server/cert.pem"), false); } @Test public void t_131_pem_without_ca() throws Exception { httpsRoundtrip("serverCert=" + getCertPath("server/cert.pem") + "," + "serverKey=" + getCertPath("server/key.pem"), false); } @Test public void t_132_pem_with_ca() throws Exception { httpsRoundtrip(getFullCertSetup(), true); } @Test(expectedExceptions = IOException.class) public void t_21_self_signed_client_cert_fail() throws Exception { httpsRoundtrip("useSslClientAuthentication=true," + getFullCertSetup(), true, "client/self-signed-with-key-usage"); } @Test public void t_22_signed_client_cert() throws Exception { // default is no extended client check httpsRoundtrip("useSslClientAuthentication=true," + getFullCertSetup(), true, "client/without-key-usage"); } @Test public void t_231_with_extended_client_key_usage() throws Exception { httpsRoundtrip("useSslClientAuthentication=true,extendedClientCheck=true," + getFullCertSetup(), true, "client/with-key-usage"); } @Test(expectedExceptions = IOException.class) public void t_232_with_wrong_extended_client_key_usage() throws Exception { httpsRoundtrip("useSslClientAuthentication=true,extendedClientCheck=true," + getFullCertSetup(), true, "client/with-wrong-key-usage"); } @Test(expectedExceptions = IOException.class, expectedExceptionsMessageRegExp = ".*403.*") public void t_2331_without_extended_client_key_usage() throws Exception { httpsRoundtrip("useSslClientAuthentication=true,extendedClientCheck=true," + getFullCertSetup(), true, "client/without-key-usage"); } @Test public void t_2332_without_extended_client_key_usage_allowed() throws Exception { httpsRoundtrip("useSslClientAuthentication=true,extendedClientCheck=false," + getFullCertSetup(), true, "client/with-key-usage"); } @Test(expectedExceptions = IOException.class) public void t_2333_with_wrong_extended_client_key_usage_allowed() throws Exception { httpsRoundtrip("useSslClientAuthentication=true,extendedClientCheck=false," + getFullCertSetup(), true, "client/with-wrong-key-usage"); } @Test public void t_241_with_client_principal() throws Exception { httpsRoundtrip("useSslClientAuthentication=true,clientPrincipal=O\\=jolokia.org\\,CN\\=Client signed with client key usage," + getFullCertSetup(), true, "client/with-key-usage"); } @Test(expectedExceptions = IOException.class, expectedExceptionsMessageRegExp = ".*403.*") public void t_242_with_wrong_client_principal() throws Exception { httpsRoundtrip("useSslClientAuthentication=true,clientPrincipal=O=microsoft.com," + getFullCertSetup(), true, "client/with-key-usage"); } @Test(expectedExceptions = IllegalArgumentException.class, expectedExceptionsMessageRegExp = ".*no CA.*") public void t_25_no_ca_given() throws Exception { httpsRoundtrip("useSslClientAuthentication=true," + "serverCert=" + getCertPath("server/cert.pem") + "," + "serverKey=" + getCertPath("server/key.pem"), true, "client/with-key-usage"); } @Test public void t_261_with_client_principal() throws Exception { httpsRoundtrip("authMode=basic,user=admin,password=password,useSslClientAuthentication=true,clientPrincipal=O\\=jolokia.org\\,CN\\=Client signed with client key usage," + getFullCertSetup(), true, "client/with-key-usage"); } @Test(expectedExceptions = IOException.class, expectedExceptionsMessageRegExp = ".*401.*") public void t_262_with_wrong_client_principal() throws Exception { httpsRoundtrip("authMode=basic,user=admin,password=password,useSslClientAuthentication=true,clientPrincipal=O=microsoft.com," + getFullCertSetup(), true, "client/with-key-usage"); } @Test public void t_263_with_basic_auth() throws Exception { httpsRoundtrip("authMode=basic,user=admin,password=password,useSslClientAuthentication=true,clientPrincipal=O=microsoft.com," + getFullCertSetup(), true, "client/with-key-usage", "admin:password"); } @Test(expectedExceptions = IOException.class, expectedExceptionsMessageRegExp = ".*401.*") public void t_264_with_wrong_basic_auth() throws Exception { httpsRoundtrip("authMode=basic,user=admin,password=password,useSslClientAuthentication=true,clientPrincipal=O=microsoft.com," + getFullCertSetup(), true, "client/with-key-usage", "admin:wrong"); } @Test(expectedExceptions = IOException.class, expectedExceptionsMessageRegExp = ".*401.*") public void t_264_with_basic_auth_and_wrong_client_cert() throws Exception { httpsRoundtrip("authMode=basic,user=admin,password=password,useSslClientAuthentication=true,clientPrincipal=O=microsoft.com," + getFullCertSetup(), true, "client/self-signed-with-key-usage", "admin:wrong"); } // ================================================================================================== private String getFullCertSetup() { return "serverCert=" + getCertPath("server/cert.pem") + "," + "serverKey=" + getCertPath("server/key.pem") + "," + "caCert=" + getCertPath("ca/cert.pem"); } @Test public void sslWithAdditionalHttpsSettings() throws Exception { httpsRoundtrip("keystore=" + getResourcePath("/keystore") + ",keystorePassword=jetty7" + ",config=" + getResourcePath("/agent-test-additionalHttpsConf.properties"), false); } @Test public void sslWithSpecialHttpsSettings() throws Exception { JvmAgentConfig config = new JvmAgentConfig( prepareConfigString("host=localhost,port=" + EnvTestUtil.getFreePort() + ",protocol=https," + getFullCertSetup() + ",config=" + getResourcePath("/agent-test-specialHttpsSettings.properties"))); JolokiaServer server = new JolokiaServer(config, false); server.start(); // Skipping hostname verification because the cert doesn't have a SAN of localhost HostnameVerifier verifier = createHostnameVerifier(); HostnameVerifier oldVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); SSLSocketFactory oldSslSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); List<String> cipherSuites = Arrays.asList(config.getSSLCipherSuites()); List<String> protocols = Arrays.asList(config.getSSLProtocols()); for (String protocol : new String[]{"SSLv3", "TLSv1", "TLSv1.1", "TLSv1.2"}) { // Make sure at least one connection for this protocol succeeds (if expected to) boolean connectionSucceeded = false; for (String cipherSuite : oldSslSocketFactory.getSupportedCipherSuites()) { if (!cipherSuites.contains(cipherSuite)) continue; try { TrustManager tms[] = getTrustManagers(true); SSLContext sc = SSLContext.getInstance(protocol); sc.init(new KeyManager[0], tms, new java.security.SecureRandom()); HttpsURLConnection.setDefaultHostnameVerifier(verifier); HttpsURLConnection.setDefaultSSLSocketFactory( new FakeSSLSocketFactory(sc.getSocketFactory(), new String[]{protocol}, new String[]{cipherSuite})); URL url = new URL(server.getUrl()); String resp = EnvTestUtil.readToString(url.openStream()); assertTrue( resp.matches(".*type.*version.*" + Version.getAgentVersion() + ".*")); if (!protocols.contains(protocol) || !cipherSuites.contains(cipherSuite)) { fail(String.format("Expected SSLHandshakeException with the %s protocol and %s cipher suite", protocol, cipherSuite)); } connectionSucceeded = true; } catch (javax.net.ssl.SSLHandshakeException e) { // We make sure at least one connection with this protocol succeeds if expected // down below } finally { HttpsURLConnection.setDefaultHostnameVerifier(oldVerifier); HttpsURLConnection.setDefaultSSLSocketFactory(oldSslSocketFactory); } } if (protocols.contains(protocol) && !connectionSucceeded) { fail("Expected at least one connection to succeed on " + protocol); } } server.stop(); } @Test(expectedExceptions = IllegalArgumentException.class,expectedExceptionsMessageRegExp = ".*password.*") public void invalidConfig() throws IOException, InterruptedException { JvmAgentConfig cfg = new JvmAgentConfig("user=roland,port=" + EnvTestUtil.getFreePort()); Thread.sleep(1000); new JolokiaServer(cfg, false); } @Test public void customHttpServer() throws IOException, NoSuchFieldException, IllegalAccessException { HttpServer httpServer = HttpServer.create(); JvmAgentConfig cfg = new JvmAgentConfig(""); JolokiaServer server = new JolokiaServer(httpServer, cfg, false); Field field = JolokiaServer.class.getDeclaredField("httpServer"); field.setAccessible(true); assertNull(field.get(server)); server.start(); server.stop(); } // ================================================================== private String getCertPath(String pCert) { return getResourcePath("/certs/" + pCert); } private String getResourcePath(String relativeResourcePath) { URL ksURL = this.getClass().getResource(relativeResourcePath); if (ksURL != null && "file".equalsIgnoreCase(ksURL.getProtocol())) { return URLDecoder.decode(ksURL.getPath()); } throw new IllegalStateException(ksURL + " is not a file URL"); } private void roundtrip(Map<String,String> pConfig, boolean pDoRequest) throws Exception { checkServer(new JvmAgentConfig(pConfig), pDoRequest); } private void roundtrip(String pConfig, boolean pDoRequest) throws Exception { JvmAgentConfig config = new JvmAgentConfig(prepareConfigString(pConfig)); checkServer(config, pDoRequest); } private void httpsRoundtrip(String pConfig, boolean pValidateCa) throws Exception { httpsRoundtrip(pConfig, pValidateCa, "client/with-key-usage"); } private void httpsRoundtrip(String pConfig, boolean pValidateCa, String clientCert) throws Exception { JvmAgentConfig config = new JvmAgentConfig( prepareConfigString("host=localhost,port=" + EnvTestUtil.getFreePort() + ",protocol=https," + pConfig)); checkServer(config, true, createHostnameVerifier(), pValidateCa, clientCert); } private void httpsRoundtrip(String pConfig, boolean pValidateCa, String clientCert, String pUserPassword) throws Exception { JvmAgentConfig config = new JvmAgentConfig( prepareConfigString("host=localhost,port=" + EnvTestUtil.getFreePort() + ",protocol=https," + pConfig)); checkServer(config, true, createHostnameVerifier(), pValidateCa, clientCert, pUserPassword); } private HostnameVerifier createHostnameVerifier() { return new HostnameVerifier() { @Override public boolean verify(String host, SSLSession sslSession) { return true; } }; } private String prepareConfigString(String pConfig) throws IOException { String c = pConfig != null ? pConfig + "," : ""; boolean portSpecified = c.contains("port="); c = c + "host=localhost,"; if (!portSpecified) { int port = EnvTestUtil.getFreePort(); c = c + "port=" + port; } return c; } private void checkServer(JvmAgentConfig pConfig, boolean pDoRequest) throws Exception { checkServer(pConfig, pDoRequest, null, false, null); } private TrustManager[] getTrustManagers(final boolean pValidateCa) throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException { if (!pValidateCa) { return new TrustManager[] { getAllowAllTrustManager() }; } else { KeyStore keystore = KeyStore.getInstance("JKS"); keystore.load(null); KeyStoreUtil.updateWithCaPem(keystore, new File(getCertPath("ca/cert.pem"))); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keystore); return tmf.getTrustManagers(); } } private TrustManager getAllowAllTrustManager() { return new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] certs, String authType) { System.out.println(certs); } public void checkServerTrusted(X509Certificate[] certs, String authType) { System.out.println(certs); } }; } private void checkServer(JvmAgentConfig pConfig, boolean pDoRequest, HostnameVerifier pVerifier, boolean pValidateCa, String pClientCert) throws Exception { checkServer(pConfig, pDoRequest, pVerifier, pValidateCa, pClientCert, null); } private void checkServer(JvmAgentConfig pConfig, boolean pDoRequest, HostnameVerifier pVerifier, boolean pValidateCa, String pClientCert, String pUserPassword) throws Exception { JolokiaServer server = new JolokiaServer(pConfig, false); server.start(); //Thread.sleep(2000); HostnameVerifier oldVerifier = HttpsURLConnection.getDefaultHostnameVerifier(); SSLSocketFactory oldSslSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory(); try { if (pDoRequest) { if (pVerifier != null) { HttpsURLConnection.setDefaultHostnameVerifier(pVerifier); } TrustManager tms[] = null; KeyManager kms[] = null; SSLContext sc = SSLContext.getInstance("SSL"); tms = getTrustManagers(pValidateCa); if (pClientCert != null) { KeyStore ks = KeyStore.getInstance("PKCS12"); InputStream fis = getClass().getResourceAsStream("/certs/" + pClientCert + "/cert.p12"); ks.load(fis, "1234".toCharArray()); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, "1234".toCharArray()); kms = kmf.getKeyManagers() ; } sc.init(kms, tms, new java.security.SecureRandom()); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); } URL url = new URL(server.getUrl()); URLConnection uc = url.openConnection(); if (pUserPassword != null) { uc.setRequestProperty("Authorization", "Basic " + Base64Util.encode(pUserPassword.getBytes())); } uc.connect(); String resp = EnvTestUtil.readToString(uc.getInputStream()); assertTrue(resp.matches(".*type.*version.*" + Version.getAgentVersion() + ".*")); } finally { server.stop(); try { Thread.sleep(10); } catch (InterruptedException e) { } HttpsURLConnection.setDefaultHostnameVerifier(oldVerifier); HttpsURLConnection.setDefaultSSLSocketFactory(oldSslSocketFactory); } } // FakeSSLSocketFactory wraps a normal SSLSocketFactory so it can set the explicit SSL / TLS // protocol version(s) and cipher suite(s) private static class FakeSSLSocketFactory extends SSLSocketFactory { private String[] cipherSuites; private String[] protocols; private SSLSocketFactory socketFactory; public FakeSSLSocketFactory(SSLSocketFactory socketFactory, String[] protocols, String[] cipherSuites) { super(); this.socketFactory = socketFactory; this.protocols = protocols; this.cipherSuites = cipherSuites; } public Socket createSocket(InetAddress host, int port) throws IOException { return wrapSocket((SSLSocket)socketFactory.createSocket(host, port)); } public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException { return wrapSocket((SSLSocket)socketFactory.createSocket(s, host, port, autoClose)); } public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException { return wrapSocket((SSLSocket)socketFactory.createSocket(address, port, localAddress, localPort)); } public Socket createSocket(String host, int port) throws IOException { return wrapSocket((SSLSocket)socketFactory.createSocket(host, port)); } public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException { return wrapSocket((SSLSocket)socketFactory.createSocket(host, port, localHost, localPort)); } public String[] getDefaultCipherSuites() { return socketFactory.getDefaultCipherSuites(); } public String[] getSupportedCipherSuites() { return socketFactory.getSupportedCipherSuites(); } private Socket wrapSocket(SSLSocket sslSocket) { sslSocket.setEnabledProtocols(this.protocols); sslSocket.setEnabledCipherSuites(this.cipherSuites); return sslSocket; } } }