Skip to content

Commit

Permalink
HTTPCLIENT-2353: Fix IDN hostname mismatch by normalizing identity wi…
Browse files Browse the repository at this point in the history
…th IDN.toUnicode before comparison so that Unicode and punycode forms match correctly. (#607)

(cherry picked from commit 9e3559e)
  • Loading branch information
arturobernalg committed Jan 6, 2025
1 parent f17a948 commit 3fa07f1
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

package org.apache.hc.client5.http.ssl;

import java.net.IDN;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.cert.Certificate;
Expand Down Expand Up @@ -228,8 +229,18 @@ private static boolean matchIdentity(final String host, final String identity,
final PublicSuffixMatcher publicSuffixMatcher,
final DomainType domainType,
final boolean strict) {

final String normalizedIdentity;
try {
// Convert only the identity to its Unicode form
normalizedIdentity = IDN.toUnicode(identity);
} catch (final IllegalArgumentException e) {
return false;
}

// Public suffix check on the Unicode identity
if (publicSuffixMatcher != null && host.contains(".")) {
if (publicSuffixMatcher.getDomainRoot(identity, domainType) == null) {
if (publicSuffixMatcher.getDomainRoot(normalizedIdentity, domainType) == null) {
return false;
}
}
Expand All @@ -239,10 +250,11 @@ private static boolean matchIdentity(final String host, final String identity,
// character * which is considered to match any single domain name
// component or component fragment..."
// Based on this statement presuming only singular wildcard is legal
final int asteriskIdx = identity.indexOf('*');
final int asteriskIdx = normalizedIdentity.indexOf('*');
if (asteriskIdx != -1) {
final String prefix = identity.substring(0, asteriskIdx);
final String suffix = identity.substring(asteriskIdx + 1);
final String prefix = normalizedIdentity.substring(0, asteriskIdx);
final String suffix = normalizedIdentity.substring(asteriskIdx + 1);

if (!prefix.isEmpty() && !host.startsWith(prefix)) {
return false;
}
Expand All @@ -252,12 +264,16 @@ private static boolean matchIdentity(final String host, final String identity,
// Additional sanity checks on content selected by wildcard can be done here
if (strict) {
final String remainder = host.substring(
prefix.length(), host.length() - suffix.length());
prefix.length(),
host.length() - suffix.length()
);
return !remainder.contains(".");
}
return true;
}
return host.equalsIgnoreCase(identity);

// Direct Unicode comparison
return host.equalsIgnoreCase(normalizedIdentity);
}

static boolean matchIdentity(final String host, final String identity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,4 +471,80 @@ void testMatchDNSName() throws Exception {
publicSuffixMatcher));
}

@Test
void testMatchIdentity() {
// Test 1: IDN matching punycode
final String unicodeHost1 = "поиск-слов.рф";
final String punycodeHost1 = "xn----dtbqigoecuc.xn--p1ai";

// These should now match, thanks to IDN.toASCII():
Assertions.assertTrue(
DefaultHostnameVerifier.matchIdentity(unicodeHost1, punycodeHost1),
"Expected the Unicode host and its punycode to match"
);

// ‘example.com’ vs. an unrelated punycode domain should fail:
Assertions.assertFalse(
DefaultHostnameVerifier.matchIdentity("example.com", punycodeHost1),
"Expected mismatch between example.com and xn----dtbqigoecuc.xn--p1ai"
);

// Test 2: Unicode host and Unicode identity
final String unicodeHost2 = "пример.рф";
final String unicodeIdentity2 = "пример.рф";
Assertions.assertTrue(
DefaultHostnameVerifier.matchIdentity(unicodeHost2, unicodeIdentity2),
"Expected Unicode host and Unicode identity to match"
);

// Test 3: Punycode host and Unicode identity
final String unicodeHost3 = "пример.рф";
final String punycodeIdentity3 = "xn--e1afmkfd.xn--p1ai";
Assertions.assertTrue(
DefaultHostnameVerifier.matchIdentity(unicodeHost3, punycodeIdentity3),
"Expected Unicode host and punycode identity to match"
);

// Test 4: Wildcard matching in the left-most label
final String unicodeHost4 = "sub.пример.рф";
final String unicodeIdentity4 = "*.пример.рф";
Assertions.assertTrue(
DefaultHostnameVerifier.matchIdentity(unicodeHost4, unicodeIdentity4),
"Expected wildcard to match subdomain"
);

// Test 5: Invalid host
final String invalidHost = "invalid_host";
final String unicodeIdentity5 = "пример.рф";
Assertions.assertFalse(
DefaultHostnameVerifier.matchIdentity(invalidHost, unicodeIdentity5),
"Expected invalid host to not match"
);

// Test 6: Invalid identity
final String unicodeHost4b = "пример.рф";
final String invalidIdentity = "xn--invalid-punycode";
Assertions.assertFalse(
DefaultHostnameVerifier.matchIdentity(unicodeHost4b, invalidIdentity),
"Expected invalid identity to not match"
);

// Test 7: Mixed case comparison
final String unicodeHost5 = "ПрИмеР.рф";
final String unicodeIdentity6 = "пример.рф";
Assertions.assertTrue(
DefaultHostnameVerifier.matchIdentity(unicodeHost5, unicodeIdentity6),
"Expected case-insensitive Unicode comparison to match"
);


// Test 8: Wildcard in the middle label (per RFC 2818, should match)
final String unicodeHost6 = "sub.пример.рф";
final String unicodeIdentity8 = "sub.*.рф";
Assertions.assertTrue(
DefaultHostnameVerifier.matchIdentity(unicodeHost6, unicodeIdentity8),
"Expected wildcard in the middle label to match"
);
}

}

0 comments on commit 3fa07f1

Please sign in to comment.