001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one
003: * or more contributor license agreements. See the NOTICE file
004: * distributed with this work for additional information
005: * regarding copyright ownership. The ASF licenses this file
006: * to you under the Apache License, Version 2.0 (the
007: * "License"); you may not use this file except in compliance
008: * with the License. You may obtain a copy of the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing,
013: * software distributed under the License is distributed on an
014: * * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015: * KIND, either express or implied. See the License for the
016: * specific language governing permissions and limitations
017: * under the License.
018: */
019: package org.apache.synapse.transport.nhttp;
020:
021: import javax.net.ssl.SSLException;
022: import javax.net.ssl.SSLPeerUnverifiedException;
023: import javax.net.ssl.SSLSession;
024: import javax.net.ssl.SSLSocket;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.security.cert.Certificate;
028: import java.security.cert.X509Certificate;
029: import java.security.cert.CertificateParsingException;
030: import java.util.*;
031:
032: /**
033: * ************************************************************************
034: * Copied from the not-yet-commons-ssl project at http://juliusdavies.ca/commons-ssl/
035: * As the above project is accepted into Apache and its JARs become available in
036: * the Maven 2 repos, we will have to switch to using the JARs instead
037: * ************************************************************************
038: * <p/>
039: * Interface for checking if a hostname matches the names stored inside the
040: * server's X.509 certificate. Correctly implements
041: * javax.net.ssl.HostnameVerifier, but that interface is not recommended.
042: * Instead we added several check() methods that take SSLSocket,
043: * or X509Certificate, or ultimately (they all end up calling this one),
044: * String. (It's easier to supply JUnit with Strings instead of mock
045: * SSLSession objects!)
046: * </p><p>Our check() methods throw exceptions if the name is
047: * invalid, whereas javax.net.ssl.HostnameVerifier just returns true/false.
048: * <p/>
049: * We provide the HostnameVerifier.DEFAULT, HostnameVerifier.STRICT, and
050: * HostnameVerifier.ALLOW_ALL implementations. We also provide the more
051: * specialized HostnameVerifier.DEFAULT_AND_LOCALHOST, as well as
052: * HostnameVerifier.STRICT_IE6. But feel free to define your own
053: * implementations!
054: * <p/>
055: * Inspired by Sebastian Hauer's original StrictSSLProtocolSocketFactory in the
056: * HttpClient "contrib" repository.
057: *
058: * @author Julius Davies
059: * @author <a href="mailto:hauer@psicode.com">Sebastian Hauer</a>
060: * @since 8-Dec-2006
061: */
062:
063: public interface HostnameVerifier extends
064: javax.net.ssl.HostnameVerifier {
065:
066: boolean verify(String host, SSLSession session);
067:
068: void check(String host, SSLSocket ssl) throws IOException;
069:
070: void check(String host, X509Certificate cert) throws SSLException;
071:
072: void check(String host, String[] cns, String[] subjectAlts)
073: throws SSLException;
074:
075: void check(String[] hosts, SSLSocket ssl) throws IOException;
076:
077: void check(String[] hosts, X509Certificate cert)
078: throws SSLException;
079:
080: /**
081: * Checks to see if the supplied hostname matches any of the supplied CNs
082: * or "DNS" Subject-Alts. Most implementations only look at the first CN,
083: * and ignore any additional CNs. Most implementations do look at all of
084: * the "DNS" Subject-Alts. The CNs or Subject-Alts may contain wildcards
085: * according to RFC 2818.
086: *
087: * @param cns CN fields, in order, as extracted from the X.509
088: * certificate.
089: * @param subjectAlts Subject-Alt fields of type 2 ("DNS"), as extracted
090: * from the X.509 certificate.
091: * @param hosts The array of hostnames to verify.
092: * @throws SSLException If verification failed.
093: */
094: void check(String[] hosts, String[] cns, String[] subjectAlts)
095: throws SSLException;
096:
097: /**
098: * The DEFAULT HostnameVerifier works the same way as Curl and Firefox.
099: * <p/>
100: * The hostname must match either the first CN, or any of the subject-alts.
101: * A wildcard can occur in the CN, and in any of the subject-alts.
102: * <p/>
103: * The only difference between DEFAULT and STRICT is that a wildcard (such
104: * as "*.foo.com") with DEFAULT matches all subdomains, including
105: * "a.b.foo.com".
106: */
107: public final static HostnameVerifier DEFAULT = new AbstractVerifier() {
108: public final void check(final String[] hosts,
109: final String[] cns, final String[] subjectAlts)
110: throws SSLException {
111: check(hosts, cns, subjectAlts, false, false);
112: }
113:
114: public final String toString() {
115: return "DEFAULT";
116: }
117: };
118:
119: /**
120: * The DEFAULT_AND_LOCALHOST HostnameVerifier works like the DEFAULT
121: * one with one additional relaxation: a host of "localhost",
122: * "localhost.localdomain", "127.0.0.1", "::1" will always pass, no matter
123: * what is in the server's certificate.
124: */
125: public final static HostnameVerifier DEFAULT_AND_LOCALHOST = new AbstractVerifier() {
126: public final void check(final String[] hosts,
127: final String[] cns, final String[] subjectAlts)
128: throws SSLException {
129: if (isLocalhost(hosts[0])) {
130: return;
131: }
132: check(hosts, cns, subjectAlts, false, false);
133: }
134:
135: public final String toString() {
136: return "DEFAULT_AND_LOCALHOST";
137: }
138: };
139:
140: /**
141: * The STRICT HostnameVerifier works the same way as java.net.URL in Sun
142: * Java 1.4, Sun Java 5, Sun Java 6. It's also pretty close to IE6.
143: * This implementation appears to be compliant with RFC 2818 for dealing
144: * with wildcards.
145: * <p/>
146: * The hostname must match either the first CN, or any of the subject-alts.
147: * A wildcard can occur in the CN, and in any of the subject-alts. The
148: * one divergence from IE6 is how we only check the first CN. IE6 allows
149: * a match against any of the CNs present. We decided to follow in
150: * Sun Java 1.4's footsteps and only check the first CN.
151: * <p/>
152: * A wildcard such as "*.foo.com" matches only subdomains in the same
153: * level, for example "a.foo.com". It does not match deeper subdomains
154: * such as "a.b.foo.com".
155: */
156: public final static HostnameVerifier STRICT = new AbstractVerifier() {
157: public final void check(final String[] host,
158: final String[] cns, final String[] subjectAlts)
159: throws SSLException {
160: check(host, cns, subjectAlts, false, true);
161: }
162:
163: public final String toString() {
164: return "STRICT";
165: }
166: };
167:
168: /**
169: * The STRICT_IE6 HostnameVerifier works just like the STRICT one with one
170: * minor variation: the hostname can match against any of the CN's in the
171: * server's certificate, not just the first one. This behaviour is
172: * identical to IE6's behaviour.
173: */
174: public final static HostnameVerifier STRICT_IE6 = new AbstractVerifier() {
175: public final void check(final String[] host,
176: final String[] cns, final String[] subjectAlts)
177: throws SSLException {
178: check(host, cns, subjectAlts, true, true);
179: }
180:
181: public final String toString() {
182: return "STRICT_IE6";
183: }
184: };
185:
186: /**
187: * The ALLOW_ALL HostnameVerifier essentially turns hostname verification
188: * off. This implementation is a no-op, and never throws the SSLException.
189: */
190: public final static HostnameVerifier ALLOW_ALL = new AbstractVerifier() {
191: public final void check(final String[] host,
192: final String[] cns, final String[] subjectAlts) {
193: // Allow everything - so never blowup.
194: }
195:
196: public final String toString() {
197: return "ALLOW_ALL";
198: }
199: };
200:
201: abstract class AbstractVerifier implements HostnameVerifier {
202:
203: /**
204: * This contains a list of 2nd-level domains that aren't allowed to
205: * have wildcards when combined with country-codes.
206: * For example: [*.co.uk].
207: * <p/>
208: * The [*.co.uk] problem is an interesting one. Should we just hope
209: * that CA's would never foolishly allow such a certificate to happen?
210: * Looks like we're the only implementation guarding against this.
211: * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
212: */
213: private final static String[] BAD_COUNTRY_2LDS = { "ac", "co",
214: "com", "ed", "edu", "go", "gouv", "gov", "info", "lg",
215: "ne", "net", "or", "org" };
216:
217: private final static String[] LOCALHOSTS = { "::1",
218: "127.0.0.1", "localhost", "localhost.localdomain" };
219:
220: static {
221: // Just in case developer forgot to manually sort the array. :-)
222: Arrays.sort(BAD_COUNTRY_2LDS);
223: Arrays.sort(LOCALHOSTS);
224: }
225:
226: protected AbstractVerifier() {
227: }
228:
229: /**
230: * The javax.net.ssl.HostnameVerifier contract.
231: *
232: * @param host 'hostname' we used to create our socket
233: * @param session SSLSession with the remote server
234: * @return true if the host matched the one in the certificate.
235: */
236: public boolean verify(String host, SSLSession session) {
237: try {
238: Certificate[] certs = session.getPeerCertificates();
239: X509Certificate x509 = (X509Certificate) certs[0];
240: check(new String[] { host }, x509);
241: return true;
242: } catch (SSLException e) {
243: return false;
244: }
245: }
246:
247: public void check(String host, SSLSocket ssl)
248: throws IOException {
249: check(new String[] { host }, ssl);
250: }
251:
252: public void check(String host, X509Certificate cert)
253: throws SSLException {
254: check(new String[] { host }, cert);
255: }
256:
257: public void check(String host, String[] cns,
258: String[] subjectAlts) throws SSLException {
259: check(new String[] { host }, cns, subjectAlts);
260: }
261:
262: public void check(String host[], SSLSocket ssl)
263: throws IOException {
264: if (host == null) {
265: throw new NullPointerException("host to verify is null");
266: }
267:
268: SSLSession session = ssl.getSession();
269: if (session == null) {
270: // In our experience this only happens under IBM 1.4.x when
271: // spurious (unrelated) certificates show up in the server'
272: // chain. Hopefully this will unearth the real problem:
273: InputStream in = ssl.getInputStream();
274: in.available();
275: /*
276: If you're looking at the 2 lines of code above because
277: you're running into a problem, you probably have two
278: options:
279:
280: #1. Clean up the certificate chain that your server
281: is presenting (e.g. edit "/etc/apache2/server.crt"
282: or wherever it is your server's certificate chain
283: is defined).
284:
285: OR
286:
287: #2. Upgrade to an IBM 1.5.x or greater JVM, or switch
288: to a non-IBM JVM.
289: */
290:
291: // If ssl.getInputStream().available() didn't cause an
292: // exception, maybe at least now the session is available?
293: session = ssl.getSession();
294: if (session == null) {
295: // If it's still null, probably a startHandshake() will
296: // unearth the real problem.
297: ssl.startHandshake();
298:
299: // Okay, if we still haven't managed to cause an exception,
300: // might as well go for the NPE. Or maybe we're okay now?
301: session = ssl.getSession();
302: }
303: }
304: Certificate[] certs;
305: try {
306: certs = session.getPeerCertificates();
307: } catch (SSLPeerUnverifiedException spue) {
308: InputStream in = ssl.getInputStream();
309: in.available();
310: // Didn't trigger anything interesting? Okay, just throw
311: // original.
312: throw spue;
313: }
314: X509Certificate x509 = (X509Certificate) certs[0];
315: check(host, x509);
316: }
317:
318: public void check(String[] host, X509Certificate cert)
319: throws SSLException {
320:
321: String[] cns = Certificates.getCNs(cert);
322: String[] subjectAlts = Certificates.getDNSSubjectAlts(cert);
323: check(host, cns, subjectAlts);
324:
325: }
326:
327: public void check(final String[] hosts, final String[] cns,
328: final String[] subjectAlts, final boolean ie6,
329: final boolean strictWithSubDomains) throws SSLException {
330: // Build up lists of allowed hosts For logging/debugging purposes.
331: StringBuffer buf = new StringBuffer(32);
332: buf.append('<');
333: for (int i = 0; i < hosts.length; i++) {
334: String h = hosts[i];
335: h = h != null ? h.trim().toLowerCase() : "";
336: hosts[i] = h;
337: if (i > 0) {
338: buf.append('/');
339: }
340: buf.append(h);
341: }
342: buf.append('>');
343: String hostnames = buf.toString();
344: // Build the list of names we're going to check. Our DEFAULT and
345: // STRICT implementations of the HostnameVerifier only use the
346: // first CN provided. All other CNs are ignored.
347: // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
348: TreeSet names = new TreeSet();
349: if (cns != null && cns.length > 0 && cns[0] != null) {
350: names.add(cns[0]);
351: if (ie6) {
352: for (int i = 1; i < cns.length; i++) {
353: names.add(cns[i]);
354: }
355: }
356: }
357: if (subjectAlts != null) {
358: for (int i = 0; i < subjectAlts.length; i++) {
359: if (subjectAlts[i] != null) {
360: names.add(subjectAlts[i]);
361: }
362: }
363: }
364: if (names.isEmpty()) {
365: String msg = "Certificate for " + hosts[0]
366: + " doesn't contain CN or DNS subjectAlt";
367: throw new SSLException(msg);
368: }
369:
370: // StringBuffer for building the error message.
371: buf = new StringBuffer();
372:
373: boolean match = false;
374: out: for (Iterator it = names.iterator(); it.hasNext();) {
375: // Don't trim the CN, though!
376: String cn = (String) it.next();
377: cn = cn.toLowerCase();
378: // Store CN in StringBuffer in case we need to report an error.
379: buf.append(" <");
380: buf.append(cn);
381: buf.append('>');
382: if (it.hasNext()) {
383: buf.append(" OR");
384: }
385:
386: // The CN better have at least two dots if it wants wildcard
387: // action. It also can't be [*.co.uk] or [*.co.jp] or
388: // [*.org.uk], etc...
389: boolean doWildcard = cn.startsWith("*.")
390: && cn.lastIndexOf('.') >= 0
391: && !isIP4Address(cn)
392: && acceptableCountryWildcard(cn);
393:
394: for (int i = 0; i < hosts.length; i++) {
395: final String hostName = hosts[i].trim()
396: .toLowerCase();
397: if (doWildcard) {
398: match = hostName.endsWith(cn.substring(1));
399: if (match && strictWithSubDomains) {
400: // If we're in strict mode, then [*.foo.com] is not
401: // allowed to match [a.b.foo.com]
402: match = countDots(hostName) == countDots(cn);
403: }
404: } else {
405: match = hostName.equals(cn);
406: }
407: if (match) {
408: break out;
409: }
410: }
411: }
412: if (!match) {
413: throw new SSLException(
414: "hostname in certificate didn't match: "
415: + hostnames + " !=" + buf);
416: }
417: }
418:
419: public static boolean isIP4Address(final String cn) {
420: boolean isIP4 = true;
421: String tld = cn;
422: int x = cn.lastIndexOf('.');
423: // We only bother analyzing the characters after the final dot
424: // in the name.
425: if (x >= 0 && x + 1 < cn.length()) {
426: tld = cn.substring(x + 1);
427: }
428: for (int i = 0; i < tld.length(); i++) {
429: if (!Character.isDigit(tld.charAt(0))) {
430: isIP4 = false;
431: break;
432: }
433: }
434: return isIP4;
435: }
436:
437: public static boolean acceptableCountryWildcard(final String cn) {
438: int cnLen = cn.length();
439: if (cnLen >= 7 && cnLen <= 9) {
440: // Look for the '.' in the 3rd-last position:
441: if (cn.charAt(cnLen - 3) == '.') {
442: // Trim off the [*.] and the [.XX].
443: String s = cn.substring(2, cnLen - 3);
444: // And test against the sorted array of bad 2lds:
445: int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s);
446: return x < 0;
447: }
448: }
449: return true;
450: }
451:
452: public static boolean isLocalhost(String host) {
453: host = host != null ? host.trim().toLowerCase() : "";
454: if (host.startsWith("::1")) {
455: int x = host.lastIndexOf('%');
456: if (x >= 0) {
457: host = host.substring(0, x);
458: }
459: }
460: int x = Arrays.binarySearch(LOCALHOSTS, host);
461: return x >= 0;
462: }
463:
464: /**
465: * Counts the number of dots "." in a string.
466: *
467: * @param s string to count dots from
468: * @return number of dots
469: */
470: public static int countDots(final String s) {
471: int count = 0;
472: for (int i = 0; i < s.length(); i++) {
473: if (s.charAt(i) == '.') {
474: count++;
475: }
476: }
477: return count;
478: }
479: }
480:
481: class Certificates {
482: public static String[] getCNs(X509Certificate cert) {
483: LinkedList cnList = new LinkedList();
484: /*
485: Sebastian Hauer's original StrictSSLProtocolSocketFactory used
486: getName() and had the following comment:
487:
488: Parses a X.500 distinguished name for the value of the
489: "Common Name" field. This is done a bit sloppy right
490: now and should probably be done a bit more according to
491: <code>RFC 2253</code>.
492:
493: I've noticed that toString() seems to do a better job than
494: getName() on these X500Principal objects, so I'm hoping that
495: addresses Sebastian's concern.
496:
497: For example, getName() gives me this:
498: 1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
499:
500: whereas toString() gives me this:
501: EMAILADDRESS=juliusdavies@cucbc.com
502:
503: Looks like toString() even works with non-ascii domain names!
504: I tested it with "花子.co.jp" and it worked fine.
505: */
506: String subjectPrincipal = cert.getSubjectX500Principal()
507: .toString();
508: StringTokenizer st = new StringTokenizer(subjectPrincipal,
509: ",");
510: while (st.hasMoreTokens()) {
511: String tok = st.nextToken();
512: int x = tok.indexOf("CN=");
513: if (x >= 0) {
514: cnList.add(tok.substring(x + 3));
515: }
516: }
517: if (!cnList.isEmpty()) {
518: String[] cns = new String[cnList.size()];
519: cnList.toArray(cns);
520: return cns;
521: } else {
522: return null;
523: }
524: }
525:
526: /**
527: * Extracts the array of SubjectAlt DNS names from an X509Certificate.
528: * Returns null if there aren't any.
529: * <p/>
530: * Note: Java doesn't appear able to extract international characters
531: * from the SubjectAlts. It can only extract international characters
532: * from the CN field.
533: * <p/>
534: * (Or maybe the version of OpenSSL I'm using to test isn't storing the
535: * international characters correctly in the SubjectAlts?).
536: *
537: * @param cert X509Certificate
538: * @return Array of SubjectALT DNS names stored in the certificate.
539: */
540: public static String[] getDNSSubjectAlts(X509Certificate cert) {
541: LinkedList subjectAltList = new LinkedList();
542: Collection c = null;
543: try {
544: c = cert.getSubjectAlternativeNames();
545: } catch (CertificateParsingException cpe) {
546: // Should probably log.debug() this?
547: cpe.printStackTrace();
548: }
549: if (c != null) {
550: Iterator it = c.iterator();
551: while (it.hasNext()) {
552: List list = (List) it.next();
553: int type = ((Integer) list.get(0)).intValue();
554: // If type is 2, then we've got a dNSName
555: if (type == 2) {
556: String s = (String) list.get(1);
557: subjectAltList.add(s);
558: }
559: }
560: }
561: if (!subjectAltList.isEmpty()) {
562: String[] subjectAlts = new String[subjectAltList.size()];
563: subjectAltList.toArray(subjectAlts);
564: return subjectAlts;
565: } else {
566: return null;
567: }
568: }
569: }
570: }
|