Created
January 7, 2018 15:01
-
-
Save swankjesse/461a53ee3078bdaa574e3d4e48aeaf07 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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 okhttp3.recipes; | |
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 java.util.regex.Pattern; | |
import javax.net.ssl.HostnameVerifier; | |
import javax.net.ssl.SSLException; | |
import javax.net.ssl.SSLSession; | |
import javax.security.auth.x500.X500Principal; | |
/** | |
* A HostnameVerifier that accepts hostnames in both the standard location (subjectAltName) | |
* and the obsolete CNAME location, which deprecated in RFC 2818 (May 2000). | |
*/ | |
public final class LegacyHostnameVerifier implements HostnameVerifier { | |
public static final LegacyHostnameVerifier INSTANCE = new LegacyHostnameVerifier(); | |
private static final int ALT_DNS_NAME = 2; | |
private static final int ALT_IPA_NAME = 7; | |
private LegacyHostnameVerifier() { | |
} | |
@Override | |
public boolean verify(String host, SSLSession session) { | |
try { | |
Certificate[] certificates = session.getPeerCertificates(); | |
return verify(host, (X509Certificate) certificates[0]); | |
} catch (SSLException e) { | |
return false; | |
} | |
} | |
public boolean verify(String host, X509Certificate certificate) { | |
return verifyAsIpAddress(host) | |
? verifyIpAddress(host, certificate) | |
: verifyHostname(host, certificate); | |
} | |
/** Returns true if {@code certificate} matches {@code ipAddress}. */ | |
private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) { | |
List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME); | |
for (int i = 0, size = altNames.size(); i < size; i++) { | |
if (ipAddress.equalsIgnoreCase(altNames.get(i))) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** Returns true if {@code certificate} matches {@code hostname}. */ | |
private boolean verifyHostname(String hostname, X509Certificate certificate) { | |
hostname = hostname.toLowerCase(Locale.US); | |
boolean hasDns = false; | |
List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME); | |
for (int i = 0, size = altNames.size(); i < size; i++) { | |
hasDns = true; | |
if (verifyHostname(hostname, altNames.get(i))) { | |
return true; | |
} | |
} | |
if (!hasDns) { | |
X500Principal principal = certificate.getSubjectX500Principal(); | |
// RFC 2818 advises using the most specific name for matching. | |
String cn = new DistinguishedNameParser(principal).findMostSpecific("cn"); | |
if (cn != null) { | |
return verifyHostname(hostname, cn); | |
} | |
} | |
return false; | |
} | |
private static List<String> getSubjectAltNames(X509Certificate certificate, int type) { | |
List<String> result = new ArrayList<>(); | |
try { | |
Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames(); | |
if (subjectAltNames == null) { | |
return Collections.emptyList(); | |
} | |
for (Object subjectAltName : subjectAltNames) { | |
List<?> entry = (List<?>) subjectAltName; | |
if (entry == null || entry.size() < 2) { | |
continue; | |
} | |
Integer altNameType = (Integer) entry.get(0); | |
if (altNameType == null) { | |
continue; | |
} | |
if (altNameType == type) { | |
String altName = (String) entry.get(1); | |
if (altName != null) { | |
result.add(altName); | |
} | |
} | |
} | |
return result; | |
} catch (CertificateParsingException e) { | |
return Collections.emptyList(); | |
} | |
} | |
/** | |
* Returns {@code true} iff {@code hostname} matches the domain name {@code pattern}. | |
* | |
* @param hostname lower-case host name. | |
* @param pattern domain name pattern from certificate. May be a wildcard pattern such as {@code | |
* *.android.com}. | |
*/ | |
public boolean verifyHostname(String hostname, String pattern) { | |
// Basic sanity checks | |
// Check length == 0 instead of .isEmpty() to support Java 5. | |
if ((hostname == null) || (hostname.length() == 0) || (hostname.startsWith(".")) | |
|| (hostname.endsWith(".."))) { | |
// Invalid domain name | |
return false; | |
} | |
if ((pattern == null) || (pattern.length() == 0) || (pattern.startsWith(".")) | |
|| (pattern.endsWith(".."))) { | |
// Invalid pattern/domain name | |
return false; | |
} | |
// Normalize hostname and pattern by turning them into absolute domain names if they are not | |
// yet absolute. This is needed because server certificates do not normally contain absolute | |
// names or patterns, 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 | |
if (!hostname.endsWith(".")) { | |
hostname += '.'; | |
} | |
if (!pattern.endsWith(".")) { | |
pattern += '.'; | |
} | |
// hostname and pattern are now absolute domain names. | |
pattern = pattern.toLowerCase(Locale.US); | |
// hostname and pattern are now in lower case -- domain names are case-insensitive. | |
if (!pattern.contains("*")) { | |
// Not a wildcard pattern -- hostname and pattern must match exactly. | |
return hostname.equals(pattern); | |
} | |
// Wildcard pattern | |
// 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. | |
if ((!pattern.startsWith("*.")) || (pattern.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() < pattern.length()) { | |
// hostname too short to match the pattern. | |
return false; | |
} | |
if ("*.".equals(pattern)) { | |
// Wildcard pattern for single-label domain name -- not permitted. | |
return false; | |
} | |
// hostname must end with the region of pattern following the asterisk. | |
String suffix = pattern.substring(1); | |
if (!hostname.endsWith(suffix)) { | |
// hostname does not end with the suffix | |
return false; | |
} | |
// Check that asterisk did not match across domain name labels. | |
int suffixStartIndexInHostname = hostname.length() - suffix.length(); | |
if ((suffixStartIndexInHostname > 0) | |
&& (hostname.lastIndexOf('.', suffixStartIndexInHostname - 1) != -1)) { | |
// Asterisk is matching across domain name labels -- not permitted. | |
return false; | |
} | |
// hostname matches pattern | |
return true; | |
} | |
/** | |
* A distinguished name (DN) parser. This parser only supports extracting a string value from a | |
* DN. It doesn't support values in the hex-string style. | |
*/ | |
static final class DistinguishedNameParser { | |
private final String dn; | |
private final int length; | |
private int pos; | |
private int beg; | |
private int end; | |
/** Temporary variable to store positions of the currently parsed item. */ | |
private int cur; | |
/** Distinguished name characters. */ | |
private char[] chars; | |
DistinguishedNameParser(X500Principal principal) { | |
// RFC2253 is used to ensure we get attributes in the reverse | |
// order of the underlying ASN.1 encoding, so that the most | |
// significant values of repeated attributes occur first. | |
this.dn = principal.getName(X500Principal.RFC2253); | |
this.length = this.dn.length(); | |
} | |
// gets next attribute type: (ALPHA 1*keychar) / oid | |
private String nextAT() { | |
// skip preceding space chars, they can present after | |
// comma or semicolon (compatibility with RFC 1779) | |
for (; pos < length && chars[pos] == ' '; pos++) { | |
} | |
if (pos == length) { | |
return null; // reached the end of DN | |
} | |
// mark the beginning of attribute type | |
beg = pos; | |
// attribute type chars | |
pos++; | |
for (; pos < length && chars[pos] != '=' && chars[pos] != ' '; pos++) { | |
// we don't follow exact BNF syntax here: | |
// accept any char except space and '=' | |
} | |
if (pos >= length) { | |
throw new IllegalStateException("Unexpected end of DN: " + dn); | |
} | |
// mark the end of attribute type | |
end = pos; | |
// skip trailing space chars between attribute type and '=' | |
// (compatibility with RFC 1779) | |
if (chars[pos] == ' ') { | |
for (; pos < length && chars[pos] != '=' && chars[pos] == ' '; pos++) { | |
} | |
if (chars[pos] != '=' || pos == length) { | |
throw new IllegalStateException("Unexpected end of DN: " + dn); | |
} | |
} | |
pos++; //skip '=' char | |
// skip space chars between '=' and attribute value | |
// (compatibility with RFC 1779) | |
for (; pos < length && chars[pos] == ' '; pos++) { | |
} | |
// in case of oid attribute type skip its prefix: "oid." or "OID." | |
// (compatibility with RFC 1779) | |
if ((end - beg > 4) && (chars[beg + 3] == '.') | |
&& (chars[beg] == 'O' || chars[beg] == 'o') | |
&& (chars[beg + 1] == 'I' || chars[beg + 1] == 'i') | |
&& (chars[beg + 2] == 'D' || chars[beg + 2] == 'd')) { | |
beg += 4; | |
} | |
return new String(chars, beg, end - beg); | |
} | |
// gets quoted attribute value: QUOTATION *( quotechar / pair ) QUOTATION | |
private String quotedAV() { | |
pos++; | |
beg = pos; | |
end = beg; | |
while (true) { | |
if (pos == length) { | |
throw new IllegalStateException("Unexpected end of DN: " + dn); | |
} | |
if (chars[pos] == '"') { | |
// enclosing quotation was found | |
pos++; | |
break; | |
} else if (chars[pos] == '\\') { | |
chars[end] = getEscaped(); | |
} else { | |
// shift char: required for string with escaped chars | |
chars[end] = chars[pos]; | |
} | |
pos++; | |
end++; | |
} | |
// skip trailing space chars before comma or semicolon. | |
// (compatibility with RFC 1779) | |
for (; pos < length && chars[pos] == ' '; pos++) { | |
} | |
return new String(chars, beg, end - beg); | |
} | |
// gets hex string attribute value: "#" hexstring | |
private String hexAV() { | |
if (pos + 4 >= length) { | |
// encoded byte array must be not less then 4 c | |
throw new IllegalStateException("Unexpected end of DN: " + dn); | |
} | |
beg = pos; // store '#' position | |
pos++; | |
while (true) { | |
// check for end of attribute value | |
// looks for space and component separators | |
if (pos == length || chars[pos] == '+' || chars[pos] == ',' | |
|| chars[pos] == ';') { | |
end = pos; | |
break; | |
} | |
if (chars[pos] == ' ') { | |
end = pos; | |
pos++; | |
// skip trailing space chars before comma or semicolon. | |
// (compatibility with RFC 1779) | |
for (; pos < length && chars[pos] == ' '; pos++) { | |
} | |
break; | |
} else if (chars[pos] >= 'A' && chars[pos] <= 'F') { | |
chars[pos] += 32; //to low case | |
} | |
pos++; | |
} | |
// verify length of hex string | |
// encoded byte array must be not less then 4 and must be even number | |
int hexLen = end - beg; // skip first '#' char | |
if (hexLen < 5 || (hexLen & 1) == 0) { | |
throw new IllegalStateException("Unexpected end of DN: " + dn); | |
} | |
// get byte encoding from string representation | |
byte[] encoded = new byte[hexLen / 2]; | |
for (int i = 0, p = beg + 1; i < encoded.length; p += 2, i++) { | |
encoded[i] = (byte) getByte(p); | |
} | |
return new String(chars, beg, hexLen); | |
} | |
// gets string attribute value: *( stringchar / pair ) | |
private String escapedAV() { | |
beg = pos; | |
end = pos; | |
while (true) { | |
if (pos >= length) { | |
// the end of DN has been found | |
return new String(chars, beg, end - beg); | |
} | |
switch (chars[pos]) { | |
case '+': | |
case ',': | |
case ';': | |
// separator char has been found | |
return new String(chars, beg, end - beg); | |
case '\\': | |
// escaped char | |
chars[end++] = getEscaped(); | |
pos++; | |
break; | |
case ' ': | |
// need to figure out whether space defines | |
// the end of attribute value or not | |
cur = end; | |
pos++; | |
chars[end++] = ' '; | |
for (; pos < length && chars[pos] == ' '; pos++) { | |
chars[end++] = ' '; | |
} | |
if (pos == length || chars[pos] == ',' || chars[pos] == '+' | |
|| chars[pos] == ';') { | |
// separator char or the end of DN has been found | |
return new String(chars, beg, cur - beg); | |
} | |
break; | |
default: | |
chars[end++] = chars[pos]; | |
pos++; | |
} | |
} | |
} | |
// returns escaped char | |
private char getEscaped() { | |
pos++; | |
if (pos == length) { | |
throw new IllegalStateException("Unexpected end of DN: " + dn); | |
} | |
switch (chars[pos]) { | |
case '"': | |
case '\\': | |
case ',': | |
case '=': | |
case '+': | |
case '<': | |
case '>': | |
case '#': | |
case ';': | |
case ' ': | |
case '*': | |
case '%': | |
case '_': | |
//FIXME: escaping is allowed only for leading or trailing space char | |
return chars[pos]; | |
default: | |
// RFC doesn't explicitly say that escaped hex pair is | |
// interpreted as UTF-8 char. It only contains an example of such DN. | |
return getUTF8(); | |
} | |
} | |
// decodes UTF-8 char | |
// see http://www.unicode.org for UTF-8 bit distribution table | |
private char getUTF8() { | |
int res = getByte(pos); | |
pos++; //FIXME tmp | |
if (res < 128) { // one byte: 0-7F | |
return (char) res; | |
} else if (res >= 192 && res <= 247) { | |
int count; | |
if (res <= 223) { // two bytes: C0-DF | |
count = 1; | |
res = res & 0x1F; | |
} else if (res <= 239) { // three bytes: E0-EF | |
count = 2; | |
res = res & 0x0F; | |
} else { // four bytes: F0-F7 | |
count = 3; | |
res = res & 0x07; | |
} | |
int b; | |
for (int i = 0; i < count; i++) { | |
pos++; | |
if (pos == length || chars[pos] != '\\') { | |
return 0x3F; //FIXME failed to decode UTF-8 char - return '?' | |
} | |
pos++; | |
b = getByte(pos); | |
pos++; //FIXME tmp | |
if ((b & 0xC0) != 0x80) { | |
return 0x3F; //FIXME failed to decode UTF-8 char - return '?' | |
} | |
res = (res << 6) + (b & 0x3F); | |
} | |
return (char) res; | |
} else { | |
return 0x3F; //FIXME failed to decode UTF-8 char - return '?' | |
} | |
} | |
// Returns byte representation of a char pair | |
// The char pair is composed of DN char in | |
// specified 'position' and the next char | |
// According to BNF syntax: | |
// hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" | |
// / "a" / "b" / "c" / "d" / "e" / "f" | |
private int getByte(int position) { | |
if (position + 1 >= length) { | |
throw new IllegalStateException("Malformed DN: " + dn); | |
} | |
int b1, b2; | |
b1 = chars[position]; | |
if (b1 >= '0' && b1 <= '9') { | |
b1 = b1 - '0'; | |
} else if (b1 >= 'a' && b1 <= 'f') { | |
b1 = b1 - 87; // 87 = 'a' - 10 | |
} else if (b1 >= 'A' && b1 <= 'F') { | |
b1 = b1 - 55; // 55 = 'A' - 10 | |
} else { | |
throw new IllegalStateException("Malformed DN: " + dn); | |
} | |
b2 = chars[position + 1]; | |
if (b2 >= '0' && b2 <= '9') { | |
b2 = b2 - '0'; | |
} else if (b2 >= 'a' && b2 <= 'f') { | |
b2 = b2 - 87; // 87 = 'a' - 10 | |
} else if (b2 >= 'A' && b2 <= 'F') { | |
b2 = b2 - 55; // 55 = 'A' - 10 | |
} else { | |
throw new IllegalStateException("Malformed DN: " + dn); | |
} | |
return (b1 << 4) + b2; | |
} | |
/** | |
* Parses the DN and returns the most significant attribute value for an attribute type, or null | |
* if none found. | |
* | |
* @param attributeType attribute type to look for (e.g. "ca") | |
*/ | |
public String findMostSpecific(String attributeType) { | |
// Initialize internal state. | |
pos = 0; | |
beg = 0; | |
end = 0; | |
cur = 0; | |
chars = dn.toCharArray(); | |
String attType = nextAT(); | |
if (attType == null) { | |
return null; | |
} | |
while (true) { | |
String attValue = ""; | |
if (pos == length) { | |
return null; | |
} | |
switch (chars[pos]) { | |
case '"': | |
attValue = quotedAV(); | |
break; | |
case '#': | |
attValue = hexAV(); | |
break; | |
case '+': | |
case ',': | |
case ';': // compatibility with RFC 1779: semicolon can separate RDNs | |
//empty attribute value | |
break; | |
default: | |
attValue = escapedAV(); | |
} | |
// Values are ordered from most specific to least specific | |
// due to the RFC2253 formatting. So take the first match | |
// we see. | |
if (attributeType.equalsIgnoreCase(attType)) { | |
return attValue; | |
} | |
if (pos >= length) { | |
return null; | |
} | |
if (chars[pos] == ',' || chars[pos] == ';') { | |
} else if (chars[pos] != '+') { | |
throw new IllegalStateException("Malformed DN: " + dn); | |
} | |
pos++; | |
attType = nextAT(); | |
if (attType == null) { | |
throw new IllegalStateException("Malformed DN: " + dn); | |
} | |
} | |
} | |
} | |
/** | |
* Quick and dirty pattern to differentiate IP addresses from hostnames. This is an approximation | |
* of Android's private InetAddress#isNumeric API. | |
* | |
* <p>This matches IPv6 addresses as a hex string containing at least one colon, and possibly | |
* including dots after the first colon. It matches IPv4 addresses as strings containing only | |
* decimal digits and dots. This pattern matches strings like "a:.23" and "54" that are neither IP | |
* addresses nor hostnames; they will be verified as IP addresses (which is a more strict | |
* verification). | |
*/ | |
private static final Pattern VERIFY_AS_IP_ADDRESS = Pattern.compile( | |
"([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)"); | |
/** Returns true if {@code host} is not a host name and might be an IP address. */ | |
static boolean verifyAsIpAddress(String host) { | |
return VERIFY_AS_IP_ADDRESS.matcher(host).matches(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment