View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.ssl;
29  
30  import java.io.ByteArrayInputStream;
31  import java.io.IOException;
32  import java.io.InputStream;
33  import java.io.InputStreamReader;
34  import java.nio.charset.StandardCharsets;
35  import java.security.cert.CertificateFactory;
36  import java.security.cert.X509Certificate;
37  import java.util.Arrays;
38  import java.util.Collections;
39  import java.util.List;
40  
41  import javax.net.ssl.SSLException;
42  
43  import org.apache.hc.client5.http.psl.PublicSuffixList;
44  import org.apache.hc.client5.http.psl.PublicSuffixListParser;
45  import org.apache.hc.client5.http.psl.PublicSuffixMatcher;
46  import org.apache.hc.client5.http.utils.DnsUtils;
47  import org.junit.jupiter.api.Assertions;
48  import org.junit.jupiter.api.BeforeEach;
49  import org.junit.jupiter.api.Test;
50  
51  /**
52   * Unit tests for {@link org.apache.hc.client5.http.ssl.DefaultHostnameVerifier}.
53   */
54  class TestDefaultHostnameVerifier {
55  
56      private DefaultHostnameVerifier impl;
57      private PublicSuffixMatcher publicSuffixMatcher;
58  
59      private static final String PUBLIC_SUFFIX_MATCHER_SOURCE_FILE = "suffixlistmatcher.txt";
60  
61      @BeforeEach
62      void setup() throws IOException {
63          impl = new DefaultHostnameVerifier();
64  
65          // Load the test PublicSuffixMatcher
66          final ClassLoader classLoader = getClass().getClassLoader();
67          final InputStream in = classLoader.getResourceAsStream(PUBLIC_SUFFIX_MATCHER_SOURCE_FILE);
68          Assertions.assertNotNull(in);
69          final List<PublicSuffixList> lists = PublicSuffixListParser.INSTANCE.parseByType(
70                  new InputStreamReader(in, StandardCharsets.UTF_8));
71          publicSuffixMatcher = new PublicSuffixMatcher(lists);
72      }
73  
74      @Test
75      void testVerify() throws Exception {
76          final CertificateFactory cf = CertificateFactory.getInstance("X.509");
77          InputStream in;
78          X509Certificate x509;
79          in = new ByteArrayInputStream(CertificatesToPlayWith.X509_FOO);
80          x509 = (X509Certificate) cf.generateCertificate(in);
81  
82          impl.verify("foo.com", x509);
83          exceptionPlease(impl, "a.foo.com", x509);
84          exceptionPlease(impl, "bar.com", x509);
85  
86          in = new ByteArrayInputStream(CertificatesToPlayWith.X509_HANAKO);
87          x509 = (X509Certificate) cf.generateCertificate(in);
88          impl.verify("\u82b1\u5b50.co.jp", x509);
89          exceptionPlease(impl, "a.\u82b1\u5b50.co.jp", x509);
90  
91          in = new ByteArrayInputStream(CertificatesToPlayWith.X509_FOO_BAR);
92          x509 = (X509Certificate) cf.generateCertificate(in);
93          exceptionPlease(impl, "foo.com", x509);
94          exceptionPlease(impl, "a.foo.com", x509);
95          impl.verify("bar.com", x509);
96          exceptionPlease(impl, "a.bar.com", x509);
97  
98          in = new ByteArrayInputStream(CertificatesToPlayWith.X509_FOO_BAR_HANAKO);
99          x509 = (X509Certificate) cf.generateCertificate(in);
100         exceptionPlease(impl, "foo.com", x509);
101         exceptionPlease(impl, "a.foo.com", x509);
102         impl.verify("bar.com", x509);
103         exceptionPlease(impl, "a.bar.com", x509);
104 
105         /*
106            Java isn't extracting international subjectAlts properly.  (Or
107            OpenSSL isn't storing them properly).
108         */
109         // DEFAULT.verify("\u82b1\u5b50.co.jp", x509 );
110         // impl.verify("\u82b1\u5b50.co.jp", x509 );
111         exceptionPlease(impl, "a.\u82b1\u5b50.co.jp", x509);
112 
113         in = new ByteArrayInputStream(CertificatesToPlayWith.X509_NO_CNS_FOO);
114         x509 = (X509Certificate) cf.generateCertificate(in);
115         impl.verify("foo.com", x509);
116         exceptionPlease(impl, "a.foo.com", x509);
117 
118         in = new ByteArrayInputStream(CertificatesToPlayWith.X509_NO_CNS_FOO);
119         x509 = (X509Certificate) cf.generateCertificate(in);
120         impl.verify("foo.com", x509);
121         exceptionPlease(impl, "a.foo.com", x509);
122 
123         in = new ByteArrayInputStream(CertificatesToPlayWith.X509_THREE_CNS_FOO_BAR_HANAKO);
124         x509 = (X509Certificate) cf.generateCertificate(in);
125         exceptionPlease(impl, "foo.com", x509);
126         exceptionPlease(impl, "a.foo.com", x509);
127         exceptionPlease(impl, "bar.com", x509);
128         exceptionPlease(impl, "a.bar.com", x509);
129         impl.verify("\u82b1\u5b50.co.jp", x509);
130         exceptionPlease(impl, "a.\u82b1\u5b50.co.jp", x509);
131 
132         in = new ByteArrayInputStream(CertificatesToPlayWith.X509_WILD_FOO);
133         x509 = (X509Certificate) cf.generateCertificate(in);
134         exceptionPlease(impl, "foo.com", x509);
135         impl.verify("www.foo.com", x509);
136         impl.verify("\u82b1\u5b50.foo.com", x509);
137         exceptionPlease(impl, "a.b.foo.com", x509);
138 
139         in = new ByteArrayInputStream(CertificatesToPlayWith.X509_WILD_CO_JP);
140         x509 = (X509Certificate) cf.generateCertificate(in);
141         // Silly test because no-one would ever be able to lookup an IP address
142         // using "*.co.jp".
143         impl.verify("*.co.jp", x509);
144         impl.verify("foo.co.jp", x509);
145         impl.verify("\u82b1\u5b50.co.jp", x509);
146 
147         in = new ByteArrayInputStream(CertificatesToPlayWith.X509_WILD_FOO_BAR_HANAKO);
148         x509 = (X509Certificate) cf.generateCertificate(in);
149         // try the foo.com variations
150         exceptionPlease(impl, "foo.com", x509);
151         exceptionPlease(impl, "www.foo.com", x509);
152         exceptionPlease(impl, "\u82b1\u5b50.foo.com", x509);
153         exceptionPlease(impl, "a.b.foo.com", x509);
154         // try the bar.com variations
155         exceptionPlease(impl, "bar.com", x509);
156         impl.verify("www.bar.com", x509);
157         impl.verify("\u82b1\u5b50.bar.com", x509);
158         exceptionPlease(impl, "a.b.bar.com", x509);
159 
160         in = new ByteArrayInputStream(CertificatesToPlayWith.X509_MULTIPLE_VALUE_AVA);
161         x509 = (X509Certificate) cf.generateCertificate(in);
162         impl.verify("repository.infonotary.com", x509);
163 
164         in = new ByteArrayInputStream(CertificatesToPlayWith.S_GOOGLE_COM);
165         x509 = (X509Certificate) cf.generateCertificate(in);
166         impl.verify("*.google.com", x509);
167 
168         in = new ByteArrayInputStream(CertificatesToPlayWith.S_GOOGLE_COM);
169         x509 = (X509Certificate) cf.generateCertificate(in);
170         impl.verify("*.Google.com", x509);
171 
172         in = new ByteArrayInputStream(CertificatesToPlayWith.IP_1_1_1_1);
173         x509 = (X509Certificate) cf.generateCertificate(in);
174         impl.verify("1.1.1.1", x509);
175         impl.verify("dummy-value.com", x509);
176 
177         exceptionPlease(impl, "1.1.1.2", x509);
178         exceptionPlease(impl, "not-the-cn.com", x509);
179 
180         in = new ByteArrayInputStream(CertificatesToPlayWith.EMAIL_ALT_SUBJECT_NAME);
181         x509 = (X509Certificate) cf.generateCertificate(in);
182         impl.verify("www.company.com", x509);
183     }
184 
185     @Test
186     void testSubjectAlt() throws Exception {
187         final CertificateFactory cf = CertificateFactory.getInstance("X.509");
188         final InputStream in = new ByteArrayInputStream(CertificatesToPlayWith.X509_MULTIPLE_SUBJECT_ALT);
189         final X509Certificate x509 = (X509Certificate) cf.generateCertificate(in);
190 
191         Assertions.assertEquals("CN=localhost, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=CH",
192                 x509.getSubjectDN().getName());
193 
194         impl.verify("localhost.localdomain", x509);
195         impl.verify("127.0.0.1", x509);
196 
197         Assertions.assertThrows(SSLException.class, () -> impl.verify("localhost", x509));
198         Assertions.assertThrows(SSLException.class, () -> impl.verify("local.host", x509));
199         Assertions.assertThrows(SSLException.class, () -> impl.verify("127.0.0.2", x509));
200     }
201 
202     public void exceptionPlease(final DefaultHostnameVerifier hv, final String host,
203                                 final X509Certificate x509) {
204         Assertions.assertThrows(SSLException.class, () -> hv.verify(host, x509));
205     }
206 
207     @Test
208     void testParseFQDN() {
209         Assertions.assertEquals(Arrays.asList("blah"),
210                 DefaultHostnameVerifier.parseFQDN("blah"));
211         Assertions.assertEquals(Arrays.asList("blah", "blah"),
212                 DefaultHostnameVerifier.parseFQDN("blah.blah"));
213         Assertions.assertEquals(Arrays.asList("blah", "blah", "blah"),
214                 DefaultHostnameVerifier.parseFQDN("blah.blah.blah"));
215         Assertions.assertEquals(Arrays.asList("", "", "blah", ""),
216                 DefaultHostnameVerifier.parseFQDN(".blah.."));
217         Assertions.assertEquals(Arrays.asList(""),
218                 DefaultHostnameVerifier.parseFQDN(""));
219         Assertions.assertEquals(Arrays.asList("", ""),
220                 DefaultHostnameVerifier.parseFQDN("."));
221         Assertions.assertEquals(Arrays.asList("com", "domain", "host"),
222                 DefaultHostnameVerifier.parseFQDN("host.domain.com"));
223     }
224 
225     @Test
226     void testDomainRootMatching() {
227         Assertions.assertFalse(DefaultHostnameVerifier.matchDomainRoot("a.b.c", null));
228         Assertions.assertTrue(DefaultHostnameVerifier.matchDomainRoot("a.b.c", "a.b.c"));
229         Assertions.assertFalse(DefaultHostnameVerifier.matchDomainRoot("aa.b.c", "a.b.c"));
230         Assertions.assertFalse(DefaultHostnameVerifier.matchDomainRoot("a.b.c", "aa.b.c"));
231         Assertions.assertTrue(DefaultHostnameVerifier.matchDomainRoot("a.a.b.c", "a.b.c"));
232     }
233 
234     static boolean matchIdentity(final String host, final String identity,
235                                  final PublicSuffixMatcher publicSuffixMatcher, final boolean strict) {
236         return DefaultHostnameVerifier.matchIdentity(
237                 DnsUtils.normalizeUnicode(host),
238                 DnsUtils.normalizeUnicode(identity),
239                 publicSuffixMatcher, strict);
240     }
241 
242     static boolean matchIdentity(final String host, final String identity,
243                                  final PublicSuffixMatcher publicSuffixMatcher) {
244         return matchIdentity(host, identity, publicSuffixMatcher, false);
245     }
246 
247     static boolean matchIdentity(final String host, final String identity) {
248         return matchIdentity(host, identity, null, false);
249     }
250 
251     static boolean matchIdentityStrict(final String host, final String identity,
252                                        final PublicSuffixMatcher publicSuffixMatcher) {
253         return matchIdentity(host, identity, publicSuffixMatcher, true);
254     }
255 
256     static boolean matchIdentityStrict(final String host, final String identity) {
257         return matchIdentity(host, identity, null, true);
258     }
259 
260     @Test
261     void testIdentityMatching() {
262 
263         Assertions.assertTrue(matchIdentity("a.b.c", "*.b.c"));
264         Assertions.assertTrue(matchIdentityStrict("a.b.c", "*.b.c"));
265 
266         Assertions.assertTrue(matchIdentity("s.a.b.c", "*.b.c"));
267         Assertions.assertFalse(matchIdentityStrict("s.a.b.c", "*.b.c")); // subdomain not OK
268 
269         Assertions.assertTrue(matchIdentity("a.gov.uk", "*.gov.uk", publicSuffixMatcher));
270         Assertions.assertTrue(matchIdentityStrict("a.gov.uk", "*.gov.uk", publicSuffixMatcher));  // Bad 2TLD
271 
272         Assertions.assertTrue(matchIdentity("s.a.gov.uk", "*.a.gov.uk", publicSuffixMatcher));
273         Assertions.assertTrue(matchIdentityStrict("s.a.gov.uk", "*.a.gov.uk", publicSuffixMatcher));
274 
275         Assertions.assertTrue(matchIdentity("s.a.gov.uk", "*.gov.uk", publicSuffixMatcher));
276         Assertions.assertFalse(matchIdentityStrict("s.a.gov.uk", "*.gov.uk", publicSuffixMatcher));  // BBad 2TLD/no subdomain allowed
277 
278         Assertions.assertTrue(matchIdentity("a.gov.com", "*.gov.com", publicSuffixMatcher));
279         Assertions.assertTrue(matchIdentityStrict("a.gov.com", "*.gov.com", publicSuffixMatcher));
280 
281         Assertions.assertTrue(matchIdentity("s.a.gov.com", "*.gov.com", publicSuffixMatcher));
282         Assertions.assertFalse(matchIdentityStrict("s.a.gov.com", "*.gov.com", publicSuffixMatcher)); // no subdomain allowed
283 
284         Assertions.assertTrue(matchIdentity("a.gov.uk", "a*.gov.uk", publicSuffixMatcher));
285         Assertions.assertTrue(matchIdentityStrict("a.gov.uk", "a*.gov.uk", publicSuffixMatcher)); // Bad 2TLD
286 
287         Assertions.assertFalse(matchIdentity("s.a.gov.uk", "a*.gov.uk", publicSuffixMatcher)); // Bad 2TLD
288         Assertions.assertFalse(matchIdentityStrict("s.a.gov.uk", "a*.gov.uk", publicSuffixMatcher)); // Bad 2TLD/no subdomain allowed
289 
290         Assertions.assertFalse(matchIdentity("a.b.c", "*.b.*"));
291         Assertions.assertFalse(matchIdentityStrict("a.b.c", "*.b.*"));
292 
293         Assertions.assertFalse(matchIdentity("a.b.c", "*.*.c"));
294         Assertions.assertFalse(matchIdentityStrict("a.b.c", "*.*.c"));
295 
296         Assertions.assertTrue(matchIdentity("a.b.xxx.uk", "a.b.xxx.uk", publicSuffixMatcher));
297         Assertions.assertTrue(matchIdentityStrict("a.b.xxx.uk", "a.b.xxx.uk", publicSuffixMatcher));
298 
299         Assertions.assertTrue(matchIdentity("a.b.xxx.uk", "*.b.xxx.uk", publicSuffixMatcher));
300         Assertions.assertTrue(matchIdentityStrict("a.b.xxx.uk", "*.b.xxx.uk", publicSuffixMatcher));
301 
302         Assertions.assertTrue(matchIdentity("b.xxx.uk", "b.xxx.uk", publicSuffixMatcher));
303         Assertions.assertTrue(matchIdentityStrict("b.xxx.uk", "b.xxx.uk", publicSuffixMatcher));
304 
305         Assertions.assertTrue(matchIdentity("b.xxx.uk", "*.xxx.uk", publicSuffixMatcher));
306         Assertions.assertTrue(matchIdentityStrict("b.xxx.uk", "*.xxx.uk", publicSuffixMatcher));
307     }
308 
309     @Test
310     void testHTTPCLIENT_1097() {
311         Assertions.assertTrue(matchIdentity("a.b.c", "a*.b.c"));
312         Assertions.assertTrue(matchIdentityStrict("a.b.c", "a*.b.c"));
313 
314         Assertions.assertTrue(matchIdentity("a.a.b.c", "a*.b.c"));
315         Assertions.assertFalse(matchIdentityStrict("a.a.b.c", "a*.b.c"));
316     }
317 
318     @Test
319     void testHTTPCLIENT_1255() {
320         Assertions.assertTrue(matchIdentity("mail.a.b.c.com", "m*.a.b.c.com"));
321         Assertions.assertTrue(matchIdentityStrict("mail.a.b.c.com", "m*.a.b.c.com"));
322     }
323 
324     @Test
325     void testHTTPCLIENT_1997() {
326         String domain;
327         // Unknown
328         domain = "dev.b.cloud.a";
329         Assertions.assertTrue(matchIdentity("service.apps." + domain, "*.apps." + domain));
330         Assertions.assertTrue(matchIdentityStrict("service.apps." + domain, "*.apps." + domain));
331         Assertions.assertTrue(matchIdentity("service.apps." + domain, "*.apps." + domain, publicSuffixMatcher));
332         Assertions.assertTrue(matchIdentityStrict("service.apps." + domain, "*.apps." + domain, publicSuffixMatcher));
333 
334         // ICANN
335         domain = "dev.b.cloud.com";
336         Assertions.assertTrue(matchIdentity("service.apps." + domain, "*.apps." + domain));
337         Assertions.assertTrue(matchIdentityStrict("service.apps." + domain, "*.apps." + domain));
338         Assertions.assertTrue(matchIdentity("service.apps." + domain, "*.apps." + domain, publicSuffixMatcher));
339         Assertions.assertTrue(matchIdentityStrict("service.apps." + domain, "*.apps." + domain, publicSuffixMatcher));
340 
341         // PRIVATE
342         domain = "dev.b.cloud.lan";
343         Assertions.assertTrue(matchIdentity("service.apps." + domain, "*.apps." + domain));
344         Assertions.assertTrue(matchIdentityStrict("service.apps." + domain, "*.apps." + domain));
345         Assertions.assertTrue(matchIdentity("service.apps." + domain, "*.apps." + domain, publicSuffixMatcher));
346         Assertions.assertTrue(matchIdentityStrict("service.apps." + domain, "*.apps." + domain, publicSuffixMatcher));
347     }
348 
349     @Test // Check compressed IPv6 hostname matching
350     void testHTTPCLIENT_1316() throws Exception{
351         final String host1 = "2001:0db8:aaaa:bbbb:cccc:0:0:0001";
352         DefaultHostnameVerifier.matchIPv6Address(host1, Collections.singletonList(SubjectName.IP("2001:0db8:aaaa:bbbb:cccc:0:0:0001")));
353         DefaultHostnameVerifier.matchIPv6Address(host1, Collections.singletonList(SubjectName.IP("2001:0db8:aaaa:bbbb:cccc::1")));
354         Assertions.assertThrows(SSLException.class, () ->
355                 DefaultHostnameVerifier.matchIPv6Address(host1, Collections.singletonList(SubjectName.IP("2001:0db8:aaaa:bbbb:cccc::10"))));
356         final String host2 = "2001:0db8:aaaa:bbbb:cccc::1";
357         DefaultHostnameVerifier.matchIPv6Address(host2, Collections.singletonList(SubjectName.IP("2001:0db8:aaaa:bbbb:cccc:0:0:0001")));
358         DefaultHostnameVerifier.matchIPv6Address(host2, Collections.singletonList(SubjectName.IP("2001:0db8:aaaa:bbbb:cccc::1")));
359         Assertions.assertThrows(SSLException.class, () ->
360                 DefaultHostnameVerifier.matchIPv6Address(host2, Collections.singletonList(SubjectName.IP("2001:0db8:aaaa:bbbb:cccc::10"))));
361     }
362 
363     @Test
364     void testHTTPCLIENT_2149() throws Exception {
365         final CertificateFactory cf = CertificateFactory.getInstance("X.509");
366         final InputStream in = new ByteArrayInputStream(CertificatesToPlayWith.SUBJECT_ALT_IP_ONLY);
367         final X509Certificate x509 = (X509Certificate) cf.generateCertificate(in);
368 
369         Assertions.assertEquals("CN=www.foo.com", x509.getSubjectDN().getName());
370 
371         impl.verify("127.0.0.1", x509);
372         impl.verify("www.foo.com", x509);
373 
374         exceptionPlease(impl, "127.0.0.2", x509);
375         exceptionPlease(impl, "www.bar.com", x509);
376     }
377 
378     @Test
379     void testExtractCN() throws Exception {
380         Assertions.assertEquals("blah", DefaultHostnameVerifier.extractCN("cn=blah, ou=blah, o=blah"));
381         Assertions.assertEquals("blah", DefaultHostnameVerifier.extractCN("cn=blah, cn=yada, cn=booh"));
382         Assertions.assertEquals("blah", DefaultHostnameVerifier.extractCN("c = pampa ,  cn  =    blah    , ou = blah , o = blah"));
383         Assertions.assertEquals("blah", DefaultHostnameVerifier.extractCN("cn=\"blah\", ou=blah, o=blah"));
384         Assertions.assertEquals("blah  blah", DefaultHostnameVerifier.extractCN("cn=\"blah  blah\", ou=blah, o=blah"));
385         Assertions.assertEquals("blah, blah", DefaultHostnameVerifier.extractCN("cn=\"blah, blah\", ou=blah, o=blah"));
386         Assertions.assertEquals("blah, blah", DefaultHostnameVerifier.extractCN("cn=blah\\, blah, ou=blah, o=blah"));
387         Assertions.assertEquals("blah", DefaultHostnameVerifier.extractCN("c = cn=uuh, cn=blah, ou=blah, o=blah"));
388         Assertions.assertThrows(SSLException.class, () ->
389                 DefaultHostnameVerifier.extractCN("blah,blah"));
390         Assertions.assertThrows(SSLException.class, () ->
391                 DefaultHostnameVerifier.extractCN("cn,o=blah"));
392     }
393 
394     @Test
395     void testMatchDNSName() throws Exception {
396         DefaultHostnameVerifier.matchDNSName(
397                 "host.domain.com",
398                 Collections.singletonList(SubjectName.DNS("*.domain.com")),
399                 publicSuffixMatcher);
400         DefaultHostnameVerifier.matchDNSName(
401                 "host.xx",
402                 Collections.singletonList(SubjectName.DNS("*.xx")),
403                 publicSuffixMatcher);
404         DefaultHostnameVerifier.matchDNSName(
405                 "host.appspot.com",
406                 Collections.singletonList(SubjectName.DNS("*.appspot.com")),
407                 publicSuffixMatcher);
408         DefaultHostnameVerifier.matchDNSName(
409                 "demo-s3-bucket.s3.eu-central-1.amazonaws.com",
410                 Collections.singletonList(SubjectName.DNS("*.s3.eu-central-1.amazonaws.com")),
411                 publicSuffixMatcher);
412         DefaultHostnameVerifier.matchDNSName(
413                 "hostname-workspace-1.local",
414                 Collections.singletonList(SubjectName.DNS("hostname-workspace-1.local")),
415                 publicSuffixMatcher);
416 
417         Assertions.assertThrows(SSLException.class, () ->
418                 DefaultHostnameVerifier.matchDNSName(
419                         "host.domain.com",
420                         Collections.singletonList(SubjectName.DNS("some.other.com")),
421                         publicSuffixMatcher));
422 
423         DefaultHostnameVerifier.matchDNSName(
424                 "host.ec2.compute-1.amazonaws.com",
425                 Collections.singletonList(SubjectName.DNS("host.ec2.compute-1.amazonaws.com")),
426                 publicSuffixMatcher);
427         DefaultHostnameVerifier.matchDNSName(
428                 "host.ec2.compute-1.amazonaws.com",
429                 Collections.singletonList(SubjectName.DNS("*.ec2.compute-1.amazonaws.com")),
430                 publicSuffixMatcher);
431         Assertions.assertThrows(SSLException.class, () ->
432             DefaultHostnameVerifier.matchDNSName(
433                     "compute-1.amazonaws.com",
434                     Collections.singletonList(SubjectName.DNS("*.compute-1.amazonaws.com")),
435                     publicSuffixMatcher));
436         DefaultHostnameVerifier.matchDNSName(
437                 "ec2.compute-1.amazonaws.com",
438                 Collections.singletonList(SubjectName.DNS("*.compute-1.amazonaws.com")),
439                 publicSuffixMatcher);
440     }
441 
442     @Test
443     void testMatchIdentity() {
444         // Test 1: IDN matching punycode
445         final String unicodeHost1 = "поиск-слов.рф";
446         final String punycodeHost1 = "xn----dtbqigoecuc.xn--p1ai";
447 
448         // These should now match, thanks to IDN.toASCII():
449         Assertions.assertTrue(matchIdentity(unicodeHost1, punycodeHost1),
450                 "Expected the Unicode host and its punycode to match"
451         );
452 
453         // ‘example.com’ vs. an unrelated punycode domain should fail:
454         Assertions.assertFalse(
455                 matchIdentity("example.com", punycodeHost1),
456                 "Expected mismatch between example.com and xn----dtbqigoecuc.xn--p1ai"
457         );
458 
459         // Test 2: Unicode host and Unicode identity
460         final String unicodeHost2 = "пример.рф";
461         final String unicodeIdentity2 = "пример.рф";
462         Assertions.assertTrue(matchIdentity(unicodeHost2, unicodeIdentity2),
463                 "Expected Unicode host and Unicode identity to match"
464         );
465 
466         // Test 3: Punycode host and Unicode identity
467         final String unicodeHost3 = "пример.рф";
468         final String punycodeIdentity3 = "xn--e1afmkfd.xn--p1ai";
469         Assertions.assertTrue(matchIdentity(unicodeHost3, punycodeIdentity3),
470                 "Expected Unicode host and punycode identity to match"
471         );
472 
473         // Test 4: Wildcard matching in the left-most label
474         final String unicodeHost4 = "sub.пример.рф";
475         final String unicodeIdentity4 = "*.пример.рф";
476         Assertions.assertTrue(matchIdentity(unicodeHost4, unicodeIdentity4),
477                 "Expected wildcard to match subdomain"
478         );
479 
480         // Test 5: Invalid host
481         final String invalidHost = "invalid_host";
482         final String unicodeIdentity5 = "пример.рф";
483         Assertions.assertFalse(
484                 matchIdentity(invalidHost, unicodeIdentity5),
485                 "Expected invalid host to not match"
486         );
487 
488         // Test 6: Invalid identity
489         final String unicodeHost4b = "пример.рф";
490         final String invalidIdentity = "xn--invalid-punycode";
491         Assertions.assertFalse(
492                 matchIdentity(unicodeHost4b, invalidIdentity),
493                 "Expected invalid identity to not match"
494         );
495 
496         // Test 7: Mixed case comparison
497         final String unicodeHost5 = "ПрИмеР.рф";
498         final String unicodeIdentity6 = "пример.рф";
499         Assertions.assertTrue(matchIdentity(unicodeHost5, unicodeIdentity6),
500                 "Expected case-insensitive Unicode comparison to match"
501         );
502 
503 
504         // Test 8: Wildcard in the middle label (per RFC 2818, should match)
505         final String unicodeHost6 = "sub.пример.рф";
506         final String unicodeIdentity8 = "sub.*.рф";
507         Assertions.assertTrue(matchIdentity(unicodeHost6, unicodeIdentity8),
508                 "Expected wildcard in the middle label to match"
509         );
510     }
511 
512 }