/*
* 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.InputStream;
import java.io.IOException;
import java.net.IDN;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import java.security.Principal;
import java.security.cert.*;
import java.text.Normalizer;
import javax.security.auth.x500.X500Principal;
import sun.net.idn.Punycode;
import sun.net.idn.StringPrep;
import sun.net.util.IPAddressUtil;
import sun.security.ssl.Krb5Helper;
import sun.security.x509.X500Name;
import sun.text.normalizer.UCharacterIterator;
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.
Throws: - CertificateException – if the name does not match any of
the names specified in the certificate
/**
* Perform the check.
*
* @exception CertificateException if the name does not match any of
* the names specified in the certificate
*/
public void match(String expectedName, X509Certificate cert)
throws CertificateException {
if (isIpAddress(expectedName)) {
matchIP(expectedName, cert);
} else {
matchDNS(expectedName, cert);
}
}
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
[RFC2459]. 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
* [RFC2459]. 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)
throws CertificateException {
// Check that the expected name is a valid domain name.
try {
// Using the checking taken from OpenJDK 8's SNIHostName
checkHostName(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)) {
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)) {
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) {
// 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 taken from OpenJDK 8's SNIHostName
checkHostName(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 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);
}
// check the validity of the string hostname
private static void checkHostName(String hostname) {
hostname = toASCII(Objects.requireNonNull(hostname,
"Server name value of host_name cannot be null"),
IDN.USE_STD3_ASCII_RULES);
// Check it can be encoded to ASCII
hostname.getBytes(StandardCharsets.US_ASCII);
if (hostname.isEmpty()) {
throw new IllegalArgumentException(
"Server name value of host_name cannot be empty");
}
if (hostname.endsWith(".")) {
throw new IllegalArgumentException(
"Server name value of host_name cannot have the trailing dot");
}
}
/*
* Local versions of toASCII(String,int), toASCIIInternal(String, int)
* and their helper methods with 8020842 fix added. Can't change the
* public version due to compatibility.
*/
// ACE Prefix is "xn--"
private static final String ACE_PREFIX = "xn--";
private static final int ACE_PREFIX_LENGTH = ACE_PREFIX.length();
private static final int MAX_LABEL_LENGTH = 63;
// single instance of nameprep
private static StringPrep namePrep = null;
static {
InputStream stream = null;
try {
final String IDN_PROFILE = "uidna.spp";
if (System.getSecurityManager() != null) {
stream = AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
public InputStream run() {
return StringPrep.class.getResourceAsStream(IDN_PROFILE);
}
});
} else {
stream = StringPrep.class.getResourceAsStream(IDN_PROFILE);
}
namePrep = new StringPrep(stream);
stream.close();
} catch (IOException e) {
// should never reach here
assert false;
}
}
Translates a string from Unicode to ASCII Compatible Encoding (ACE),
as defined by the ToASCII operation of RFC 3490.
ToASCII operation can fail. ToASCII fails if any step of it fails.
If ToASCII operation fails, an IllegalArgumentException will be thrown.
In this case, the input string should not be used in an internationalized domain name.
A label is an individual part of a domain name. The original ToASCII operation,
as defined in RFC 3490, only operates on a single label. This method can handle
both label and entire domain name, by assuming that labels in a domain name are
always separated by dots. The following characters are recognized as dots:
\u002E (full stop), \u3002 (ideographic full stop), \uFF0E (fullwidth full stop),
and \uFF61 (halfwidth ideographic full stop). if dots are
used as label separators, this method also changes all of them to \u002E (full stop)
in output translated string.
Params: - input – the string to be processed
- flag – process flag; can be 0 or any logical OR of possible flags
Throws: - IllegalArgumentException – if the input string doesn't conform to RFC 3490 specification
Returns: the translated String
/**
* Translates a string from Unicode to ASCII Compatible Encoding (ACE),
* as defined by the ToASCII operation of <a href="http://www.ietf.org/rfc/rfc3490.txt">RFC 3490</a>.
*
* <p>ToASCII operation can fail. ToASCII fails if any step of it fails.
* If ToASCII operation fails, an IllegalArgumentException will be thrown.
* In this case, the input string should not be used in an internationalized domain name.
*
* <p> A label is an individual part of a domain name. The original ToASCII operation,
* as defined in RFC 3490, only operates on a single label. This method can handle
* both label and entire domain name, by assuming that labels in a domain name are
* always separated by dots. The following characters are recognized as dots:
* \u002E (full stop), \u3002 (ideographic full stop), \uFF0E (fullwidth full stop),
* and \uFF61 (halfwidth ideographic full stop). if dots are
* used as label separators, this method also changes all of them to \u002E (full stop)
* in output translated string.
*
* @param input the string to be processed
* @param flag process flag; can be 0 or any logical OR of possible flags
*
* @return the translated {@code String}
*
* @throws IllegalArgumentException if the input string doesn't conform to RFC 3490 specification
*/
private static String toASCII(String input, int flag)
{
int p = 0, q = 0;
StringBuffer out = new StringBuffer();
if (isRootLabel(input)) {
return ".";
}
while (p < input.length()) {
q = searchDots(input, p);
out.append(toASCIIInternal(input.substring(p, q), flag));
if (q != (input.length())) {
// has more labels, or keep the trailing dot as at present
out.append('.');
}
p = q + 1;
}
return out.toString();
}
//
// toASCII operation; should only apply to a single label
//
private static String toASCIIInternal(String label, int flag)
{
// step 1
// Check if the string contains code points outside the ASCII range 0..0x7c.
boolean isASCII = isAllASCII(label);
StringBuffer dest;
// step 2
// perform the nameprep operation; flag ALLOW_UNASSIGNED is used here
if (!isASCII) {
UCharacterIterator iter = UCharacterIterator.getInstance(label);
try {
dest = namePrep.prepare(iter, flag);
} catch (java.text.ParseException e) {
throw new IllegalArgumentException(e);
}
} else {
dest = new StringBuffer(label);
}
// step 8, move forward to check the smallest number of the code points
// the length must be inside 1..63
if (dest.length() == 0) {
throw new IllegalArgumentException(
"Empty label is not a legal name");
}
// step 3
// Verify the absence of non-LDH ASCII code points
// 0..0x2c, 0x2e..0x2f, 0x3a..0x40, 0x5b..0x60, 0x7b..0x7f
// Verify the absence of leading and trailing hyphen
boolean useSTD3ASCIIRules = ((flag & IDN.USE_STD3_ASCII_RULES) != 0);
if (useSTD3ASCIIRules) {
for (int i = 0; i < dest.length(); i++) {
int c = dest.charAt(i);
if (isNonLDHAsciiCodePoint(c)) {
throw new IllegalArgumentException(
"Contains non-LDH ASCII characters");
}
}
if (dest.charAt(0) == '-' ||
dest.charAt(dest.length() - 1) == '-') {
throw new IllegalArgumentException(
"Has leading or trailing hyphen");
}
}
if (!isASCII) {
// step 4
// If all code points are inside 0..0x7f, skip to step 8
if (!isAllASCII(dest.toString())) {
// step 5
// verify the sequence does not begin with ACE prefix
if(!startsWithACEPrefix(dest)){
// step 6
// encode the sequence with punycode
try {
dest = Punycode.encode(dest, null);
} catch (java.text.ParseException e) {
throw new IllegalArgumentException(e);
}
dest = toASCIILower(dest);
// step 7
// prepend the ACE prefix
dest.insert(0, ACE_PREFIX);
} else {
throw new IllegalArgumentException("The input starts with the ACE Prefix");
}
}
}
// step 8
// the length must be inside 1..63
if (dest.length() > MAX_LABEL_LENGTH) {
throw new IllegalArgumentException("The label in the input is too long");
}
return dest.toString();
}
//
// LDH stands for "letter/digit/hyphen", with characters restricted to the
// 26-letter Latin alphabet <A-Z a-z>, the digits <0-9>, and the hyphen
// <->.
// Non LDH refers to characters in the ASCII range, but which are not
// letters, digits or the hypen.
//
// non-LDH = 0..0x2C, 0x2E..0x2F, 0x3A..0x40, 0x5B..0x60, 0x7B..0x7F
//
private static boolean isNonLDHAsciiCodePoint(int ch){
return (0x0000 <= ch && ch <= 0x002C) ||
(0x002E <= ch && ch <= 0x002F) ||
(0x003A <= ch && ch <= 0x0040) ||
(0x005B <= ch && ch <= 0x0060) ||
(0x007B <= ch && ch <= 0x007F);
}
//
// search dots in a string and return the index of that character;
// or if there is no dots, return the length of input string
// dots might be: \u002E (full stop), \u3002 (ideographic full stop), \uFF0E (fullwidth full stop),
// and \uFF61 (halfwidth ideographic full stop).
//
private static int searchDots(String s, int start) {
int i;
for (i = start; i < s.length(); i++) {
if (isLabelSeparator(s.charAt(i))) {
break;
}
}
return i;
}
//
// to check if a string is a root label, ".".
//
private static boolean isRootLabel(String s) {
return (s.length() == 1 && isLabelSeparator(s.charAt(0)));
}
//
// to check if a character is a label separator, i.e. a dot character.
//
private static boolean isLabelSeparator(char c) {
return (c == '.' || c == '\u3002' || c == '\uFF0E' || c == '\uFF61');
}
//
// to check if a string starts with ACE-prefix
//
private static boolean startsWithACEPrefix(StringBuffer input){
boolean startsWithPrefix = true;
if(input.length() < ACE_PREFIX_LENGTH){
return false;
}
for(int i = 0; i < ACE_PREFIX_LENGTH; i++){
if(toASCIILower(input.charAt(i)) != ACE_PREFIX.charAt(i)){
startsWithPrefix = false;
}
}
return startsWithPrefix;
}
private static char toASCIILower(char ch){
if('A' <= ch && ch <= 'Z'){
return (char)(ch + 'a' - 'A');
}
return ch;
}
private static StringBuffer toASCIILower(StringBuffer input){
StringBuffer dest = new StringBuffer();
for(int i = 0; i < input.length();i++){
dest.append(toASCIILower(input.charAt(i)));
}
return dest;
}
//
// to check if a string only contains US-ASCII code point
//
private static boolean isAllASCII(String input) {
boolean isASCII = true;
for (int i = 0; i < input.length(); i++) {
int c = input.charAt(i);
if (c > 0x7F) {
isASCII = false;
break;
}
}
return isASCII;
}
}