diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/CertificatePinner.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/CertificatePinner.kt index 925502aceacb..3996bd10025e 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/CertificatePinner.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/CertificatePinner.kt @@ -290,8 +290,14 @@ class CertificatePinner internal constructor( } } - fun matchesHostname(hostname: String): Boolean = - when { + fun matchesHostname(hostname: String): Boolean { + // A hostname may carry a trailing dot for an absolute DNS name -- ConnectPlan derives it + // from address.url.host, which explicitly preserves one. A pin's pattern is + // developer-authored and never has one, so without this normalization the pin is silently + // skipped against the dotted spelling of an otherwise-identical hostname. + // OkHostnameVerifier already treats both spellings as equivalent for the same reason. + val hostname = if (hostname.endsWith(".")) hostname.dropLast(1) else hostname + return when { pattern.startsWith("**.") -> { // With ** empty prefixes match so exclude the dot from regionMatches(). val suffixLength = pattern.length - 3 @@ -312,6 +318,7 @@ class CertificatePinner internal constructor( hostname == pattern } } + } fun matchesCertificate(certificate: X509Certificate): Boolean = when (hashAlgorithm) { diff --git a/okhttp/src/jvmTest/kotlin/okhttp3/CertificatePinnerKotlinTest.kt b/okhttp/src/jvmTest/kotlin/okhttp3/CertificatePinnerKotlinTest.kt index 4a48ac813436..d6cf32d1fb7f 100644 --- a/okhttp/src/jvmTest/kotlin/okhttp3/CertificatePinnerKotlinTest.kt +++ b/okhttp/src/jvmTest/kotlin/okhttp3/CertificatePinnerKotlinTest.kt @@ -180,6 +180,23 @@ class CertificatePinnerKotlinTest { assertFalse(pin.matchesHostname("www.example.com")) } + @Test fun testMatchesHostnameWithTrailingDot() { + // https://github.com/lysine-dev/okhttp/issues/9724 + val exactPin = Pin("example.com", certA1Sha256Pin) + assertTrue(exactPin.matchesHostname("example.com")) + assertTrue(exactPin.matchesHostname("example.com.")) + + val wildcardPin = Pin("*.example.com", certA1Sha256Pin) + assertTrue(wildcardPin.matchesHostname("a.example.com")) + assertTrue(wildcardPin.matchesHostname("a.example.com.")) + + val certificatePinner = + CertificatePinner.Builder() + .add("example.com", certA1Sha256Pin) + .build() + assertThat(certificatePinner.findMatchingPins("example.com.")).containsExactly(exactPin) + } + @Test fun testMatchesSha256() { val pin = Pin("example.com", certA1Sha256Pin) assertTrue(pin.matchesCertificate(certA1.certificate))