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