001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.util;
020:
021: import java.net.HttpURLConnection;
022: import java.net.Socket;
023: import java.security.GeneralSecurityException;
024: import java.security.Principal;
025: import java.security.PrivateKey;
026: import java.security.Provider;
027: import java.security.SecureRandom;
028: import java.security.cert.X509Certificate;
029:
030: import javax.net.ssl.HostnameVerifier;
031: import javax.net.ssl.HttpsURLConnection;
032: import javax.net.ssl.KeyManager;
033: import javax.net.ssl.KeyManagerFactory;
034: import javax.net.ssl.SSLContext;
035: import javax.net.ssl.SSLSession;
036: import javax.net.ssl.TrustManager;
037: import javax.net.ssl.TrustManagerFactory;
038: import javax.net.ssl.X509KeyManager;
039: import javax.net.ssl.X509TrustManager;
040:
041: import org.apache.commons.httpclient.protocol.Protocol;
042: import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
043: import org.apache.jmeter.util.keystore.JmeterKeyStore;
044: import org.apache.jorphan.logging.LoggingManager;
045: import org.apache.log.Logger;
046:
047: /**
048: * The SSLManager handles the KeyStore information for JMeter. Basically, it
049: * handles all the logic for loading and initializing all the JSSE parameters
050: * and selecting the alias to authenticate against if it is available.
051: * SSLManager will try to automatically select the client certificate for you,
052: * but if it can't make a decision, it will pop open a dialog asking you for
053: * more information.
054: *
055: * TODO: does not actually prompt
056: *
057: */
058: public class JsseSSLManager extends SSLManager {
059: private static final Logger log = LoggingManager
060: .getLoggerForClass();
061:
062: private static final String HTTPS = "https"; // $NON-NLS-1$
063:
064: // Temporary fix to allow default protocol to be changed
065: private static final String DEFAULT_SSL_PROTOCOL = JMeterUtils
066: .getPropDefault("https.default.protocol", "TLS"); // $NON-NLS-1$ // $NON-NLS-2$
067:
068: // Allow reversion to original shared session context
069: private static final boolean SHARED_SESSION_CONTEXT = JMeterUtils
070: .getPropDefault("https.sessioncontext.shared", false); // $NON-NLS-1$
071:
072: private static final int cps;
073:
074: static {
075: log.info("Using default SSL protocol: " + DEFAULT_SSL_PROTOCOL);
076: log.info("SSL session context: "
077: + (SHARED_SESSION_CONTEXT ? "shared" : "per-thread"));
078: cps = JMeterUtils.getPropDefault("httpclient.socket.https.cps",
079: 0); // $NON-NLS-1$
080:
081: if (cps > 0) {
082: log.info("Setting up HTTPS SlowProtocol, cps=" + cps);
083: }
084:
085: }
086:
087: /**
088: * Cache the SecureRandom instance because it takes a long time to create
089: */
090: private SecureRandom rand;
091:
092: private Provider pro = null;
093:
094: private SSLContext defaultContext; // If we are using a single session
095: private ThreadLocal threadlocal; // Otherwise
096:
097: /**
098: * Create the SSLContext, and wrap all the X509KeyManagers with
099: * our X509KeyManager so that we can choose our alias.
100: *
101: * @param provider
102: * Description of Parameter
103: */
104: public JsseSSLManager(Provider provider) {
105: log.debug("ssl Provider = " + provider);
106: setProvider(provider);
107: if (null == this .rand) { // Surely this is always null in the constructor?
108: this .rand = new SecureRandom();
109: }
110: try {
111: if (SHARED_SESSION_CONTEXT) {
112: log.debug("Creating shared context");
113: this .defaultContext = createContext();
114: } else {
115: this .threadlocal = new ThreadLocal();
116: }
117:
118: /*
119: * Set up Java defaults.
120: * N.B. does not allow SlowSocket - fails with:
121: * java.lang.RuntimeException: Export restriction: this JSSE implementation is non-pluggable.
122: */
123:
124: HttpsURLConnection
125: .setDefaultSSLSocketFactory(new HttpSSLProtocolSocketFactory(
126: this ));
127: HttpsURLConnection
128: .setDefaultHostnameVerifier(new HostnameVerifier() {
129: public boolean verify(String hostname,
130: SSLSession session) {
131: return true;
132: }
133: });
134:
135: /*
136: * Also set up HttpClient defaults
137: */
138: Protocol protocol = new Protocol(
139: JsseSSLManager.HTTPS,
140: (ProtocolSocketFactory) new HttpSSLProtocolSocketFactory(
141: this , cps), 443);
142: Protocol.registerProtocol(JsseSSLManager.HTTPS, protocol);
143: log.debug("SSL stuff all set");
144: } catch (GeneralSecurityException ex) {
145: log.error("Could not set up SSLContext", ex);
146: }
147: log.debug("JsseSSLManager installed");
148: }
149:
150: /**
151: * Sets the Context attribute of the JsseSSLManager object
152: *
153: * @param conn
154: * The new Context value
155: */
156: public void setContext(HttpURLConnection conn) {
157: if (conn instanceof HttpsURLConnection) {
158: /*
159: * No point doing this on a per-connection basis, as there is currently no way to configure it.
160: * So we leave it to the defaults set up in the SSL Context
161: *
162: */
163: // HttpsURLConnection secureConn = (HttpsURLConnection) conn;
164: // secureConn.setSSLSocketFactory(this.getContext().getSocketFactory());
165: } else {
166: log.warn("Unexpected HttpURLConnection class: "
167: + conn.getClass().getName());
168: }
169: }
170:
171: /**
172: * Sets the Provider attribute of the JsseSSLManager object
173: *
174: * @param p
175: * The new Provider value
176: */
177: protected final void setProvider(Provider p) {
178: super .setProvider(p);
179: if (null == this .pro) {
180: this .pro = p;
181: }
182: }
183:
184: /**
185: * Returns the SSLContext we are using.
186: * This is either a context per thread,
187: * or, for backwards compatibility, a single shared context.
188: *
189: * @return The Context value
190: */
191: public SSLContext getContext() throws GeneralSecurityException {
192: if (SHARED_SESSION_CONTEXT) {
193: if (log.isDebugEnabled()) {
194: log.debug("Using shared SSL context for: "
195: + Thread.currentThread().getName());
196: }
197: return this .defaultContext;
198: }
199:
200: SSLContext sslContext = (SSLContext) this .threadlocal.get();
201: if (sslContext == null) {
202: if (log.isDebugEnabled()) {
203: log.debug("Creating threadLocal SSL context for: "
204: + Thread.currentThread().getName());
205: }
206: sslContext = createContext();
207: this .threadlocal.set(sslContext);
208: }
209: if (log.isDebugEnabled()) {
210: log.debug("Using threadLocal SSL context for: "
211: + Thread.currentThread().getName());
212: }
213: return sslContext;
214: }
215:
216: /**
217: * Resets the SSLContext if using per-thread contexts.
218: *
219: */
220: public void resetContext() {
221: if (!SHARED_SESSION_CONTEXT) {
222: log.debug("Clearing session context for current thread");
223: this .threadlocal.set(null);
224: }
225: }
226:
227: /*
228: *
229: * Creates new SSL context
230: * @return SSL context
231: * @throws GeneralSecurityException
232: */
233: private SSLContext createContext() throws GeneralSecurityException {
234: SSLContext context;
235: if (pro != null) {
236: context = SSLContext.getInstance(DEFAULT_SSL_PROTOCOL, pro); // $NON-NLS-1$
237: } else {
238: context = SSLContext.getInstance(DEFAULT_SSL_PROTOCOL); // $NON-NLS-1$
239: }
240: KeyManagerFactory managerFactory = KeyManagerFactory
241: .getInstance(KeyManagerFactory.getDefaultAlgorithm());
242: JmeterKeyStore keys = this .getKeyStore();
243: managerFactory.init(null, defaultpw == null ? new char[] {}
244: : defaultpw.toCharArray());
245: KeyManager[] managers = managerFactory.getKeyManagers();
246: log.debug(keys.getClass().toString());
247:
248: // Now wrap the default managers with our key manager
249: for (int i = 0; i < managers.length; i++) {
250: if (managers[i] instanceof X509KeyManager) {
251: X509KeyManager manager = (X509KeyManager) managers[i];
252: managers[i] = new WrappedX509KeyManager(manager, keys);
253: }
254: }
255:
256: // Get the default trust managers
257: TrustManagerFactory tmfactory = TrustManagerFactory
258: .getInstance(TrustManagerFactory.getDefaultAlgorithm());
259: tmfactory.init(this .getTrustStore());
260:
261: // Wrap the defaults in our custom trust manager
262: TrustManager[] trustmanagers = tmfactory.getTrustManagers();
263: for (int i = 0; i < trustmanagers.length; i++) {
264: if (trustmanagers[i] instanceof X509TrustManager) {
265: trustmanagers[i] = new CustomX509TrustManager(
266: (X509TrustManager) trustmanagers[i]);
267: }
268: }
269: context.init(managers, trustmanagers, this .rand);
270: if (log.isDebugEnabled()) {
271: String[] dCiphers = context.getSocketFactory()
272: .getDefaultCipherSuites();
273: String[] sCiphers = context.getSocketFactory()
274: .getSupportedCipherSuites();
275: int len = (dCiphers.length > sCiphers.length) ? dCiphers.length
276: : sCiphers.length;
277: for (int i = 0; i < len; i++) {
278: if (i < dCiphers.length) {
279: log.debug("Default Cipher: " + dCiphers[i]);
280: }
281: if (i < sCiphers.length) {
282: log.debug("Supported Cipher: " + sCiphers[i]);
283: }
284: }
285: }
286: return context;
287: }
288:
289: /**
290: * This is the X509KeyManager we have defined for the sole purpose of
291: * selecting the proper key and certificate based on the keystore available.
292: *
293: * @author MStover Created March 21, 2002
294: */
295: private static class WrappedX509KeyManager implements
296: X509KeyManager {
297: /**
298: * The parent X509KeyManager
299: */
300: private final X509KeyManager manager;
301:
302: /**
303: * The KeyStore this KeyManager uses
304: */
305: private final JmeterKeyStore store;
306:
307: /**
308: * Instantiate a new WrappedX509KeyManager.
309: *
310: * @param parent
311: * The parent X509KeyManager
312: * @param ks
313: * The KeyStore we derive our client certs and keys from
314: */
315: public WrappedX509KeyManager(X509KeyManager parent,
316: JmeterKeyStore ks) {
317: this .manager = parent;
318: this .store = ks;
319: }
320:
321: /**
322: * Compiles the list of all client aliases with a private key.
323: * Currently, keyType and issuers are both ignored.
324: *
325: * @param keyType
326: * the type of private key the server expects (RSA, DSA,
327: * etc.)
328: * @param issuers
329: * the CA certificates we are narrowing our selection on.
330: * @return the ClientAliases value
331: */
332: public String[] getClientAliases(String keyType,
333: Principal[] issuers) {
334: log.debug("WrappedX509Manager: getClientAliases: ");
335: log.debug(this .store.getAlias());
336: return new String[] { this .store.getAlias() };
337: }
338:
339: /**
340: * Get the list of server aliases for the SSLServerSockets. This is not
341: * used in JMeter.
342: *
343: * @param keyType
344: * the type of private key the server expects (RSA, DSA,
345: * etc.)
346: * @param issuers
347: * the CA certificates we are narrowing our selection on.
348: * @return the ServerAliases value
349: */
350: public String[] getServerAliases(String keyType,
351: Principal[] issuers) {
352: log.debug("WrappedX509Manager: getServerAliases: ");
353: return this .manager.getServerAliases(keyType, issuers);
354: }
355:
356: /**
357: * Get the Certificate chain for a particular alias
358: *
359: * @param alias
360: * The client alias
361: * @return The CertificateChain value
362: */
363: public X509Certificate[] getCertificateChain(String alias) {
364: log.debug("WrappedX509Manager: getCertificateChain("
365: + alias + ")");
366: return this .store.getCertificateChain();
367: }
368:
369: /**
370: * Get the Private Key for a particular alias
371: *
372: * @param alias
373: * The client alias
374: * @return The PrivateKey value
375: */
376: public PrivateKey getPrivateKey(String alias) {
377: log.debug("WrappedX509Manager: getPrivateKey: "
378: + this .store.getPrivateKey());
379: return this .store.getPrivateKey();
380: }
381:
382: /**
383: * Select the Alias we will authenticate as if Client authentication is
384: * required by the server we are connecting to. We get the list of
385: * aliases, and if there is only one alias we automatically select it.
386: * If there are more than one alias that has a private key, we prompt
387: * the user to choose which alias using a combo box. Otherwise, we
388: * simply provide a text box, which may or may not work. The alias does
389: * have to match one in the keystore.
390: *
391: * TODO? - does not actually allow the user to choose an alias at present
392: *
393: * @see javax.net.ssl.X509KeyManager#chooseClientAlias(String[], Principal[], Socket)
394: */
395: public String chooseClientAlias(String[] keyType,
396: Principal[] issuers, Socket socket) {
397: String alias = this .store.getAlias();
398: log.debug("ClientAlias: " + alias);
399: if (alias == null || alias.length() == 0) {
400: log.debug("ClientAlias not found.");
401: }
402: return alias;
403: }
404:
405: /**
406: * Choose the server alias for the SSLServerSockets. This are not used
407: * in JMeter.
408: *
409: * @see javax.net.ssl.X509KeyManager#chooseServerAlias(String, Principal[], Socket)
410: */
411: public String chooseServerAlias(String arg0, Principal[] arg1,
412: Socket arg2) {
413: return this.manager.chooseServerAlias(arg0, arg1, arg2);
414: }
415: }
416: }
|