All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nl.altindag.ssl.hostnameverifier.FenixHostnameVerifier Maven / Gradle / Ivy

Go to download

High level library to configure a SSLContext and other properties to enable SSL/TLS connection

There is a newer version: 8.3.7
Show newest version
/*
 *  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.
 */
package nl.altindag.ssl.hostnameverifier;

import nl.altindag.ssl.util.internal.StringUtils;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import java.nio.charset.StandardCharsets;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

import static nl.altindag.ssl.hostnameverifier.HostnameCommon.canParseAsIpAddress;
import static nl.altindag.ssl.hostnameverifier.Hostnames.toCanonicalHost;

/**
 * NOTE:
 * Please don't use this class directly as it is part of the internal API. Class name and methods can be changed any time.
 * Instead use the {@link nl.altindag.ssl.util.HostnameVerifierUtils HostnameVerifierUtils} which provides the same functionality
 * while it has a stable API because it is part of the public API.
 * 

* This verifier does not accept addresses in the subjectDN:commonName attribute * and it will not verify names or wildcards against the Public Suffix List. * Therefore use of wildcards is not limited to subdomains. *

* * This HostnameVerifier is copied from OkHttp library, see here for the original content: * - https://github.com/square/okhttp/blob/69ae7f3e10dae0554f3181edaa52bcd77ee448ab/okhttp/src/jvmMain/kotlin/okhttp3/internal/tls/OkHostnameVerifier.kt#L1 */ public final class FenixHostnameVerifier implements HostnameVerifier { private static final HostnameVerifier INSTANCE = new FenixHostnameVerifier(); private static final int ALT_DNS_NAME = 2; private static final int ALT_IPA_NAME = 7; private FenixHostnameVerifier() {} public static HostnameVerifier getInstance() { return INSTANCE; } @Override public boolean verify(String host, SSLSession sslSession) { if (!isAscii(host)) { return false; } Optional peerCertificate = getPeerCertificate(sslSession); return peerCertificate.isPresent() && verify(host, peerCertificate.get()); } /** * Returns true if the [String] is ASCII encoded. */ private boolean isAscii(String value) { return StandardCharsets.US_ASCII.newEncoder().canEncode(value); } /** * Returns the first certificate from the peer certificates if present. */ private Optional getPeerCertificate(SSLSession sslSession) { try { return Arrays.stream(sslSession.getPeerCertificates()) .filter(Objects::nonNull) .filter(X509Certificate.class::isInstance) .map(X509Certificate.class::cast) .findFirst(); } catch (SSLPeerUnverifiedException e) { return Optional.empty(); } } private boolean verify(String host, X509Certificate certificate) { if (canParseAsIpAddress(host)) { return verifyIpAddress(host, certificate); } else { return verifyHostname(host, certificate); } } /** * Returns true if [certificate] matches [hostname]. */ private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) { String canonicalIpAddress = toCanonicalHost(ipAddress); if (canonicalIpAddress == null) { return false; } List subjectAltNames = getSubjectAltNames(certificate, ALT_IPA_NAME); return subjectAltNames.stream().anyMatch(subjectAltName -> canonicalIpAddress.equals(toCanonicalHost(subjectAltName))); } private List getSubjectAltNames(X509Certificate certificate, int type) { try { Collection> subjectAlternativeNames = Optional.ofNullable(certificate.getSubjectAlternativeNames()) .orElseGet(Collections::emptyList); return subjectAlternativeNames.stream() .filter(Objects::nonNull) .filter(subjectAlternativeName -> subjectAlternativeName.size() == 2) .filter(subjectAlternativeName -> subjectAlternativeName.get(0) instanceof Integer && ((Integer) subjectAlternativeName.get(0)) == type) .map(subjectAlternativeName -> subjectAlternativeName.get(1)) .filter(String.class::isInstance) .map(String.class::cast) .collect(Collectors.toList()); } catch (CertificateParsingException exception) { return Collections.emptyList(); } } private boolean verifyHostname(String hostname, X509Certificate certificate) { List subjectAltNames = getSubjectAltNames(certificate, ALT_DNS_NAME); return subjectAltNames.stream().anyMatch(subjectAltName -> verifyHostname(hostname, subjectAltName)); } /** * Returns true if [hostname] matches the domain name pattern. * * @param hostname lower-case host name. * @param domainNamePattern domain name pattern from certificate. Maybe a wildcard pattern such as * `*.android.com`. */ private boolean verifyHostname(String hostname, String domainNamePattern) { if (isHostnameInValid(hostname) || isHostnameInValid(domainNamePattern)) { return false; } String resultingHostname = toAbsolute(hostname); String resultingDomainNamePattern = toAbsolute(domainNamePattern); // Hostname and pattern are now absolute domain names. resultingHostname = asciiToLowercase(resultingHostname); resultingDomainNamePattern = asciiToLowercase(resultingDomainNamePattern); // Hostname and pattern are now in lower case -- domain names are case-insensitive. if (!resultingDomainNamePattern.contains("*")) { // Not a wildcard pattern -- hostname and pattern must match exactly. return resultingHostname.equals(resultingDomainNamePattern); } // Wildcard pattern return verifyWildcardPattern(resultingHostname, resultingDomainNamePattern); } private boolean isHostnameInValid(String hostname) { return StringUtils.isBlank(hostname) || hostname.startsWith(".") || hostname.endsWith(".."); } /** * Normalize hostname by turning it into absolute domain names if it is not * yet absolute. This is needed because server certificates do not normally contain absolute * names, but they should be treated as absolute. At the same time, any hostname * presented to this method should also be treated as absolute for the purposes of matching * to the server certificate. * www.android.com matches www.android.com * www.android.com matches www.android.com. * www.android.com. matches www.android.com. * www.android.com. matches www.android.com */ String toAbsolute(String hostname) { String absoluteHostname = hostname; if (!absoluteHostname.endsWith(".")) { absoluteHostname += "."; } return absoluteHostname; } /** * This is like [toLowerCase] except that it does nothing if this contains any non-ASCII * characters. We want to avoid lower casing special chars like U+212A (Kelvin symbol) because * they can return ASCII characters that match real hostnames. */ private String asciiToLowercase(String value) { return isAscii(value) ? value.toLowerCase(Locale.US) : value; } /** * WILDCARD PATTERN RULES: * 1. Asterisk (*) is only permitted in the left-most domain name label and must be the * only character in that label (i.e., must match the whole left-most label). * For example, *.example.com is permitted, while *a.example.com, a*.example.com, * a*b.example.com, a.*.example.com are not permitted. * 2. Asterisk (*) cannot match across domain name labels. * For example, *.example.com matches test.example.com but does not match * sub.test.example.com. * 3. Wildcard patterns for single-label domain names are not permitted. */ @SuppressWarnings("RedundantIfStatement") private boolean verifyWildcardPattern(String hostname, String domainNamePattern) { if (!domainNamePattern.startsWith("*.") || domainNamePattern.indexOf("*", 1) != -1) { // Asterisk (*) is only permitted in the left-most domain name label and must be the only // character in that label return false; } // Optimization: check whether hostname is too short to match the pattern. hostName must be at // least as long as the pattern because asterisk must match the whole left-most label and // hostname starts with a non-empty label. Thus, asterisk has to match one or more characters. if (hostname.length() < domainNamePattern.length()) { return false; // Hostname too short to match the pattern. } if ("*.".equals(domainNamePattern)) { return false; // Wildcard pattern for single-label domain name -- not permitted. } // Hostname must end with the region of pattern following the asterisk. String suffix = domainNamePattern.substring(1); if (!hostname.endsWith(suffix)) { return false; // Hostname does not end with the suffix. } // Check that asterisk did not match across domain name labels. int suffixStartIndexInHostname = hostname.length() - domainNamePattern.length(); if (suffixStartIndexInHostname > 0 && hostname.lastIndexOf(".", suffixStartIndexInHostname - 1) != -1) { return false; // Asterisk is matching across domain name labels -- not permitted. } // Hostname matches pattern. return true; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy