mirror of https://github.com/grpc/grpc-java.git
[OkHttp]Add hostname verification.
This commit is contained in:
parent
663a4f4390
commit
d948330ef2
|
|
@ -31,6 +31,12 @@
|
|||
|
||||
package io.grpc.testing.integration;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.protobuf.EmptyProtos.Empty;
|
||||
|
||||
import com.squareup.okhttp.ConnectionSpec;
|
||||
import com.squareup.okhttp.TlsVersion;
|
||||
|
||||
|
|
@ -52,6 +58,8 @@ import org.junit.runners.JUnit4;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
|
||||
/**
|
||||
* Integration tests for GRPC over Http2 using the OkHttp framework.
|
||||
*/
|
||||
|
|
@ -117,4 +125,29 @@ public class Http2OkHttpTest extends AbstractTransportTest {
|
|||
recorder.awaitCompletion();
|
||||
emptyUnary();
|
||||
}
|
||||
|
||||
@Test(timeout = 10000)
|
||||
public void wrongHostNameFailHostnameVerification() throws Exception {
|
||||
OkHttpChannelBuilder builder = OkHttpChannelBuilder.forAddress("127.0.0.1", serverPort)
|
||||
.connectionSpec(new ConnectionSpec.Builder(OkHttpChannelBuilder.DEFAULT_CONNECTION_SPEC)
|
||||
.cipherSuites(TestUtils.preferredTestCiphers().toArray(new String[0]))
|
||||
.tlsVersions(ConnectionSpec.MODERN_TLS.tlsVersions().toArray(new TlsVersion[0]))
|
||||
.build())
|
||||
.overrideAuthority(GrpcUtil.authorityFromHostAndPort(
|
||||
"I.am.a.bad.hostname", serverPort));
|
||||
ManagedChannel channel = builder.sslSocketFactory(
|
||||
TestUtils.newSslSocketFactoryForCa(TestUtils.loadCert("ca.pem"))).build();
|
||||
TestServiceGrpc.TestServiceBlockingStub blockingStub =
|
||||
TestServiceGrpc.newBlockingStub(channel);
|
||||
|
||||
try {
|
||||
blockingStub.emptyCall(Empty.getDefaultInstance());
|
||||
fail("The rpc should have been failed due to hostname verification");
|
||||
} catch (Throwable t) {
|
||||
Throwable cause = Throwables.getRootCause(t);
|
||||
assertTrue("Failed by unexpected exception: " + cause,
|
||||
cause instanceof SSLPeerUnverifiedException);
|
||||
}
|
||||
channel.shutdown();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import java.security.Provider;
|
|||
import java.security.Security;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
/**
|
||||
|
|
@ -96,8 +97,10 @@ class OkHttpProtocolNegotiator {
|
|||
* @throws RuntimeException if the negotiation completed, but no protocol was selected.
|
||||
*/
|
||||
public String negotiate(
|
||||
SSLSocket sslSocket, String hostname, List<Protocol> protocols) throws IOException {
|
||||
configureTlsExtensions(sslSocket, hostname, protocols);
|
||||
SSLSocket sslSocket, String hostname, @Nullable List<Protocol> protocols) throws IOException {
|
||||
if (protocols != null) {
|
||||
configureTlsExtensions(sslSocket, hostname, protocols);
|
||||
}
|
||||
try {
|
||||
// Force handshake.
|
||||
sslSocket.startHandshake();
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ package io.grpc.okhttp;
|
|||
import com.google.common.base.Preconditions;
|
||||
|
||||
import io.grpc.okhttp.internal.ConnectionSpec;
|
||||
import io.grpc.okhttp.internal.OkHostnameVerifier;
|
||||
import io.grpc.okhttp.internal.Protocol;
|
||||
|
||||
import java.io.IOException;
|
||||
|
|
@ -42,6 +43,7 @@ import java.util.Arrays;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
|
|
@ -69,11 +71,13 @@ final class OkHttpTlsUpgrader {
|
|||
SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(
|
||||
socket, host, port, true /* auto close */);
|
||||
spec.apply(sslSocket, false);
|
||||
if (spec.supportsTlsExtensions()) {
|
||||
String negotiatedProtocol =
|
||||
OkHttpProtocolNegotiator.get().negotiate(sslSocket, host, TLS_PROTOCOLS);
|
||||
Preconditions.checkState(HTTP2_PROTOCOL_NAME.equals(negotiatedProtocol),
|
||||
"Only \"h2\" is supported, but negotiated protocol is %s", negotiatedProtocol);
|
||||
String negotiatedProtocol = OkHttpProtocolNegotiator.get().negotiate(
|
||||
sslSocket, host, spec.supportsTlsExtensions() ? TLS_PROTOCOLS : null);
|
||||
Preconditions.checkState(HTTP2_PROTOCOL_NAME.equals(negotiatedProtocol),
|
||||
"Only \"h2\" is supported, but negotiated protocol is %s", negotiatedProtocol);
|
||||
|
||||
if (!OkHostnameVerifier.INSTANCE.verify(host, sslSocket.getSession())) {
|
||||
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + host);
|
||||
}
|
||||
return sslSocket;
|
||||
}
|
||||
|
|
|
|||
410
okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/DistinguishedNameParser.java
vendored
Normal file
410
okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/DistinguishedNameParser.java
vendored
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* Forked from OkHttp 2.5.0
|
||||
*/
|
||||
|
||||
package io.grpc.okhttp.internal;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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;
|
||||
|
||||
public 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
255
okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/OkHostnameVerifier.java
vendored
Normal file
255
okhttp/third_party/okhttp/java/io/grpc/okhttp/internal/OkHostnameVerifier.java
vendored
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/*
|
||||
* Forked from OkHttp 2.5.0
|
||||
*/
|
||||
|
||||
package io.grpc.okhttp.internal;
|
||||
|
||||
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 consistent with <a
|
||||
* href="http://www.ietf.org/rfc/rfc2818.txt">RFC 2818</a>.
|
||||
*/
|
||||
public final class OkHostnameVerifier implements HostnameVerifier {
|
||||
public static final OkHostnameVerifier INSTANCE = new OkHostnameVerifier();
|
||||
|
||||
/**
|
||||
* 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.]+)");
|
||||
|
||||
private static final int ALT_DNS_NAME = 2;
|
||||
private static final int ALT_IPA_NAME = 7;
|
||||
|
||||
private OkHostnameVerifier() {
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
static boolean verifyAsIpAddress(String host) {
|
||||
return VERIFY_AS_IP_ADDRESS.matcher(host).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
public static List<String> allSubjectAltNames(X509Certificate certificate) {
|
||||
List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
|
||||
List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
|
||||
List<String> result = new ArrayList<String>(altIpaNames.size() + altDnsNames.size());
|
||||
result.addAll(altIpaNames);
|
||||
result.addAll(altDnsNames);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<String> getSubjectAltNames(X509Certificate certificate, int type) {
|
||||
List<String> result = new ArrayList<String>();
|
||||
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}.
|
||||
*/
|
||||
private 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;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue