|
19 | 19 |
|
20 | 20 | import java.io.ByteArrayInputStream; |
21 | 21 | import java.security.cert.CertificateFactory; |
| 22 | +import java.security.cert.CertificateParsingException; |
22 | 23 | import java.security.cert.X509Certificate; |
| 24 | +import java.util.stream.Stream; |
23 | 25 | import javax.net.ssl.HostnameVerifier; |
24 | 26 | import javax.net.ssl.SSLSession; |
25 | 27 | import javax.security.auth.x500.X500Principal; |
26 | 28 | import okhttp3.FakeSSLSession; |
| 29 | +import okhttp3.OkHttpClient; |
27 | 30 | import okhttp3.internal.Util; |
28 | | -import org.junit.Ignore; |
| 31 | +import okhttp3.tls.HeldCertificate; |
| 32 | +import okhttp3.tls.internal.TlsUtil; |
29 | 33 | import org.junit.Test; |
30 | 34 |
|
31 | 35 | import static java.nio.charset.StandardCharsets.UTF_8; |
|
36 | 40 | * from the Apache HTTP Client test suite. |
37 | 41 | */ |
38 | 42 | public final class HostnameVerifierTest { |
39 | | - private HostnameVerifier verifier = OkHostnameVerifier.INSTANCE; |
| 43 | + private OkHostnameVerifier verifier = OkHostnameVerifier.INSTANCE; |
40 | 44 |
|
41 | | - @Test public void verify() throws Exception { |
| 45 | + @Test public void verify() { |
42 | 46 | FakeSSLSession session = new FakeSSLSession(); |
43 | 47 | assertThat(verifier.verify("localhost", session)).isFalse(); |
44 | 48 | } |
@@ -148,7 +152,7 @@ public final class HostnameVerifierTest { |
148 | 152 | * are parsed. Android fails to parse these, which means we fall back to the CN. The RI does parse |
149 | 153 | * them, so the CN is unused. |
150 | 154 | */ |
151 | | - @Test @Ignore public void verifyNonAsciiSubjectAlt() throws Exception { |
| 155 | + @Test public void verifyNonAsciiSubjectAlt() throws Exception { |
152 | 156 | // CN=foo.com, subjectAlt=bar.com, subjectAlt=花子.co.jp |
153 | 157 | // (hanako.co.jp in kanji) |
154 | 158 | SSLSession session = session("" |
@@ -178,16 +182,20 @@ public final class HostnameVerifierTest { |
178 | 182 | + "sWIKHYrmhCIRshUNohGXv50m2o+1w9oWmQ6Dkq7lCjfXfUB4wIbggJjpyEtbNqBt\n" |
179 | 183 | + "j4MC2x5rfsLKKqToKmNE7pFEgqwe8//Aar1b+Qj+\n" |
180 | 184 | + "-----END CERTIFICATE-----\n"); |
181 | | - assertThat(verifier.verify("foo.com", session)).isTrue(); |
| 185 | + |
| 186 | + X509Certificate peerCertificate = ((X509Certificate) session.getPeerCertificates()[0]); |
| 187 | + assertThat(certificateSANs(peerCertificate)).containsExactly("bar.com", "������.co.jp"); |
| 188 | + |
| 189 | + assertThat(verifier.verify("foo.com", session)).isFalse(); |
182 | 190 | assertThat(verifier.verify("a.foo.com", session)).isFalse(); |
183 | 191 | // these checks test alternative subjects. The test data contains an |
184 | 192 | // alternative subject starting with a japanese kanji character. This is |
185 | 193 | // not supported by Android because the underlying implementation from |
186 | 194 | // harmony follows the definition from rfc 1034 page 10 for alternative |
187 | 195 | // subject names. This causes the code to drop all alternative subjects. |
188 | | - // assertTrue(verifier.verify("bar.com", session)); |
189 | | - // assertFalse(verifier.verify("a.bar.com", session)); |
190 | | - // assertFalse(verifier.verify("a.\u82b1\u5b50.co.jp", session)); |
| 196 | + assertThat(verifier.verify("bar.com", session)).isTrue(); |
| 197 | + assertThat(verifier.verify("a.bar.com", session)).isFalse(); |
| 198 | + assertThat(verifier.verify("a.\u82b1\u5b50.co.jp", session)).isFalse(); |
191 | 199 | } |
192 | 200 |
|
193 | 201 | @Test public void verifySubjectAltOnly() throws Exception { |
@@ -329,11 +337,11 @@ public final class HostnameVerifierTest { |
329 | 337 | } |
330 | 338 |
|
331 | 339 | /** |
332 | | - * Ignored due to incompatibilities between Android and Java on how non-ASCII subject alt names |
333 | | - * are parsed. Android fails to parse these, which means we fall back to the CN. The RI does parse |
334 | | - * them, so the CN is unused. |
| 340 | + * Previously ignored due to incompatibilities between Android and Java on how non-ASCII subject |
| 341 | + * alt names are parsed. Android fails to parse these, which means we fall back to the CN. |
| 342 | + * The RI does parse them, so the CN is unused. |
335 | 343 | */ |
336 | | - @Test @Ignore public void testWilcardNonAsciiSubjectAlt() throws Exception { |
| 344 | + @Test public void testWilcardNonAsciiSubjectAlt() throws Exception { |
337 | 345 | // CN=*.foo.com, subjectAlt=*.bar.com, subjectAlt=*.花子.co.jp |
338 | 346 | // (*.hanako.co.jp in kanji) |
339 | 347 | SSLSession session = session("" |
@@ -363,20 +371,24 @@ public final class HostnameVerifierTest { |
363 | 371 | + "qFr0AIZKBlg6NZZFf/0dP9zcKhzSriW27bY0XfzA6GSiRDXrDjgXq6baRT6YwgIg\n" |
364 | 372 | + "pgJsDbJtZfHnV1nd3M6zOtQPm1TIQpNmMMMd/DPrGcUQerD3\n" |
365 | 373 | + "-----END CERTIFICATE-----\n"); |
| 374 | + |
| 375 | + X509Certificate peerCertificate = ((X509Certificate) session.getPeerCertificates()[0]); |
| 376 | + assertThat(certificateSANs(peerCertificate)).containsExactly("*.bar.com", "*.������.co.jp"); |
| 377 | + |
366 | 378 | // try the foo.com variations |
367 | | - assertThat(verifier.verify("foo.com", session)).isTrue(); |
368 | | - assertThat(verifier.verify("www.foo.com", session)).isTrue(); |
369 | | - assertThat(verifier.verify("\u82b1\u5b50.foo.com", session)).isTrue(); |
| 379 | + assertThat(verifier.verify("foo.com", session)).isFalse(); |
| 380 | + assertThat(verifier.verify("www.foo.com", session)).isFalse(); |
| 381 | + assertThat(verifier.verify("\u82b1\u5b50.foo.com", session)).isFalse(); |
370 | 382 | assertThat(verifier.verify("a.b.foo.com", session)).isFalse(); |
371 | 383 | // these checks test alternative subjects. The test data contains an |
372 | 384 | // alternative subject starting with a japanese kanji character. This is |
373 | 385 | // not supported by Android because the underlying implementation from |
374 | 386 | // harmony follows the definition from rfc 1034 page 10 for alternative |
375 | 387 | // subject names. This causes the code to drop all alternative subjects. |
376 | | - // assertFalse(verifier.verify("bar.com", session)); |
377 | | - // assertTrue(verifier.verify("www.bar.com", session)); |
378 | | - // assertTrue(verifier.verify("\u82b1\u5b50.bar.com", session)); |
379 | | - // assertTrue(verifier.verify("a.b.bar.com", session)); |
| 388 | + assertThat(verifier.verify("bar.com", session)).isFalse(); |
| 389 | + assertThat(verifier.verify("www.bar.com", session)).isTrue(); |
| 390 | + assertThat(verifier.verify("\u82b1\u5b50.bar.com", session)).isFalse(); |
| 391 | + assertThat(verifier.verify("a.b.bar.com", session)).isFalse(); |
380 | 392 | } |
381 | 393 |
|
382 | 394 | @Test public void subjectAltUsesLocalDomainAndIp() throws Exception { |
@@ -554,6 +566,143 @@ public final class HostnameVerifierTest { |
554 | 566 | assertThat(verifier.verify("0:0:0:0:0:FFFF:C0A8:0101", session)).isTrue(); |
555 | 567 | } |
556 | 568 |
|
| 569 | + @Test public void generatedCertificate() throws Exception { |
| 570 | + HeldCertificate heldCertificate = new HeldCertificate.Builder() |
| 571 | + .commonName("Foo Corp") |
| 572 | + .addSubjectAlternativeName("foo.com") |
| 573 | + .build(); |
| 574 | + |
| 575 | + SSLSession session = session(heldCertificate.certificatePem()); |
| 576 | + assertThat(verifier.verify("foo.com", session)).isTrue(); |
| 577 | + assertThat(verifier.verify("bar.com", session)).isFalse(); |
| 578 | + } |
| 579 | + |
| 580 | + @Test public void specialKInHostname() throws Exception { |
| 581 | + // https://github.com/apache/httpcomponents-client/commit/303e435d7949652ea77a6c50df1c548682476b6e |
| 582 | + // https://www.gosecure.net/blog/2020/10/27/weakness-in-java-tls-host-verification/ |
| 583 | + |
| 584 | + HeldCertificate heldCertificate = new HeldCertificate.Builder() |
| 585 | + .commonName("Foo Corp") |
| 586 | + .addSubjectAlternativeName("k.com") |
| 587 | + .addSubjectAlternativeName("tel.com") |
| 588 | + .build(); |
| 589 | + |
| 590 | + SSLSession session = session(heldCertificate.certificatePem()); |
| 591 | + assertThat(verifier.verify("foo.com", session)).isFalse(); |
| 592 | + assertThat(verifier.verify("bar.com", session)).isFalse(); |
| 593 | + assertThat(verifier.verify("k.com", session)).isTrue(); |
| 594 | + assertThat(verifier.verify("K.com", session)).isTrue(); |
| 595 | + |
| 596 | + assertThat(verifier.verify("\u2121.com", session)).isFalse(); |
| 597 | + assertThat(verifier.verify("℡.com", session)).isFalse(); |
| 598 | + |
| 599 | + // These should ideally be false, but we know that hostname is usually already checked by us |
| 600 | + assertThat(verifier.verify("\u212A.com", session)).isFalse(); |
| 601 | + // Kelvin character below |
| 602 | + assertThat(verifier.verify("K.com", session)).isFalse(); |
| 603 | + } |
| 604 | + |
| 605 | + @Test public void specialKInCert() throws Exception { |
| 606 | + // https://github.com/apache/httpcomponents-client/commit/303e435d7949652ea77a6c50df1c548682476b6e |
| 607 | + // https://www.gosecure.net/blog/2020/10/27/weakness-in-java-tls-host-verification/ |
| 608 | + |
| 609 | + HeldCertificate heldCertificate = new HeldCertificate.Builder() |
| 610 | + .commonName("Foo Corp") |
| 611 | + .addSubjectAlternativeName("\u2121.com") |
| 612 | + .addSubjectAlternativeName("\u212A.com") |
| 613 | + .build(); |
| 614 | + |
| 615 | + SSLSession session = session(heldCertificate.certificatePem()); |
| 616 | + assertThat(verifier.verify("foo.com", session)).isFalse(); |
| 617 | + assertThat(verifier.verify("bar.com", session)).isFalse(); |
| 618 | + assertThat(verifier.verify("k.com", session)).isFalse(); |
| 619 | + assertThat(verifier.verify("K.com", session)).isFalse(); |
| 620 | + |
| 621 | + assertThat(verifier.verify("tel.com", session)).isFalse(); |
| 622 | + assertThat(verifier.verify("k.com", session)).isFalse(); |
| 623 | + } |
| 624 | + |
| 625 | + @Test public void specialKInExternalCert() throws Exception { |
| 626 | + // $ cat ./cert.cnf |
| 627 | + // [req] |
| 628 | + // distinguished_name=distinguished_name |
| 629 | + // req_extensions=req_extensions |
| 630 | + // x509_extensions=x509_extensions |
| 631 | + // [distinguished_name] |
| 632 | + // [req_extensions] |
| 633 | + // [x509_extensions] |
| 634 | + // subjectAltName=DNS:℡.com,DNS:K.com |
| 635 | + // |
| 636 | + // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \ |
| 637 | + // -newkey rsa:512 -out cert.pem |
| 638 | + SSLSession session = session("" |
| 639 | + + "-----BEGIN CERTIFICATE-----\n" |
| 640 | + + "MIIBSDCB86ADAgECAhRLR4TGgXBegg0np90FZ1KPeWpDtjANBgkqhkiG9w0BAQsF\n" |
| 641 | + + "ADASMRAwDgYDVQQDDAdmb28uY29tMCAXDTIwMTAyOTA2NTkwNVoYDzIxMjAxMDA1\n" |
| 642 | + + "MDY1OTA1WjASMRAwDgYDVQQDDAdmb28uY29tMFwwDQYJKoZIhvcNAQEBBQADSwAw\n" |
| 643 | + + "SAJBALQcTVW9aW++ClIV9/9iSzijsPvQGEu/FQOjIycSrSIheZyZmR8bluSNBq0C\n" |
| 644 | + + "9fpalRKZb0S2tlCTi5WoX8d3K30CAwEAAaMfMB0wGwYDVR0RBBQwEoIH4oShLmNv\n" |
| 645 | + + "bYIH4oSqLmNvbTANBgkqhkiG9w0BAQsFAANBAA1+/eDvSUGv78iEjNW+1w3OPAwt\n" |
| 646 | + + "Ij1qLQ/YI8OogZPMk7YY46/ydWWp7UpD47zy/vKmm4pOc8Glc8MoDD6UADs=\n" |
| 647 | + + "-----END CERTIFICATE-----\n"); |
| 648 | + |
| 649 | + X509Certificate peerCertificate = ((X509Certificate) session.getPeerCertificates()[0]); |
| 650 | + assertThat(certificateSANs(peerCertificate)).containsExactly("���.com", "���.com"); |
| 651 | + |
| 652 | + assertThat(verifier.verify("tel.com", session)).isFalse(); |
| 653 | + assertThat(verifier.verify("k.com", session)).isFalse(); |
| 654 | + |
| 655 | + assertThat(verifier.verify("foo.com", session)).isFalse(); |
| 656 | + assertThat(verifier.verify("bar.com", session)).isFalse(); |
| 657 | + assertThat(verifier.verify("k.com", session)).isFalse(); |
| 658 | + assertThat(verifier.verify("K.com", session)).isFalse(); |
| 659 | + } |
| 660 | + |
| 661 | + private Stream<String> certificateSANs(X509Certificate peerCertificate) |
| 662 | + throws CertificateParsingException { |
| 663 | + return peerCertificate.getSubjectAlternativeNames().stream().map(c -> (String) c.get(1)); |
| 664 | + } |
| 665 | + |
| 666 | + @Test public void replacementCharacter() throws Exception { |
| 667 | + // $ cat ./cert.cnf |
| 668 | + // [req] |
| 669 | + // distinguished_name=distinguished_name |
| 670 | + // req_extensions=req_extensions |
| 671 | + // x509_extensions=x509_extensions |
| 672 | + // [distinguished_name] |
| 673 | + // [req_extensions] |
| 674 | + // [x509_extensions] |
| 675 | + // subjectAltName=DNS:℡.com,DNS:K.com |
| 676 | + // |
| 677 | + // $ openssl req -x509 -nodes -days 36500 -subj '/CN=foo.com' -config ./cert.cnf \ |
| 678 | + // -newkey rsa:512 -out cert.pem |
| 679 | + SSLSession session = session("" |
| 680 | + + "-----BEGIN CERTIFICATE-----\n" |
| 681 | + + "MIIBSDCB86ADAgECAhRLR4TGgXBegg0np90FZ1KPeWpDtjANBgkqhkiG9w0BAQsF\n" |
| 682 | + + "ADASMRAwDgYDVQQDDAdmb28uY29tMCAXDTIwMTAyOTA2NTkwNVoYDzIxMjAxMDA1\n" |
| 683 | + + "MDY1OTA1WjASMRAwDgYDVQQDDAdmb28uY29tMFwwDQYJKoZIhvcNAQEBBQADSwAw\n" |
| 684 | + + "SAJBALQcTVW9aW++ClIV9/9iSzijsPvQGEu/FQOjIycSrSIheZyZmR8bluSNBq0C\n" |
| 685 | + + "9fpalRKZb0S2tlCTi5WoX8d3K30CAwEAAaMfMB0wGwYDVR0RBBQwEoIH4oShLmNv\n" |
| 686 | + + "bYIH4oSqLmNvbTANBgkqhkiG9w0BAQsFAANBAA1+/eDvSUGv78iEjNW+1w3OPAwt\n" |
| 687 | + + "Ij1qLQ/YI8OogZPMk7YY46/ydWWp7UpD47zy/vKmm4pOc8Glc8MoDD6UADs=\n" |
| 688 | + + "-----END CERTIFICATE-----\n"); |
| 689 | + |
| 690 | + // Replacement characters are deliberate, from certificate loading. |
| 691 | + assertThat(verifier.verify("���.com", session)).isFalse(); |
| 692 | + assertThat(verifier.verify("℡.com", session)).isFalse(); |
| 693 | + } |
| 694 | + |
| 695 | + @Test |
| 696 | + public void thatCatchesErrorsWithBadSession() { |
| 697 | + HostnameVerifier localVerifier = new OkHttpClient().hostnameVerifier(); |
| 698 | + |
| 699 | + // Since this is public API, okhttp3.internal.tls.OkHostnameVerifier.verify is also |
| 700 | + assertThat(verifier).isInstanceOf(OkHostnameVerifier.class); |
| 701 | + |
| 702 | + SSLSession session = TlsUtil.localhost().sslContext().createSSLEngine().getSession(); |
| 703 | + assertThat(localVerifier.verify("\uD83D\uDCA9.com", session)).isFalse(); |
| 704 | + } |
| 705 | + |
557 | 706 | @Test public void verifyAsIpAddress() { |
558 | 707 | // IPv4 |
559 | 708 | assertThat(Util.canParseAsIpAddress("127.0.0.1")).isTrue(); |
|
0 commit comments