Skip to content

Commit

Permalink
Use generated certificates in unit tests (#6346)
Browse files Browse the repository at this point in the history
* Use generated certificates in unit tests
* Strict to ascii lowercase implementation

Co-authored-by: Jesse Wilson <jwilson@squareup.com>
  • Loading branch information
yschimke and swankjesse committed Oct 30, 2020
1 parent 31256ca commit bdd55bc
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 4 deletions.
21 changes: 17 additions & 4 deletions okhttp/src/main/kotlin/okhttp3/internal/tls/OkHostnameVerifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
*/
package okhttp3.internal.tls

import okhttp3.internal.canParseAsIpAddress
import okhttp3.internal.toCanonicalHost
import okio.utf8Size
import java.security.cert.CertificateParsingException
import java.security.cert.X509Certificate
import java.util.Locale
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLException
import javax.net.ssl.SSLSession
import okhttp3.internal.canParseAsIpAddress
import okhttp3.internal.toCanonicalHost

/**
* A HostnameVerifier consistent with [RFC 2818][rfc_2818].
Expand Down Expand Up @@ -61,12 +62,24 @@ object OkHostnameVerifier : HostnameVerifier {

/** Returns true if [certificate] matches [hostname]. */
private fun verifyHostname(hostname: String, certificate: X509Certificate): Boolean {
val hostname = hostname.toLowerCase(Locale.US)
val hostname = hostname.asciiToLowercase()
return getSubjectAltNames(certificate, ALT_DNS_NAME).any {
verifyHostname(hostname, it)
}
}

/**
* This is like [toLowerCase] except that it does nothing if this contains any non-ASCII
* characters. We want to avoid lower casing special chars like U+212A (Kelvin symbol) because
* they can return ASCII characters that match real hostnames.
*/
private fun String.asciiToLowercase(): String {
return when {
length == utf8Size().toInt() -> toLowerCase(Locale.US) // This is an ASCII string.
else -> this
}
}

/**
* Returns true if [hostname] matches the domain name [pattern].
*
Expand Down Expand Up @@ -108,7 +121,7 @@ object OkHostnameVerifier : HostnameVerifier {
}
// Hostname and pattern are now absolute domain names.

pattern = pattern.toLowerCase(Locale.US)
pattern = pattern.asciiToLowercase()
// Hostname and pattern are now in lower case -- domain names are case-insensitive.

if ("*" !in pattern) {
Expand Down
11 changes: 11 additions & 0 deletions okhttp/src/test/java/okhttp3/HttpUrlTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1773,6 +1773,17 @@ public void unparseableTopPrivateDomain() {
assertInvalid("http://../", "Invalid URL host: \"..\"");
}

@Test
public void hostnameTelephone() throws Exception {
// https://www.gosecure.net/blog/2020/10/27/weakness-in-java-tls-host-verification/

// Map the single character telephone symbol (℡) to the string "tel".
assertThat(parse("http://\u2121").host()).isEqualTo("tel");

// Map the Kelvin symbol (K) to the string "k".
assertThat(parse("http://\u212A").host()).isEqualTo("k");
}

private void assertInvalid(String string, String exceptionMessage) {
if (useGet) {
try {
Expand Down
103 changes: 103 additions & 0 deletions okhttp/src/test/java/okhttp3/internal/tls/HostnameVerifierTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import javax.net.ssl.SSLSession;
import javax.security.auth.x500.X500Principal;
import okhttp3.FakeSSLSession;
import okhttp3.OkHttpClient;
import okhttp3.internal.Util;
import okhttp3.tls.HeldCertificate;
import okhttp3.tls.internal.TlsUtil;
import org.junit.Ignore;
import org.junit.Test;

Expand Down Expand Up @@ -554,6 +557,106 @@ public final class HostnameVerifierTest {
assertThat(verifier.verify("0:0:0:0:0:FFFF:C0A8:0101", session)).isTrue();
}

@Test public void generatedCertificate() throws Exception {
HeldCertificate heldCertificate = new HeldCertificate.Builder()
.commonName("Foo Corp")
.addSubjectAlternativeName("foo.com")
.build();

SSLSession session = session(heldCertificate.certificatePem());
assertThat(verifier.verify("foo.com", session)).isTrue();
assertThat(verifier.verify("bar.com", session)).isFalse();
}

@Test public void specialKInHostname() throws Exception {
// https://github.com/apache/httpcomponents-client/commit/303e435d7949652ea77a6c50df1c548682476b6e
// https://www.gosecure.net/blog/2020/10/27/weakness-in-java-tls-host-verification/

HeldCertificate heldCertificate = new HeldCertificate.Builder()
.commonName("Foo Corp")
.addSubjectAlternativeName("k.com")
.addSubjectAlternativeName("tel.com")
.build();

SSLSession session = session(heldCertificate.certificatePem());
assertThat(verifier.verify("foo.com", session)).isFalse();
assertThat(verifier.verify("bar.com", session)).isFalse();
assertThat(verifier.verify("k.com", session)).isTrue();
assertThat(verifier.verify("K.com", session)).isTrue();

assertThat(verifier.verify("\u2121.com", session)).isFalse();
assertThat(verifier.verify("℡.com", session)).isFalse();

// These should ideally be false, but we know that hostname is usually already checked by us
assertThat(verifier.verify("\u212A.com", session)).isFalse();
// Kelvin character below
assertThat(verifier.verify("K.com", session)).isFalse();
}

@Test public void specialKInCert() throws Exception {
// https://github.com/apache/httpcomponents-client/commit/303e435d7949652ea77a6c50df1c548682476b6e
// https://www.gosecure.net/blog/2020/10/27/weakness-in-java-tls-host-verification/

HeldCertificate heldCertificate = new HeldCertificate.Builder()
.commonName("Foo Corp")
.addSubjectAlternativeName("\u2121.com")
.addSubjectAlternativeName("\u212A.com")
.build();

SSLSession session = session(heldCertificate.certificatePem());
assertThat(verifier.verify("foo.com", session)).isFalse();
assertThat(verifier.verify("bar.com", session)).isFalse();
assertThat(verifier.verify("k.com", session)).isFalse();
assertThat(verifier.verify("K.com", session)).isFalse();

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

@Test public void specialKInExternalCert() 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");

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

assertThat(verifier.verify("foo.com", session)).isFalse();
assertThat(verifier.verify("bar.com", session)).isFalse();
assertThat(verifier.verify("k.com", session)).isFalse();
assertThat(verifier.verify("K.com", session)).isFalse();
}

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

// Since this is public API, okhttp3.internal.tls.OkHostnameVerifier.verify is also
assertThat(verifier).isInstanceOf(OkHostnameVerifier.class);

SSLSession session = TlsUtil.localhost().sslContext().createSSLEngine().getSession();
assertThat(localVerifier.verify("\uD83D\uDCA9.com", session)).isFalse();
}

@Test public void verifyAsIpAddress() {
// IPv4
assertThat(Util.canParseAsIpAddress("127.0.0.1")).isTrue();
Expand Down

0 comments on commit bdd55bc

Please sign in to comment.