| /* |
| * ==================================================================== |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| * ==================================================================== |
| * |
| * This software consists of voluntary contributions made by many |
| * individuals on behalf of the Apache Software Foundation. For more |
| * information on the Apache Software Foundation, please see |
| * <http://www.apache.org/>. |
| * |
| */ |
| |
| package org.apache.hc.client5.http.ssl; |
| |
| import java.net.InetAddress; |
| import java.net.UnknownHostException; |
| import java.security.cert.Certificate; |
| import java.security.cert.CertificateParsingException; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Locale; |
| |
| import javax.net.ssl.SSLException; |
| import javax.net.ssl.SSLPeerUnverifiedException; |
| import javax.net.ssl.SSLSession; |
| import javax.security.auth.x500.X500Principal; |
| |
| import org.apache.hc.client5.http.psl.DomainType; |
| import org.apache.hc.client5.http.psl.PublicSuffixMatcher; |
| import org.apache.hc.core5.annotation.Contract; |
| import org.apache.hc.core5.annotation.ThreadingBehavior; |
| import org.apache.hc.core5.http.NameValuePair; |
| import org.apache.hc.core5.net.InetAddressUtils; |
| import org.apache.hc.core5.util.TextUtils; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * Default {@link javax.net.ssl.HostnameVerifier} implementation. |
| * |
| * @since 4.4 |
| */ |
| @Contract(threading = ThreadingBehavior.STATELESS) |
| public final class DefaultHostnameVerifier implements HttpClientHostnameVerifier { |
| |
| enum HostNameType { |
| |
| IPv4(7), IPv6(7), DNS(2); |
| |
| final int subjectType; |
| |
| HostNameType(final int subjectType) { |
| this.subjectType = subjectType; |
| } |
| |
| } |
| |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| |
| private final PublicSuffixMatcher publicSuffixMatcher; |
| |
| public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) { |
| this.publicSuffixMatcher = publicSuffixMatcher; |
| } |
| |
| public DefaultHostnameVerifier() { |
| this(null); |
| } |
| |
| @Override |
| public boolean verify(final String host, final SSLSession session) { |
| try { |
| final Certificate[] certs = session.getPeerCertificates(); |
| final X509Certificate x509 = (X509Certificate) certs[0]; |
| verify(host, x509); |
| return true; |
| } catch (final SSLException ex) { |
| if (log.isDebugEnabled()) { |
| log.debug(ex.getMessage(), ex); |
| } |
| return false; |
| } |
| } |
| |
| @Override |
| public void verify( |
| final String host, final X509Certificate cert) throws SSLException { |
| final HostNameType hostType = determineHostFormat(host); |
| final List<SubjectName> subjectAlts = getSubjectAltNames(cert); |
| if (subjectAlts != null && !subjectAlts.isEmpty()) { |
| switch (hostType) { |
| case IPv4: |
| matchIPAddress(host, subjectAlts); |
| break; |
| case IPv6: |
| matchIPv6Address(host, subjectAlts); |
| break; |
| default: |
| matchDNSName(host, subjectAlts, this.publicSuffixMatcher); |
| } |
| } else { |
| // CN matching has been deprecated by rfc2818 and can be used |
| // as fallback only when no subjectAlts are available |
| final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); |
| final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); |
| if (cn == null) { |
| throw new SSLException("Certificate subject for <" + host + "> doesn't contain " + |
| "a common name and does not have alternative names"); |
| } |
| matchCN(host, cn, this.publicSuffixMatcher); |
| } |
| } |
| |
| static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) throws SSLException { |
| for (int i = 0; i < subjectAlts.size(); i++) { |
| final SubjectName subjectAlt = subjectAlts.get(i); |
| if (subjectAlt.getType() == SubjectName.IP) { |
| if (host.equals(subjectAlt.getValue())) { |
| return; |
| } |
| } |
| } |
| throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " + |
| "of the subject alternative names: " + subjectAlts); |
| } |
| |
| static void matchIPv6Address(final String host, final List<SubjectName> subjectAlts) throws SSLException { |
| final String normalisedHost = normaliseAddress(host); |
| for (int i = 0; i < subjectAlts.size(); i++) { |
| final SubjectName subjectAlt = subjectAlts.get(i); |
| if (subjectAlt.getType() == SubjectName.IP) { |
| final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue()); |
| if (normalisedHost.equals(normalizedSubjectAlt)) { |
| return; |
| } |
| } |
| } |
| throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " + |
| "of the subject alternative names: " + subjectAlts); |
| } |
| |
| static void matchDNSName(final String host, final List<SubjectName> subjectAlts, |
| final PublicSuffixMatcher publicSuffixMatcher) throws SSLException { |
| final String normalizedHost = host.toLowerCase(Locale.ROOT); |
| for (int i = 0; i < subjectAlts.size(); i++) { |
| final SubjectName subjectAlt = subjectAlts.get(i); |
| if (subjectAlt.getType() == SubjectName.DNS) { |
| final String normalizedSubjectAlt = subjectAlt.getValue().toLowerCase(Locale.ROOT); |
| if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher, DomainType.ICANN)) { |
| return; |
| } |
| } |
| } |
| throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " + |
| "of the subject alternative names: " + subjectAlts); |
| } |
| |
| static void matchCN(final String host, final String cn, |
| final PublicSuffixMatcher publicSuffixMatcher) throws SSLException { |
| final String normalizedHost = host.toLowerCase(Locale.ROOT); |
| final String normalizedCn = cn.toLowerCase(Locale.ROOT); |
| if (!matchIdentityStrict(normalizedHost, normalizedCn, publicSuffixMatcher, DomainType.ICANN)) { |
| throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " + |
| "common name of the certificate subject: " + cn); |
| } |
| } |
| |
| static boolean matchDomainRoot(final String host, final String domainRoot) { |
| if (domainRoot == null) { |
| return false; |
| } |
| return host.endsWith(domainRoot) && (host.length() == domainRoot.length() |
| || host.charAt(host.length() - domainRoot.length() - 1) == '.'); |
| } |
| |
| private static boolean matchIdentity(final String host, final String identity, |
| final PublicSuffixMatcher publicSuffixMatcher, |
| final DomainType domainType, |
| final boolean strict) { |
| if (publicSuffixMatcher != null && host.contains(".")) { |
| if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, domainType))) { |
| return false; |
| } |
| } |
| |
| // RFC 2818, 3.1. Server Identity |
| // "...Names may contain the wildcard |
| // character * which is considered to match any single domain name |
| // component or component fragment..." |
| // Based on this statement presuming only singular wildcard is legal |
| final int asteriskIdx = identity.indexOf('*'); |
| if (asteriskIdx != -1) { |
| final String prefix = identity.substring(0, asteriskIdx); |
| final String suffix = identity.substring(asteriskIdx + 1); |
| if (!prefix.isEmpty() && !host.startsWith(prefix)) { |
| return false; |
| } |
| if (!suffix.isEmpty() && !host.endsWith(suffix)) { |
| return false; |
| } |
| // Additional sanity checks on content selected by wildcard can be done here |
| if (strict) { |
| final String remainder = host.substring( |
| prefix.length(), host.length() - suffix.length()); |
| if (remainder.contains(".")) { |
| return false; |
| } |
| } |
| return true; |
| } |
| return host.equalsIgnoreCase(identity); |
| } |
| |
| static boolean matchIdentity(final String host, final String identity, |
| final PublicSuffixMatcher publicSuffixMatcher) { |
| return matchIdentity(host, identity, publicSuffixMatcher, null, false); |
| } |
| |
| static boolean matchIdentity(final String host, final String identity) { |
| return matchIdentity(host, identity, null, null, false); |
| } |
| |
| static boolean matchIdentityStrict(final String host, final String identity, |
| final PublicSuffixMatcher publicSuffixMatcher) { |
| return matchIdentity(host, identity, publicSuffixMatcher, null, true); |
| } |
| |
| static boolean matchIdentityStrict(final String host, final String identity) { |
| return matchIdentity(host, identity, null, null, true); |
| } |
| |
| static boolean matchIdentity(final String host, final String identity, |
| final PublicSuffixMatcher publicSuffixMatcher, |
| final DomainType domainType) { |
| return matchIdentity(host, identity, publicSuffixMatcher, domainType, false); |
| } |
| |
| static boolean matchIdentityStrict(final String host, final String identity, |
| final PublicSuffixMatcher publicSuffixMatcher, |
| final DomainType domainType) { |
| return matchIdentity(host, identity, publicSuffixMatcher, domainType, true); |
| } |
| |
| static String extractCN(final String subjectPrincipal) throws SSLException { |
| if (subjectPrincipal == null) { |
| return null; |
| } |
| final List<NameValuePair> attributes = DistinguishedNameParser.INSTANCE.parse(subjectPrincipal); |
| for (final NameValuePair attribute: attributes) { |
| if (TextUtils.isBlank(attribute.getName()) || attribute.getValue() == null) { |
| throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name"); |
| } |
| if (attribute.getName().equalsIgnoreCase("cn")) { |
| return attribute.getValue(); |
| } |
| } |
| return null; |
| } |
| |
| static HostNameType determineHostFormat(final String host) { |
| if (InetAddressUtils.isIPv4Address(host)) { |
| return HostNameType.IPv4; |
| } |
| String s = host; |
| if (s.startsWith("[") && s.endsWith("]")) { |
| s = host.substring(1, host.length() - 1); |
| } |
| if (InetAddressUtils.isIPv6Address(s)) { |
| return HostNameType.IPv6; |
| } |
| return HostNameType.DNS; |
| } |
| |
| static List<SubjectName> getSubjectAltNames(final X509Certificate cert) { |
| try { |
| final Collection<List<?>> entries = cert.getSubjectAlternativeNames(); |
| if (entries == null) { |
| return Collections.emptyList(); |
| } |
| final List<SubjectName> result = new ArrayList<>(); |
| for (final List<?> entry : entries) { |
| final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null; |
| if (type != null) { |
| if (type == SubjectName.DNS || type == SubjectName.IP) { |
| final Object o = entry.get(1); |
| if (o instanceof String) { |
| result.add(new SubjectName((String) o, type)); |
| } else if (o instanceof byte[]) { |
| // TODO ASN.1 DER encoded form |
| } |
| } |
| } |
| } |
| return result; |
| } catch (final CertificateParsingException ignore) { |
| return Collections.emptyList(); |
| } |
| } |
| |
| /* |
| * Normalize IPv6 or DNS name. |
| */ |
| static String normaliseAddress(final String hostname) { |
| if (hostname == null) { |
| return hostname; |
| } |
| try { |
| final InetAddress inetAddress = InetAddress.getByName(hostname); |
| return inetAddress.getHostAddress(); |
| } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above |
| return hostname; |
| } |
| } |
| } |