/*
* Copyright (c) 2002, 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.util;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.security.Principal;
import java.security.cert.*;
import java.text.Normalizer;
import javax.security.auth.x500.X500Principal;
import javax.net.ssl.SNIHostName;
import sun.security.ssl.Krb5Helper;
import sun.security.x509.X500Name;
import sun.net.util.IPAddressUtil;
import sun.security.ssl.SSLLogger;
Class to check hostnames against the names specified in a certificate as
required for TLS and LDAP.
/**
* Class to check hostnames against the names specified in a certificate as
* required for TLS and LDAP.
*
*/
public class HostnameChecker {
// Constant for a HostnameChecker for TLS
public final static byte TYPE_TLS = 1;
private final static HostnameChecker INSTANCE_TLS =
new HostnameChecker(TYPE_TLS);
// Constant for a HostnameChecker for LDAP
public final static byte TYPE_LDAP = 2;
private final static HostnameChecker INSTANCE_LDAP =
new HostnameChecker(TYPE_LDAP);
// constants for subject alt names of type DNS and IP
private final static int ALTNAME_DNS = 2;
private final static int ALTNAME_IP = 7;
// the algorithm to follow to perform the check. Currently unused.
private final byte checkType;
private HostnameChecker(byte checkType) {
this.checkType = checkType;
}
Get a HostnameChecker instance. checkType should be one of the
TYPE_* constants defined in this class.
/**
* Get a HostnameChecker instance. checkType should be one of the
* TYPE_* constants defined in this class.
*/
public static HostnameChecker getInstance(byte checkType) {
if (checkType == TYPE_TLS) {
return INSTANCE_TLS;
} else if (checkType == TYPE_LDAP) {
return INSTANCE_LDAP;
}
throw new IllegalArgumentException("Unknown check type: " + checkType);
}
Perform the check.
Params: - expectedName – the expected host name or ip address
- cert – the certificate to check against
- chainsToPublicCA – true if the certificate chains to a public
root CA (as pre-installed in the cacerts file)
Throws: - CertificateException – if the name does not match any of
the names specified in the certificate
/**
* Perform the check.
*
* @param expectedName the expected host name or ip address
* @param cert the certificate to check against
* @param chainsToPublicCA true if the certificate chains to a public
* root CA (as pre-installed in the cacerts file)
* @throws CertificateException if the name does not match any of
* the names specified in the certificate
*/
public void match(String expectedName, X509Certificate cert,
boolean chainsToPublicCA) throws CertificateException {
if (isIpAddress(expectedName)) {
matchIP(expectedName, cert);
} else {
matchDNS(expectedName, cert, chainsToPublicCA);
}
}
public void match(String expectedName, X509Certificate cert)
throws CertificateException {
match(expectedName, cert, false);
}
Perform the check for Kerberos.
/**
* Perform the check for Kerberos.
*/
public static boolean match(String expectedName, Principal principal) {
String hostName = getServerName(principal);
return (expectedName.equalsIgnoreCase(hostName));
}
Return the Server name from Kerberos principal.
/**
* Return the Server name from Kerberos principal.
*/
public static String getServerName(Principal principal) {
return Krb5Helper.getPrincipalHostName(principal);
}
Test whether the given hostname looks like a literal IPv4 or IPv6
address. The hostname does not need to be a fully qualified name.
This is not a strict check that performs full input validation.
That means if the method returns true, name need not be a correct
IP address, rather that it does not represent a valid DNS hostname.
Likewise for IP addresses when it returns false.
/**
* Test whether the given hostname looks like a literal IPv4 or IPv6
* address. The hostname does not need to be a fully qualified name.
*
* This is not a strict check that performs full input validation.
* That means if the method returns true, name need not be a correct
* IP address, rather that it does not represent a valid DNS hostname.
* Likewise for IP addresses when it returns false.
*/
private static boolean isIpAddress(String name) {
if (IPAddressUtil.isIPv4LiteralAddress(name) ||
IPAddressUtil.isIPv6LiteralAddress(name)) {
return true;
} else {
return false;
}
}
Check if the certificate allows use of the given IP address.
From RFC2818:
In some cases, the URI is specified as an IP address rather than a
hostname. In this case, the iPAddress subjectAltName must be present
in the certificate and must exactly match the IP in the URI.
/**
* Check if the certificate allows use of the given IP address.
*
* From RFC2818:
* In some cases, the URI is specified as an IP address rather than a
* hostname. In this case, the iPAddress subjectAltName must be present
* in the certificate and must exactly match the IP in the URI.
*/
private static void matchIP(String expectedIP, X509Certificate cert)
throws CertificateException {
Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames();
if (subjAltNames == null) {
throw new CertificateException
("No subject alternative names present");
}
for (List<?> next : subjAltNames) {
// For IP address, it needs to be exact match
if (((Integer)next.get(0)).intValue() == ALTNAME_IP) {
String ipAddress = (String)next.get(1);
if (expectedIP.equalsIgnoreCase(ipAddress)) {
return;
} else {
// compare InetAddress objects in order to ensure
// equality between a long IPv6 address and its
// abbreviated form.
try {
if (InetAddress.getByName(expectedIP).equals(
InetAddress.getByName(ipAddress))) {
return;
}
} catch (UnknownHostException e) {
} catch (SecurityException e) {}
}
}
}
throw new CertificateException("No subject alternative " +
"names matching " + "IP address " +
expectedIP + " found");
}
Check if the certificate allows use of the given DNS name.
From RFC2818:
If a subjectAltName extension of type dNSName is present, that MUST
be used as the identity. Otherwise, the (most specific) Common Name
field in the Subject field of the certificate MUST be used. Although
the use of the Common Name is existing practice, it is deprecated and
Certification Authorities are encouraged to use the dNSName instead.
Matching is performed using the matching rules specified by
[RFC5280]. If more than one identity of a given type is present in
the certificate (e.g., more than one dNSName name, a match in any one
of the set is considered acceptable.)
/**
* Check if the certificate allows use of the given DNS name.
*
* From RFC2818:
* If a subjectAltName extension of type dNSName is present, that MUST
* be used as the identity. Otherwise, the (most specific) Common Name
* field in the Subject field of the certificate MUST be used. Although
* the use of the Common Name is existing practice, it is deprecated and
* Certification Authorities are encouraged to use the dNSName instead.
*
* Matching is performed using the matching rules specified by
* [RFC5280]. If more than one identity of a given type is present in
* the certificate (e.g., more than one dNSName name, a match in any one
* of the set is considered acceptable.)
*/
private void matchDNS(String expectedName, X509Certificate cert,
boolean chainsToPublicCA)
throws CertificateException {
// Check that the expected name is a valid domain name.
try {
// Using the checking implemented in SNIHostName
SNIHostName sni = new SNIHostName(expectedName);
} catch (IllegalArgumentException iae) {
throw new CertificateException(
"Illegal given domain name: " + expectedName, iae);
}
Collection<List<?>> subjAltNames = cert.getSubjectAlternativeNames();
if (subjAltNames != null) {
boolean foundDNS = false;
for ( List<?> next : subjAltNames) {
if (((Integer)next.get(0)).intValue() == ALTNAME_DNS) {
foundDNS = true;
String dnsName = (String)next.get(1);
if (isMatched(expectedName, dnsName, chainsToPublicCA)) {
return;
}
}
}
if (foundDNS) {
// if certificate contains any subject alt names of type DNS
// but none match, reject
throw new CertificateException("No subject alternative DNS "
+ "name matching " + expectedName + " found.");
}
}
X500Name subjectName = getSubjectX500Name(cert);
DerValue derValue = subjectName.findMostSpecificAttribute
(X500Name.commonName_oid);
if (derValue != null) {
try {
String cname = derValue.getAsString();
if (!Normalizer.isNormalized(cname, Normalizer.Form.NFKC)) {
throw new CertificateException("Not a formal name "
+ cname);
}
if (isMatched(expectedName, cname,
chainsToPublicCA)) {
return;
}
} catch (IOException e) {
// ignore
}
}
String msg = "No name matching " + expectedName + " found";
throw new CertificateException(msg);
}
Return the subject of a certificate as X500Name, by reparsing if
necessary. X500Name should only be used if access to name components
is required, in other cases X500Principal is to be preferred.
This method is currently used from within JSSE, do not remove.
/**
* Return the subject of a certificate as X500Name, by reparsing if
* necessary. X500Name should only be used if access to name components
* is required, in other cases X500Principal is to be preferred.
*
* This method is currently used from within JSSE, do not remove.
*/
public static X500Name getSubjectX500Name(X509Certificate cert)
throws CertificateParsingException {
try {
Principal subjectDN = cert.getSubjectDN();
if (subjectDN instanceof X500Name) {
return (X500Name)subjectDN;
} else {
X500Principal subjectX500 = cert.getSubjectX500Principal();
return new X500Name(subjectX500.getEncoded());
}
} catch (IOException e) {
throw(CertificateParsingException)
new CertificateParsingException().initCause(e);
}
}
Returns true if name matches against template.
The matching is performed as per RFC 2818 rules for TLS and
RFC 2830 rules for LDAP.
The name
parameter should represent a DNS name. The
template
parameter may contain the wildcard character '*'.
/**
* Returns true if name matches against template.<p>
*
* The matching is performed as per RFC 2818 rules for TLS and
* RFC 2830 rules for LDAP.<p>
*
* The <code>name</code> parameter should represent a DNS name. The
* <code>template</code> parameter may contain the wildcard character '*'.
*/
private boolean isMatched(String name, String template,
boolean chainsToPublicCA) {
if (hasIllegalWildcard(name, template, chainsToPublicCA)) {
return false;
}
// check the validity of the domain name template.
try {
// Replacing wildcard character '*' with 'z' so as to check
// the domain name template validity.
//
// Using the checking implemented in SNIHostName
SNIHostName sni = new SNIHostName(template.replace('*', 'z'));
} catch (IllegalArgumentException iae) {
// It would be nice to add debug log if not matching.
return false;
}
if (checkType == TYPE_TLS) {
return matchAllWildcards(name, template);
} else if (checkType == TYPE_LDAP) {
return matchLeftmostWildcard(name, template);
} else {
return false;
}
}
Returns true if the template contains an illegal wildcard character.
/**
* Returns true if the template contains an illegal wildcard character.
*/
private static boolean hasIllegalWildcard(String domain, String template,
boolean chainsToPublicCA) {
// not ok if it is a single wildcard character or "*."
if (template.equals("*") || template.equals("*.")) {
if (SSLLogger.isOn) {
SSLLogger.fine(
"Certificate domain name has illegal single " +
"wildcard character: " + template);
}
return true;
}
int lastWildcardIndex = template.lastIndexOf("*");
// ok if it has no wildcard character
if (lastWildcardIndex == -1) {
return false;
}
String afterWildcard = template.substring(lastWildcardIndex);
int firstDotIndex = afterWildcard.indexOf(".");
// not ok if there is no dot after wildcard (ex: "*com")
if (firstDotIndex == -1) {
if (SSLLogger.isOn) {
SSLLogger.fine(
"Certificate domain name has illegal wildcard, " +
"no dot after wildcard character: " + template);
}
return true;
}
// If the wildcarded domain is a top-level domain under which names
// can be registered, then a wildcard is not allowed.
if (!chainsToPublicCA) {
return false; // skip check for non-public certificates
}
Optional<RegisteredDomain> rd = RegisteredDomain.from(domain)
.filter(d -> d.type() == RegisteredDomain.Type.ICANN);
if (rd.isPresent()) {
String wDomain = afterWildcard.substring(firstDotIndex + 1);
if (rd.get().publicSuffix().equalsIgnoreCase(wDomain)) {
if (SSLLogger.isOn) {
SSLLogger.fine(
"Certificate domain name has illegal " +
"wildcard for public suffix: " + template);
}
return true;
}
}
return false;
}
Returns true if name matches against template.
According to RFC 2818, section 3.1 -
Names may contain the wildcard character * which is
considered to match any single domain name component
or component fragment.
E.g., *.a.com matches foo.a.com but not
bar.foo.a.com. f*.com matches foo.com but not bar.com.
/**
* Returns true if name matches against template.<p>
*
* According to RFC 2818, section 3.1 -
* Names may contain the wildcard character * which is
* considered to match any single domain name component
* or component fragment.
* E.g., *.a.com matches foo.a.com but not
* bar.foo.a.com. f*.com matches foo.com but not bar.com.
*/
private static boolean matchAllWildcards(String name,
String template) {
name = name.toLowerCase(Locale.ENGLISH);
template = template.toLowerCase(Locale.ENGLISH);
StringTokenizer nameSt = new StringTokenizer(name, ".");
StringTokenizer templateSt = new StringTokenizer(template, ".");
if (nameSt.countTokens() != templateSt.countTokens()) {
return false;
}
while (nameSt.hasMoreTokens()) {
if (!matchWildCards(nameSt.nextToken(),
templateSt.nextToken())) {
return false;
}
}
return true;
}
Returns true if name matches against template.
As per RFC 2830, section 3.6 -
The "*" wildcard character is allowed. If present, it applies only
to the left-most name component.
E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not
bar.com.
/**
* Returns true if name matches against template.<p>
*
* As per RFC 2830, section 3.6 -
* The "*" wildcard character is allowed. If present, it applies only
* to the left-most name component.
* E.g. *.bar.com would match a.bar.com, b.bar.com, etc. but not
* bar.com.
*/
private static boolean matchLeftmostWildcard(String name,
String template) {
name = name.toLowerCase(Locale.ENGLISH);
template = template.toLowerCase(Locale.ENGLISH);
// Retreive leftmost component
int templateIdx = template.indexOf(".");
int nameIdx = name.indexOf(".");
if (templateIdx == -1)
templateIdx = template.length();
if (nameIdx == -1)
nameIdx = name.length();
if (matchWildCards(name.substring(0, nameIdx),
template.substring(0, templateIdx))) {
// match rest of the name
return template.substring(templateIdx).equals(
name.substring(nameIdx));
} else {
return false;
}
}
Returns true if the name matches against the template that may
contain wildcard char *
/**
* Returns true if the name matches against the template that may
* contain wildcard char * <p>
*/
private static boolean matchWildCards(String name, String template) {
int wildcardIdx = template.indexOf("*");
if (wildcardIdx == -1)
return name.equals(template);
boolean isBeginning = true;
String beforeWildcard = "";
String afterWildcard = template;
while (wildcardIdx != -1) {
// match in sequence the non-wildcard chars in the template.
beforeWildcard = afterWildcard.substring(0, wildcardIdx);
afterWildcard = afterWildcard.substring(wildcardIdx + 1);
int beforeStartIdx = name.indexOf(beforeWildcard);
if ((beforeStartIdx == -1) ||
(isBeginning && beforeStartIdx != 0)) {
return false;
}
isBeginning = false;
// update the match scope
name = name.substring(beforeStartIdx + beforeWildcard.length());
wildcardIdx = afterWildcard.indexOf("*");
}
return name.endsWith(afterWildcard);
}
}