Skip to content

Commit

Permalink
More restrictive behaviour of OkHostnameVerifier (#6353)
Browse files Browse the repository at this point in the history
* Test quirks of HostnameVerifier.
* Restrict successful results to ascii input.
  • Loading branch information
yschimke authored Oct 31, 2020
1 parent 4c15ae4 commit 003bf4f
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 23 deletions.
16 changes: 12 additions & 4 deletions okhttp/src/main/kotlin/okhttp3/internal/tls/OkHostnameVerifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,16 @@ object OkHostnameVerifier : HostnameVerifier {
private const val ALT_IPA_NAME = 7

override fun verify(host: String, session: SSLSession): Boolean {
return try {
verify(host, session.peerCertificates[0] as X509Certificate)
} catch (_: SSLException) {
return if (!host.isAscii()) {
false
} else {
try {
verify(host, session.peerCertificates[0] as X509Certificate)
} catch (_: SSLException) {
false
}
}

}

fun verify(host: String, certificate: X509Certificate): Boolean {
Expand Down Expand Up @@ -75,11 +80,14 @@ object OkHostnameVerifier : HostnameVerifier {
*/
private fun String.asciiToLowercase(): String {
return when {
length == utf8Size().toInt() -> toLowerCase(Locale.US) // This is an ASCII string.
isAscii() -> toLowerCase(Locale.US) // This is an ASCII string.
else -> this
}
}

/** Returns true if the [String] is ASCII encoded (0-127). */
private fun String.isAscii() = length == utf8Size().toInt()

/**
* Returns true if [hostname] matches the domain name [pattern].
*
Expand Down
84 changes: 65 additions & 19 deletions okhttp/src/test/java/okhttp3/internal/tls/HostnameVerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@

import java.io.ByteArrayInputStream;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateParsingException;
import java.security.cert.X509Certificate;
import java.util.stream.Stream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSession;
import javax.security.auth.x500.X500Principal;
Expand All @@ -28,7 +30,6 @@
import okhttp3.internal.Util;
import okhttp3.tls.HeldCertificate;
import okhttp3.tls.internal.TlsUtil;
import org.junit.Ignore;
import org.junit.Test;

import static java.nio.charset.StandardCharsets.UTF_8;
Expand All @@ -39,9 +40,9 @@
* from the Apache HTTP Client test suite.
*/
public final class HostnameVerifierTest {
private HostnameVerifier verifier = OkHostnameVerifier.INSTANCE;
private OkHostnameVerifier verifier = OkHostnameVerifier.INSTANCE;

@Test public void verify() throws Exception {
@Test public void verify() {
FakeSSLSession session = new FakeSSLSession();
assertThat(verifier.verify("localhost", session)).isFalse();
}
Expand Down Expand Up @@ -151,7 +152,7 @@ public final class HostnameVerifierTest {
* are parsed. Android fails to parse these, which means we fall back to the CN. The RI does parse
* them, so the CN is unused.
*/
@Test @Ignore public void verifyNonAsciiSubjectAlt() throws Exception {
@Test public void verifyNonAsciiSubjectAlt() throws Exception {
// CN=foo.com, subjectAlt=bar.com, subjectAlt=花子.co.jp
// (hanako.co.jp in kanji)
SSLSession session = session(""
Expand Down Expand Up @@ -181,16 +182,20 @@ public final class HostnameVerifierTest {
+ "sWIKHYrmhCIRshUNohGXv50m2o+1w9oWmQ6Dkq7lCjfXfUB4wIbggJjpyEtbNqBt\n"
+ "j4MC2x5rfsLKKqToKmNE7pFEgqwe8//Aar1b+Qj+\n"
+ "-----END CERTIFICATE-----\n");
assertThat(verifier.verify("foo.com", session)).isTrue();

X509Certificate peerCertificate = ((X509Certificate) session.getPeerCertificates()[0]);
assertThat(certificateSANs(peerCertificate)).containsExactly("bar.com", "������.co.jp");

assertThat(verifier.verify("foo.com", session)).isFalse();
assertThat(verifier.verify("a.foo.com", session)).isFalse();
// these checks test alternative subjects. The test data contains an
// alternative subject starting with a japanese kanji character. This is
// not supported by Android because the underlying implementation from
// harmony follows the definition from rfc 1034 page 10 for alternative
// subject names. This causes the code to drop all alternative subjects.
// assertTrue(verifier.verify("bar.com", session));
// assertFalse(verifier.verify("a.bar.com", session));
// assertFalse(verifier.verify("a.\u82b1\u5b50.co.jp", session));
assertThat(verifier.verify("bar.com", session)).isTrue();
assertThat(verifier.verify("a.bar.com", session)).isFalse();
assertThat(verifier.verify("a.\u82b1\u5b50.co.jp", session)).isFalse();
}

@Test public void verifySubjectAltOnly() throws Exception {
Expand Down Expand Up @@ -332,11 +337,11 @@ public final class HostnameVerifierTest {
}

/**
* Ignored due to incompatibilities between Android and Java on how non-ASCII subject alt names
* are parsed. Android fails to parse these, which means we fall back to the CN. The RI does parse
* them, so the CN is unused.
* Previously ignored due to incompatibilities between Android and Java on how non-ASCII subject
* alt names are parsed. Android fails to parse these, which means we fall back to the CN.
* The RI does parse them, so the CN is unused.
*/
@Test @Ignore public void testWilcardNonAsciiSubjectAlt() throws Exception {
@Test public void testWilcardNonAsciiSubjectAlt() throws Exception {
// CN=*.foo.com, subjectAlt=*.bar.com, subjectAlt=*.花子.co.jp
// (*.hanako.co.jp in kanji)
SSLSession session = session(""
Expand Down Expand Up @@ -366,20 +371,24 @@ public final class HostnameVerifierTest {
+ "qFr0AIZKBlg6NZZFf/0dP9zcKhzSriW27bY0XfzA6GSiRDXrDjgXq6baRT6YwgIg\n"
+ "pgJsDbJtZfHnV1nd3M6zOtQPm1TIQpNmMMMd/DPrGcUQerD3\n"
+ "-----END CERTIFICATE-----\n");

X509Certificate peerCertificate = ((X509Certificate) session.getPeerCertificates()[0]);
assertThat(certificateSANs(peerCertificate)).containsExactly("*.bar.com", "*.������.co.jp");

// try the foo.com variations
assertThat(verifier.verify("foo.com", session)).isTrue();
assertThat(verifier.verify("www.foo.com", session)).isTrue();
assertThat(verifier.verify("\u82b1\u5b50.foo.com", session)).isTrue();
assertThat(verifier.verify("foo.com", session)).isFalse();
assertThat(verifier.verify("www.foo.com", session)).isFalse();
assertThat(verifier.verify("\u82b1\u5b50.foo.com", session)).isFalse();
assertThat(verifier.verify("a.b.foo.com", session)).isFalse();
// these checks test alternative subjects. The test data contains an
// alternative subject starting with a japanese kanji character. This is
// not supported by Android because the underlying implementation from
// harmony follows the definition from rfc 1034 page 10 for alternative
// subject names. This causes the code to drop all alternative subjects.
// assertFalse(verifier.verify("bar.com", session));
// assertTrue(verifier.verify("www.bar.com", session));
// assertTrue(verifier.verify("\u82b1\u5b50.bar.com", session));
// assertTrue(verifier.verify("a.b.bar.com", session));
assertThat(verifier.verify("bar.com", session)).isFalse();
assertThat(verifier.verify("www.bar.com", session)).isTrue();
assertThat(verifier.verify("\u82b1\u5b50.bar.com", session)).isFalse();
assertThat(verifier.verify("a.b.bar.com", session)).isFalse();
}

@Test public void subjectAltUsesLocalDomainAndIp() throws Exception {
Expand Down Expand Up @@ -637,6 +646,9 @@ public final class HostnameVerifierTest {
+ "Ij1qLQ/YI8OogZPMk7YY46/ydWWp7UpD47zy/vKmm4pOc8Glc8MoDD6UADs=\n"
+ "-----END CERTIFICATE-----\n");

X509Certificate peerCertificate = ((X509Certificate) session.getPeerCertificates()[0]);
assertThat(certificateSANs(peerCertificate)).containsExactly("���.com", "���.com");

assertThat(verifier.verify("tel.com", session)).isFalse();
assertThat(verifier.verify("k.com", session)).isFalse();

Expand All @@ -646,6 +658,40 @@ public final class HostnameVerifierTest {
assertThat(verifier.verify("K.com", session)).isFalse();
}

private Stream<String> certificateSANs(X509Certificate peerCertificate)
throws CertificateParsingException {
return peerCertificate.getSubjectAlternativeNames().stream().map(c -> (String) c.get(1));
}

@Test public void replacementCharacter() throws Exception {
// $ cat ./cert.cnf
// [req]
// distinguished_name=distinguished_name
// req_extensions=req_extensions
// x509_extensions=x509_extensions
// [distinguished_name]
// [req_extensions]
// [x509_extensions]
// subjectAltName=DNS:℡.com,DNS:K.com
//
// $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \
// -newkey rsa:512 -out cert.pem
SSLSession session = session(""
+ "-----BEGIN CERTIFICATE-----\n"
+ "MIIBSDCB86ADAgECAhRLR4TGgXBegg0np90FZ1KPeWpDtjANBgkqhkiG9w0BAQsF\n"
+ "ADASMRAwDgYDVQQDDAdmb28uY29tMCAXDTIwMTAyOTA2NTkwNVoYDzIxMjAxMDA1\n"
+ "MDY1OTA1WjASMRAwDgYDVQQDDAdmb28uY29tMFwwDQYJKoZIhvcNAQEBBQADSwAw\n"
+ "SAJBALQcTVW9aW++ClIV9/9iSzijsPvQGEu/FQOjIycSrSIheZyZmR8bluSNBq0C\n"
+ "9fpalRKZb0S2tlCTi5WoX8d3K30CAwEAAaMfMB0wGwYDVR0RBBQwEoIH4oShLmNv\n"
+ "bYIH4oSqLmNvbTANBgkqhkiG9w0BAQsFAANBAA1+/eDvSUGv78iEjNW+1w3OPAwt\n"
+ "Ij1qLQ/YI8OogZPMk7YY46/ydWWp7UpD47zy/vKmm4pOc8Glc8MoDD6UADs=\n"
+ "-----END CERTIFICATE-----\n");

// Replacement characters are deliberate, from certificate loading.
assertThat(verifier.verify("���.com", session)).isFalse();
assertThat(verifier.verify("℡.com", session)).isFalse();
}

@Test
public void thatCatchesErrorsWithBadSession() {
HostnameVerifier localVerifier = new OkHttpClient().hostnameVerifier();
Expand Down

0 comments on commit 003bf4f

Please sign in to comment.