001: /*
002: * $Header: /home/cvs/jakarta-tomcat-4.0/catalina/src/share/org/apache/catalina/valves/CertificatesValve.java,v 1.10 2002/06/09 02:19:44 remm Exp $
003: * $Revision: 1.10 $
004: * $Date: 2002/06/09 02:19:44 $
005: *
006: * ====================================================================
007: *
008: * The Apache Software License, Version 1.1
009: *
010: * Copyright (c) 1999-2001 The Apache Software Foundation. All rights
011: * reserved.
012: *
013: * Redistribution and use in source and binary forms, with or without
014: * modification, are permitted provided that the following conditions
015: * are met:
016: *
017: * 1. Redistributions of source code must retain the above copyright
018: * notice, this list of conditions and the following disclaimer.
019: *
020: * 2. Redistributions in binary form must reproduce the above copyright
021: * notice, this list of conditions and the following disclaimer in
022: * the documentation and/or other materials provided with the
023: * distribution.
024: *
025: * 3. The end-user documentation included with the redistribution, if
026: * any, must include the following acknowlegement:
027: * "This product includes software developed by the
028: * Apache Software Foundation (http://www.apache.org/)."
029: * Alternately, this acknowlegement may appear in the software itself,
030: * if and wherever such third-party acknowlegements normally appear.
031: *
032: * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
033: * Foundation" must not be used to endorse or promote products derived
034: * from this software without prior written permission. For written
035: * permission, please contact apache@apache.org.
036: *
037: * 5. Products derived from this software may not be called "Apache"
038: * nor may "Apache" appear in their names without prior written
039: * permission of the Apache Group.
040: *
041: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
042: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
043: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
044: * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
045: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
046: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
047: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
048: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
049: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
050: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
051: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
052: * SUCH DAMAGE.
053: * ====================================================================
054: *
055: * This software consists of voluntary contributions made by many
056: * individuals on behalf of the Apache Software Foundation. For more
057: * information on the Apache Software Foundation, please see
058: * <http://www.apache.org/>.
059: *
060: * [Additional notices, if required by prior licensing conditions]
061: *
062: */
063:
064: package org.apache.catalina.valves;
065:
066: import java.io.ByteArrayInputStream;
067: import java.io.IOException;
068: import javax.net.ssl.SSLPeerUnverifiedException;
069: import javax.net.ssl.SSLSession;
070: import javax.net.ssl.SSLSocket;
071: import java.security.cert.CertificateFactory;
072: import javax.security.cert.X509Certificate;
073: import javax.servlet.ServletException;
074: import org.apache.catalina.Context;
075: import org.apache.catalina.Globals;
076: import org.apache.catalina.Lifecycle;
077: import org.apache.catalina.LifecycleEvent;
078: import org.apache.catalina.LifecycleException;
079: import org.apache.catalina.LifecycleListener;
080: import org.apache.catalina.Logger;
081: import org.apache.catalina.Request;
082: import org.apache.catalina.Response;
083: import org.apache.catalina.Valve;
084: import org.apache.catalina.ValveContext;
085: import org.apache.catalina.connector.RequestWrapper;
086: import org.apache.catalina.deploy.LoginConfig;
087: import org.apache.catalina.util.LifecycleSupport;
088: import org.apache.catalina.util.StringManager;
089:
090: /**
091: * <p>Implementation of a Valve that deals with SSL client certificates, as
092: * follows:</p>
093: * <ul>
094: * <li>If this request was not received on an SSL socket, simply pass it
095: * on unmodified.</li>
096: * <li>If this request was received on an SSL socket:
097: * <ul>
098: * <li>If this web application is using client certificate authentication,
099: * and no certificate chain is present (because the underlying socket
100: * did not default to requiring it), force an SSL handshake to acquire
101: * the client certificate chain.</li>
102: * <li>If there is a client certificate chain present, expose it as a
103: * request attribute.</li>
104: * <li>Expose the cipher suite and key size used on this SSL socket
105: * as request attributes.</li>
106: * </ul>
107: *
108: * <p>The above tasks have been combined into a single Valve to minimize the
109: * amount of code that has to check for the existence of JSSE classes.</p>
110: *
111: * @author Craig R. McClanahan
112: * @version $Revision: 1.10 $ $Date: 2002/06/09 02:19:44 $
113: */
114:
115: public final class CertificatesValve extends ValveBase implements
116: Lifecycle {
117:
118: // ----------------------------------------------------- Instance Variables
119:
120: /**
121: * Are certificates required for authentication by this web application?
122: */
123: protected boolean certificates = false;
124:
125: /**
126: * A mapping table to determine the number of effective bits in the key
127: * when using a cipher suite containing the specified cipher name. The
128: * underlying data came from the TLS Specification (RFC 2246), Appendix C.
129: */
130: protected static final CipherData ciphers[] = {
131: new CipherData("_WITH_NULL_", 0),
132: new CipherData("_WITH_IDEA_CBC_", 128),
133: new CipherData("_WITH_RC2_CBC_40_", 40),
134: new CipherData("_WITH_RC4_40_", 40),
135: new CipherData("_WITH_RC4_128_", 128),
136: new CipherData("_WITH_DES40_CBC_", 40),
137: new CipherData("_WITH_DES_CBC_", 56),
138: new CipherData("_WITH_3DES_EDE_CBC_", 168) };
139:
140: /**
141: * The debugging detail level for this component.
142: */
143: protected int debug = 0;
144:
145: /**
146: * The descriptive information related to this implementation.
147: */
148: protected static final String info = "org.apache.catalina.valves.CertificatesValve/1.0";
149:
150: /**
151: * The lifecycle event support for this component.
152: */
153: protected LifecycleSupport lifecycle = new LifecycleSupport(this );
154:
155: /**
156: * The StringManager for this package.
157: */
158: protected static StringManager sm = StringManager
159: .getManager(Constants.Package);
160:
161: /**
162: * Has this component been started yet?
163: */
164: protected boolean started = false;
165:
166: // ------------------------------------------------------------- Properties
167:
168: /**
169: * Return the debugging detail level for this component.
170: */
171: public int getDebug() {
172:
173: return (this .debug);
174:
175: }
176:
177: /**
178: * Set the debugging detail level for this component.
179: */
180: public void setDebug(int debug) {
181:
182: this .debug = debug;
183:
184: }
185:
186: /**
187: * Return descriptive information about this Valve implementation.
188: */
189: public String getInfo() {
190:
191: return (info);
192:
193: }
194:
195: // --------------------------------------------------------- Public Methods
196:
197: /**
198: * Expose the certificates chain if one was included on this request.
199: *
200: * @param request The servlet request to be processed
201: * @param response The servlet response to be created
202: * @param context The valve context used to invoke the next valve
203: * in the current processing pipeline
204: *
205: * @exception IOException if an input/output error occurs
206: * @exception ServletException if a servlet error occurs
207: */
208: public void invoke(Request request, Response response,
209: ValveContext context) throws IOException, ServletException {
210:
211: // Identify the underlying request if this request was wrapped
212: Request actual = request;
213: while (actual instanceof RequestWrapper)
214: actual = ((RequestWrapper) actual).getWrappedRequest();
215: // if (debug >= 2)
216: // log("Processing request");
217:
218: // Verify the existence of a certificate chain if appropriate
219: if (certificates)
220: verify(request, actual);
221:
222: // Expose the certificate chain if appropriate
223: expose(request, actual);
224:
225: // Invoke the next Valve in our Pipeline
226: context.invokeNext(request, response);
227:
228: }
229:
230: // ------------------------------------------------------ Lifecycle Methods
231:
232: /**
233: * Add a LifecycleEvent listener to this component.
234: *
235: * @param listener The listener to add
236: */
237: public void addLifecycleListener(LifecycleListener listener) {
238:
239: lifecycle.addLifecycleListener(listener);
240:
241: }
242:
243: /**
244: * Get the lifecycle listeners associated with this lifecycle. If this
245: * Lifecycle has no listeners registered, a zero-length array is returned.
246: */
247: public LifecycleListener[] findLifecycleListeners() {
248:
249: return lifecycle.findLifecycleListeners();
250:
251: }
252:
253: /**
254: * Remove a LifecycleEvent listener from this component.
255: *
256: * @param listener The listener to remove
257: */
258: public void removeLifecycleListener(LifecycleListener listener) {
259:
260: lifecycle.removeLifecycleListener(listener);
261:
262: }
263:
264: /**
265: * Prepare for the beginning of active use of the public methods of this
266: * component. This method should be called before any of the public
267: * methods of this component are utilized. It should also send a
268: * LifecycleEvent of type START_EVENT to any registered listeners.
269: *
270: * @exception LifecycleException if this component detects a fatal error
271: * that prevents this component from being used
272: */
273: public void start() throws LifecycleException {
274:
275: // Validate and update our current component state
276: if (started)
277: throw new LifecycleException(sm
278: .getString("certificatesValve.alreadyStarted"));
279: started = true;
280: if (debug >= 1)
281: log("Starting");
282:
283: // Check what type of authentication (if any) we are doing
284: certificates = false;
285: if (container instanceof Context) {
286: Context context = (Context) container;
287: LoginConfig loginConfig = context.getLoginConfig();
288: if (loginConfig != null) {
289: String authMethod = loginConfig.getAuthMethod();
290: if ("CLIENT-CERT".equalsIgnoreCase(authMethod))
291: certificates = true;
292: }
293: }
294:
295: // Notify our interested LifecycleListeners
296: lifecycle.fireLifecycleEvent(Lifecycle.START_EVENT, null);
297:
298: }
299:
300: /**
301: * Gracefully terminate the active use of the public methods of this
302: * component. This method should be the last one called on a given
303: * instance of this component. It should also send a LifecycleEvent
304: * of type STOP_EVENT to any registered listeners.
305: *
306: * @exception LifecycleException if this component detects a fatal error
307: * that needs to be reported
308: */
309: public void stop() throws LifecycleException {
310:
311: // Validate and update our current component state
312: if (!started)
313: throw new LifecycleException(sm
314: .getString("certificatesValve.notStarted"));
315: lifecycle.fireLifecycleEvent(Lifecycle.STOP_EVENT, null);
316: started = false;
317: if (debug >= 1)
318: log("Stopping");
319:
320: certificates = false;
321:
322: }
323:
324: // ------------------------------------------------------ Protected Methods
325:
326: /**
327: * Expose the certificate chain for this request, if there is one.
328: *
329: * @param request The possibly wrapped Request being processed
330: * @param actual The actual underlying Request object
331: */
332: protected void expose(Request request, Request actual) {
333:
334: // Ensure that this request came in on an SSLSocket
335: if (actual.getSocket() == null)
336: return;
337: if (!(actual.getSocket() instanceof SSLSocket))
338: return;
339: SSLSocket socket = (SSLSocket) actual.getSocket();
340:
341: // Look up the current SSLSession
342: SSLSession session = socket.getSession();
343: if (session == null)
344: return;
345: // if (debug >= 2)
346: // log(" expose: Has current SSLSession");
347:
348: // Expose the cipher suite and key size
349: String cipherSuite = session.getCipherSuite();
350: if (cipherSuite != null)
351: request.getRequest().setAttribute(
352: Globals.CIPHER_SUITE_ATTR, cipherSuite);
353: Integer keySize = (Integer) session
354: .getValue(Globals.KEY_SIZE_ATTR);
355: if (keySize == null) {
356: int size = 0;
357: for (int i = 0; i < ciphers.length; i++) {
358: if (cipherSuite.indexOf(ciphers[i].phrase) >= 0) {
359: size = ciphers[i].keySize;
360: break;
361: }
362: }
363: keySize = new Integer(size);
364: session.putValue(Globals.KEY_SIZE_ATTR, keySize);
365: }
366: request.getRequest().setAttribute(Globals.KEY_SIZE_ATTR,
367: keySize);
368: // if (debug >= 2)
369: // log(" expose: Has cipher suite " + cipherSuite +
370: // " and key size " + keySize);
371:
372: // Expose ssl_session (getId)
373: byte[] ssl_session = session.getId();
374: if (ssl_session != null) {
375: StringBuffer buf = new StringBuffer("");
376: for (int x = 0; x < ssl_session.length; x++) {
377: String digit = Integer
378: .toHexString((int) ssl_session[x]);
379: if (digit.length() < 2)
380: buf.append('0');
381: if (digit.length() > 2)
382: digit = digit.substring(digit.length() - 2);
383: buf.append(digit);
384: }
385: request.getRequest()
386: .setAttribute("javax.servlet.request.ssl_session",
387: buf.toString());
388: }
389:
390: // If we have cached certificates, return them
391: Object cached = session.getValue(Globals.CERTIFICATES_ATTR);
392: if (cached != null) {
393: // if (debug >= 2)
394: // log(" expose: Has cached certificates");
395: request.getRequest().setAttribute(
396: Globals.CERTIFICATES_ATTR, cached);
397: return;
398: }
399:
400: // Convert JSSE's certificate format to the ones we need
401: X509Certificate jsseCerts[] = null;
402: java.security.cert.X509Certificate x509Certs[] = null;
403: try {
404: jsseCerts = session.getPeerCertificateChain();
405: if (jsseCerts == null)
406: jsseCerts = new X509Certificate[0];
407: x509Certs = new java.security.cert.X509Certificate[jsseCerts.length];
408: for (int i = 0; i < x509Certs.length; i++) {
409: byte buffer[] = jsseCerts[i].getEncoded();
410: CertificateFactory cf = CertificateFactory
411: .getInstance("X.509");
412: ByteArrayInputStream stream = new ByteArrayInputStream(
413: buffer);
414: x509Certs[i] = (java.security.cert.X509Certificate) cf
415: .generateCertificate(stream);
416: }
417: } catch (Throwable t) {
418: return;
419: }
420:
421: // Expose these certificates as a request attribute
422: if ((x509Certs == null) || (x509Certs.length < 1))
423: return;
424: session.putValue(Globals.CERTIFICATES_ATTR, x509Certs);
425: log(" expose: Exposing converted certificates");
426: request.getRequest().setAttribute(Globals.CERTIFICATES_ATTR,
427: x509Certs);
428:
429: }
430:
431: /**
432: * Log a message on the Logger associated with our Container (if any).
433: *
434: * @param message Message to be logged
435: */
436: protected void log(String message) {
437:
438: Logger logger = container.getLogger();
439: if (logger != null)
440: logger.log("CertificatesValve[" + container.getName()
441: + "]: " + message);
442: else
443: System.out.println("CertificatesValve["
444: + container.getName() + "]: " + message);
445:
446: }
447:
448: /**
449: * Log a message on the Logger associated with our Container (if any).
450: *
451: * @param message Message to be logged
452: * @param throwable Associated exception
453: */
454: protected void log(String message, Throwable throwable) {
455:
456: Logger logger = container.getLogger();
457: if (logger != null)
458: logger.log("CertificatesValve[" + container.getName()
459: + "]: " + message, throwable);
460: else {
461: System.out.println("CertificatesValve["
462: + container.getName() + "]: " + message);
463: throwable.printStackTrace(System.out);
464: }
465:
466: }
467:
468: /**
469: * Verify that a client certificate chain exists if our web application
470: * is doing client certificate authentication.
471: *
472: * @param request The possibly wrapped Request being processed
473: * @param actual The actual underlying Request object
474: */
475: protected void verify(Request request, Request actual) {
476:
477: // Ensure that this request came in on an SSLSocket
478: if (actual.getSocket() == null)
479: return;
480: if (!(actual.getSocket() instanceof SSLSocket))
481: return;
482: SSLSocket socket = (SSLSocket) actual.getSocket();
483:
484: // Look up the current SSLSession
485: SSLSession session = socket.getSession();
486: if (session == null)
487: return;
488: // if (debug >= 2)
489: // log(" verify: Has current SSLSession");
490:
491: // Verify that there is a client certificate chain present
492: X509Certificate jsseCerts[] = null;
493: try {
494: jsseCerts = session.getPeerCertificateChain();
495: if (jsseCerts == null)
496: jsseCerts = new X509Certificate[0];
497: } catch (SSLPeerUnverifiedException e) {
498: log(" verify: SSLPeerUnverifiedException");
499: jsseCerts = new X509Certificate[0];
500: }
501: // if (debug >= 2)
502: // log(" verify: Certificate chain has " +
503: // jsseCerts.length + " certificates");
504: if (jsseCerts.length > 0)
505: return;
506:
507: // Force a new handshake to request the client certificates
508: // if (debug >= 2)
509: // log(" verify: Invalidating current session");
510: session.invalidate();
511: // if (debug >= 2)
512: // log(" verify: Forcing new SSL handshake");
513: socket.setNeedClientAuth(true);
514: try {
515: socket.startHandshake();
516: } catch (IOException e) {
517: log(" verify: ", e);
518: }
519:
520: // Revalidate the existence of the required certificates
521: session = socket.getSession();
522: if (session == null)
523: return;
524: try {
525: jsseCerts = session.getPeerCertificateChain();
526: if (jsseCerts == null)
527: jsseCerts = new X509Certificate[0];
528: } catch (SSLPeerUnverifiedException e) {
529: log(" verify: SSLPeerUnverifiedException");
530: jsseCerts = new X509Certificate[0];
531: }
532: // if (debug >= 2)
533: // log(" verify: Certificate chain has " +
534: // jsseCerts.length + " certificates");
535:
536: }
537:
538: }
539:
540: // ------------------------------------------------------------ Private Classes
541:
542: /**
543: * Simple data class that represents the cipher being used, along with the
544: * corresponding effective key size. The specified phrase must appear in the
545: * name of the cipher suite to be recognized.
546: */
547:
548: final class CipherData {
549:
550: String phrase = null;
551:
552: int keySize = 0;
553:
554: public CipherData(String phrase, int keySize) {
555: this.phrase = phrase;
556: this.keySize = keySize;
557: }
558:
559: }
|