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.net.InetAddress;
031import java.net.UnknownHostException;
032import java.security.cert.Certificate;
033import java.security.cert.CertificateParsingException;
034import java.security.cert.X509Certificate;
035import java.util.ArrayList;
036import java.util.Collection;
037import java.util.Collections;
038import java.util.List;
039import java.util.Locale;
040import java.util.NoSuchElementException;
041
042import javax.naming.InvalidNameException;
043import javax.naming.NamingException;
044import javax.naming.directory.Attribute;
045import javax.naming.directory.Attributes;
046import javax.naming.ldap.LdapName;
047import javax.naming.ldap.Rdn;
048import javax.net.ssl.HostnameVerifier;
049import javax.net.ssl.SSLException;
050import javax.net.ssl.SSLPeerUnverifiedException;
051import javax.net.ssl.SSLSession;
052import javax.security.auth.x500.X500Principal;
053
054import org.apache.commons.logging.Log;
055import org.apache.commons.logging.LogFactory;
056import org.apache.http.annotation.Contract;
057import org.apache.http.annotation.ThreadingBehavior;
058import org.apache.http.conn.util.DomainType;
059import org.apache.http.conn.util.InetAddressUtils;
060import org.apache.http.conn.util.PublicSuffixMatcher;
061
062/**
063 * Default {@link javax.net.ssl.HostnameVerifier} implementation.
064 *
065 * @since 4.4
066 */
067@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
068public final class DefaultHostnameVerifier implements HostnameVerifier {
069
070    enum HostNameType {
071
072        IPv4(7), IPv6(7), DNS(2);
073
074        final int subjectType;
075
076        HostNameType(final int subjectType) {
077            this.subjectType = subjectType;
078        }
079
080    }
081
082    private final Log log = LogFactory.getLog(getClass());
083
084    private final PublicSuffixMatcher publicSuffixMatcher;
085
086    public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) {
087        this.publicSuffixMatcher = publicSuffixMatcher;
088    }
089
090    public DefaultHostnameVerifier() {
091        this(null);
092    }
093
094    @Override
095    public boolean verify(final String host, final SSLSession session) {
096        try {
097            final Certificate[] certs = session.getPeerCertificates();
098            final X509Certificate x509 = (X509Certificate) certs[0];
099            verify(host, x509);
100            return true;
101        } catch (final SSLException ex) {
102            if (log.isDebugEnabled()) {
103                log.debug(ex.getMessage(), ex);
104            }
105            return false;
106        }
107    }
108
109    public void verify(
110            final String host, final X509Certificate cert) throws SSLException {
111        final HostNameType hostType = determineHostFormat(host);
112        final List<SubjectName> subjectAlts = getSubjectAltNames(cert);
113        if (subjectAlts != null && !subjectAlts.isEmpty()) {
114            switch (hostType) {
115                case IPv4:
116                    matchIPAddress(host, subjectAlts);
117                    break;
118                case IPv6:
119                    matchIPv6Address(host, subjectAlts);
120                    break;
121                default:
122                    matchDNSName(host, subjectAlts, this.publicSuffixMatcher);
123            }
124        } else {
125            // CN matching has been deprecated by rfc2818 and can be used
126            // as fallback only when no subjectAlts are available
127            final X500Principal subjectPrincipal = cert.getSubjectX500Principal();
128            final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253));
129            if (cn == null) {
130                throw new SSLException("Certificate subject for <" + host + "> doesn't contain " +
131                        "a common name and does not have alternative names");
132            }
133            matchCN(host, cn, this.publicSuffixMatcher);
134        }
135    }
136
137    static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) throws SSLException {
138        for (int i = 0; i < subjectAlts.size(); i++) {
139            final SubjectName subjectAlt = subjectAlts.get(i);
140            if (subjectAlt.getType() == SubjectName.IP) {
141                if (host.equals(subjectAlt.getValue())) {
142                    return;
143                }
144            }
145        }
146        throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
147                "of the subject alternative names: " + subjectAlts);
148    }
149
150    static void matchIPv6Address(final String host, final List<SubjectName> subjectAlts) throws SSLException {
151        final String normalisedHost = normaliseAddress(host);
152        for (int i = 0; i < subjectAlts.size(); i++) {
153            final SubjectName subjectAlt = subjectAlts.get(i);
154            if (subjectAlt.getType() == SubjectName.IP) {
155                final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue());
156                if (normalisedHost.equals(normalizedSubjectAlt)) {
157                    return;
158                }
159            }
160        }
161        throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
162                "of the subject alternative names: " + subjectAlts);
163    }
164
165    static void matchDNSName(final String host, final List<SubjectName> subjectAlts,
166                             final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
167        final String normalizedHost = host.toLowerCase(Locale.ROOT);
168        for (int i = 0; i < subjectAlts.size(); i++) {
169            final SubjectName subjectAlt = subjectAlts.get(i);
170            if (subjectAlt.getType() == SubjectName.DNS) {
171                final String normalizedSubjectAlt = subjectAlt.getValue().toLowerCase(Locale.ROOT);
172                if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) {
173                    return;
174                }
175            }
176        }
177        throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " +
178                "of the subject alternative names: " + subjectAlts);
179    }
180
181    static void matchCN(final String host, final String cn,
182                 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException {
183        final String normalizedHost = host.toLowerCase(Locale.ROOT);
184        final String normalizedCn = cn.toLowerCase(Locale.ROOT);
185        if (!matchIdentityStrict(normalizedHost, normalizedCn, publicSuffixMatcher)) {
186            throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " +
187                    "common name of the certificate subject: " + cn);
188        }
189    }
190
191    static boolean matchDomainRoot(final String host, final String domainRoot) {
192        if (domainRoot == null) {
193            return false;
194        }
195        return host.endsWith(domainRoot) && (host.length() == domainRoot.length()
196                || host.charAt(host.length() - domainRoot.length() - 1) == '.');
197    }
198
199    private static boolean matchIdentity(final String host, final String identity,
200                                         final PublicSuffixMatcher publicSuffixMatcher,
201                                         final boolean strict) {
202        if (publicSuffixMatcher != null && host.contains(".")) {
203            if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, DomainType.ICANN))) {
204                return false;
205            }
206        }
207
208        // RFC 2818, 3.1. Server Identity
209        // "...Names may contain the wildcard
210        // character * which is considered to match any single domain name
211        // component or component fragment..."
212        // Based on this statement presuming only singular wildcard is legal
213        final int asteriskIdx = identity.indexOf('*');
214        if (asteriskIdx != -1) {
215            final String prefix = identity.substring(0, asteriskIdx);
216            final String suffix = identity.substring(asteriskIdx + 1);
217            if (!prefix.isEmpty() && !host.startsWith(prefix)) {
218                return false;
219            }
220            if (!suffix.isEmpty() && !host.endsWith(suffix)) {
221                return false;
222            }
223            // Additional sanity checks on content selected by wildcard can be done here
224            if (strict) {
225                final String remainder = host.substring(
226                        prefix.length(), host.length() - suffix.length());
227                if (remainder.contains(".")) {
228                    return false;
229                }
230            }
231            return true;
232        }
233        return host.equalsIgnoreCase(identity);
234    }
235
236    static boolean matchIdentity(final String host, final String identity,
237                                 final PublicSuffixMatcher publicSuffixMatcher) {
238        return matchIdentity(host, identity, publicSuffixMatcher, false);
239    }
240
241    static boolean matchIdentity(final String host, final String identity) {
242        return matchIdentity(host, identity, null, false);
243    }
244
245    static boolean matchIdentityStrict(final String host, final String identity,
246                                       final PublicSuffixMatcher publicSuffixMatcher) {
247        return matchIdentity(host, identity, publicSuffixMatcher, true);
248    }
249
250    static boolean matchIdentityStrict(final String host, final String identity) {
251        return matchIdentity(host, identity, null, true);
252    }
253
254    static String extractCN(final String subjectPrincipal) throws SSLException {
255        if (subjectPrincipal == null) {
256            return null;
257        }
258        try {
259            final LdapName subjectDN = new LdapName(subjectPrincipal);
260            final List<Rdn> rdns = subjectDN.getRdns();
261            for (int i = rdns.size() - 1; i >= 0; i--) {
262                final Rdn rds = rdns.get(i);
263                final Attributes attributes = rds.toAttributes();
264                final Attribute cn = attributes.get("cn");
265                if (cn != null) {
266                    try {
267                        final Object value = cn.get();
268                        if (value != null) {
269                            return value.toString();
270                        }
271                    } catch (final NoSuchElementException ignore) {
272                        // ignore exception
273                    } catch (final NamingException ignore) {
274                        // ignore exception
275                    }
276                }
277            }
278            return null;
279        } catch (final InvalidNameException e) {
280            throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name");
281        }
282    }
283
284    static HostNameType determineHostFormat(final String host) {
285        if (InetAddressUtils.isIPv4Address(host)) {
286            return HostNameType.IPv4;
287        } else {
288            String s = host;
289            if (s.startsWith("[") && s.endsWith("]")) {
290                s = host.substring(1, host.length() - 1);
291            }
292            if (InetAddressUtils.isIPv6Address(s)) {
293                return HostNameType.IPv6;
294            }
295        }
296        return HostNameType.DNS;
297    }
298
299    static List<SubjectName> getSubjectAltNames(final X509Certificate cert) {
300        try {
301            final Collection<List<?>> entries = cert.getSubjectAlternativeNames();
302            if (entries == null) {
303                return Collections.emptyList();
304            }
305            final List<SubjectName> result = new ArrayList<SubjectName>();
306            for (List<?> entry: entries) {
307                final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null;
308                if (type != null) {
309                    final String s = (String) entry.get(1);
310                    result.add(new SubjectName(s, type));
311                }
312            }
313            return result;
314        } catch (final CertificateParsingException ignore) {
315            return Collections.emptyList();
316        }
317    }
318
319    /*
320     * Normalize IPv6 or DNS name.
321     */
322    static String normaliseAddress(final String hostname) {
323        if (hostname == null) {
324            return hostname;
325        }
326        try {
327            final InetAddress inetAddress = InetAddress.getByName(hostname);
328            return inetAddress.getHostAddress();
329        } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above
330            return hostname;
331        }
332    }
333}