001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.conn.ssl;
029
030import java.io.IOException;
031import java.io.InputStream;
032import java.security.cert.Certificate;
033import java.security.cert.X509Certificate;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.List;
037import java.util.Locale;
038
039import javax.net.ssl.SSLException;
040import javax.net.ssl.SSLSession;
041import javax.net.ssl.SSLSocket;
042import javax.security.auth.x500.X500Principal;
043
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046import org.apache.http.conn.util.InetAddressUtils;
047import org.apache.http.util.Args;
048
049/**
050 * Abstract base class for all standard {@link X509HostnameVerifier}
051 * implementations.
052 *
053 * @since 4.0
054 *
055 * @deprecated (4.4) use an implementation of {@link javax.net.ssl.HostnameVerifier} or
056 *  {@link DefaultHostnameVerifier}.
057 */
058@Deprecated
059public abstract class AbstractVerifier implements X509HostnameVerifier {
060
061    private final Log log = LogFactory.getLog(getClass());
062
063    final static String[] BAD_COUNTRY_2LDS =
064            { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
065                    "lg", "ne", "net", "or", "org" };
066
067    static {
068        // Just in case developer forgot to manually sort the array.  :-)
069        Arrays.sort(BAD_COUNTRY_2LDS);
070    }
071
072    @Override
073    public final void verify(final String host, final SSLSocket ssl)
074            throws IOException {
075        Args.notNull(host, "Host");
076        SSLSession session = ssl.getSession();
077        if(session == null) {
078            // In our experience this only happens under IBM 1.4.x when
079            // spurious (unrelated) certificates show up in the server'
080            // chain.  Hopefully this will unearth the real problem:
081            final InputStream in = ssl.getInputStream();
082            in.available();
083            /*
084              If you're looking at the 2 lines of code above because
085              you're running into a problem, you probably have two
086              options:
087
088                #1.  Clean up the certificate chain that your server
089                     is presenting (e.g. edit "/etc/apache2/server.crt"
090                     or wherever it is your server's certificate chain
091                     is defined).
092
093                                           OR
094
095                #2.   Upgrade to an IBM 1.5.x or greater JVM, or switch
096                      to a non-IBM JVM.
097            */
098
099            // If ssl.getInputStream().available() didn't cause an
100            // exception, maybe at least now the session is available?
101            session = ssl.getSession();
102            if(session == null) {
103                // If it's still null, probably a startHandshake() will
104                // unearth the real problem.
105                ssl.startHandshake();
106
107                // Okay, if we still haven't managed to cause an exception,
108                // might as well go for the NPE.  Or maybe we're okay now?
109                session = ssl.getSession();
110            }
111        }
112
113        final Certificate[] certs = session.getPeerCertificates();
114        final X509Certificate x509 = (X509Certificate) certs[0];
115        verify(host, x509);
116    }
117
118    @Override
119    public final boolean verify(final String host, final SSLSession session) {
120        try {
121            final Certificate[] certs = session.getPeerCertificates();
122            final X509Certificate x509 = (X509Certificate) certs[0];
123            verify(host, x509);
124            return true;
125        } catch(final SSLException ex) {
126            if (log.isDebugEnabled()) {
127                log.debug(ex.getMessage(), ex);
128            }
129            return false;
130        }
131    }
132
133    @Override
134    public final void verify(
135            final String host, final X509Certificate cert) throws SSLException {
136        final List<SubjectName> allSubjectAltNames = DefaultHostnameVerifier.getSubjectAltNames(cert);
137        final List<String> subjectAlts = new ArrayList<String>();
138        if (InetAddressUtils.isIPv4Address(host) || InetAddressUtils.isIPv6Address(host)) {
139            for (SubjectName subjectName: allSubjectAltNames) {
140                if (subjectName.getType() == SubjectName.IP) {
141                    subjectAlts.add(subjectName.getValue());
142                }
143            }
144        } else {
145            for (SubjectName subjectName: allSubjectAltNames) {
146                if (subjectName.getType() == SubjectName.DNS) {
147                    subjectAlts.add(subjectName.getValue());
148                }
149            }
150        }
151        final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
152        final String cn = DefaultHostnameVerifier.extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
153        verify(host,
154                cn != null ? new String[] {cn} : null,
155                subjectAlts != null && !subjectAlts.isEmpty() ? subjectAlts.toArray(new String[subjectAlts.size()]) : null);
156    }
157
158    public final void verify(final String host, final String[] cns,
159                             final String[] subjectAlts,
160                             final boolean strictWithSubDomains)
161            throws SSLException {
162
163        final String cn = cns != null && cns.length > 0 ? cns[0] : null;
164        final List<String> subjectAltList = subjectAlts != null && subjectAlts.length > 0 ? Arrays.asList(subjectAlts) : null;
165
166        final String normalizedHost = InetAddressUtils.isIPv6Address(host) ?
167                DefaultHostnameVerifier.normaliseAddress(host.toLowerCase(Locale.ROOT)) : host;
168
169        if (subjectAltList != null) {
170            for (final String subjectAlt: subjectAltList) {
171                final String normalizedAltSubject = InetAddressUtils.isIPv6Address(subjectAlt) ?
172                        DefaultHostnameVerifier.normaliseAddress(subjectAlt) : subjectAlt;
173                if (matchIdentity(normalizedHost, normalizedAltSubject, strictWithSubDomains)) {
174                    return;
175                }
176            }
177            throw new SSLException("Certificate for <" + host + "> doesn't match any " +
178                    "of the subject alternative names: " + subjectAltList);
179        } else if (cn != null) {
180            final String normalizedCN = InetAddressUtils.isIPv6Address(cn) ?
181                    DefaultHostnameVerifier.normaliseAddress(cn) : cn;
182            if (matchIdentity(normalizedHost, normalizedCN, strictWithSubDomains)) {
183                return;
184            }
185            throw new SSLException("Certificate for <" + host + "> doesn't match " +
186                    "common name of the certificate subject: " + cn);
187        } else {
188            throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
189                    "a common name and does not have alternative names");
190        }
191    }
192
193    private static boolean matchIdentity(final String host, final String identity, final boolean strict) {
194        if (host == null) {
195            return false;
196        }
197        final String normalizedHost = host.toLowerCase(Locale.ROOT);
198        final String normalizedIdentity = identity.toLowerCase(Locale.ROOT);
199        // The CN better have at least two dots if it wants wildcard
200        // action.  It also can't be [*.co.uk] or [*.co.jp] or
201        // [*.org.uk], etc...
202        final String parts[] = normalizedIdentity.split("\\.");
203        final boolean doWildcard = parts.length >= 3 && parts[0].endsWith("*") &&
204                (!strict || validCountryWildcard(parts));
205        if (doWildcard) {
206            boolean match;
207            final String firstpart = parts[0];
208            if (firstpart.length() > 1) { // e.g. server*
209                final String prefix = firstpart.substring(0, firstpart.length() - 1); // e.g. server
210                final String suffix = normalizedIdentity.substring(firstpart.length()); // skip wildcard part from cn
211                final String hostSuffix = normalizedHost.substring(prefix.length()); // skip wildcard part from normalizedHost
212                match = normalizedHost.startsWith(prefix) && hostSuffix.endsWith(suffix);
213            } else {
214                match = normalizedHost.endsWith(normalizedIdentity.substring(1));
215            }
216            return match && (!strict || countDots(normalizedHost) == countDots(normalizedIdentity));
217        } else {
218            return normalizedHost.equals(normalizedIdentity);
219        }
220    }
221
222    private static boolean validCountryWildcard(final String parts[]) {
223        if (parts.length != 3 || parts[2].length() != 2) {
224            return true; // it's not an attempt to wildcard a 2TLD within a country code
225        }
226        return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
227    }
228
229    public static boolean acceptableCountryWildcard(final String cn) {
230        return validCountryWildcard(cn.split("\\."));
231    }
232
233    public static String[] getCNs(final X509Certificate cert) {
234        final String subjectPrincipal = cert.getSubjectX500Principal().toString();
235        try {
236            final String cn = DefaultHostnameVerifier.extractCN(subjectPrincipal);
237            return cn != null ? new String[] { cn } : null;
238        } catch (final SSLException ex) {
239            return null;
240        }
241    }
242
243    /**
244     * Extracts the array of SubjectAlt DNS names from an X509Certificate.
245     * Returns null if there aren't any.
246     * <p>
247     * Note:  Java doesn't appear able to extract international characters
248     * from the SubjectAlts.  It can only extract international characters
249     * from the CN field.
250     * </p>
251     * <p>
252     * (Or maybe the version of OpenSSL I'm using to test isn't storing the
253     * international characters correctly in the SubjectAlts?).
254     * </p>
255     *
256     * @param cert X509Certificate
257     * @return Array of SubjectALT DNS names stored in the certificate.
258     */
259    public static String[] getDNSSubjectAlts(final X509Certificate cert) {
260        final List<SubjectName> subjectAltNames = DefaultHostnameVerifier.getSubjectAltNames(cert);
261        if (subjectAltNames == null) {
262            return null;
263        }
264        final List<String> dnsAlts = new ArrayList<String>();
265        for (SubjectName subjectName: subjectAltNames) {
266            if (subjectName.getType() == SubjectName.DNS) {
267                dnsAlts.add(subjectName.getValue());
268            }
269        }
270        return dnsAlts.isEmpty() ? dnsAlts.toArray(new String[dnsAlts.size()]) : null;
271    }
272
273    /**
274     * Counts the number of dots "." in a string.
275     * @param s  string to count dots from
276     * @return  number of dots
277     */
278    public static int countDots(final String s) {
279        int count = 0;
280        for(int i = 0; i < s.length(); i++) {
281            if(s.charAt(i) == '.') {
282                count++;
283            }
284        }
285        return count;
286    }
287
288}