diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 067922284..6ac57f038 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,6 +230,62 @@ jobs: path: openjdk/build/libs/conscrypt-openjdk-*-tests.jar if-no-files-found: error + android-test: + needs: boringssl_clone + runs-on: macos-15-intel + strategy: + fail-fast: false + matrix: + api-level: [26, 30, 34] + include: + - api-level: 26 + arch: x86_64 + target: google_apis + - api-level: 30 + arch: x86_64 + target: google_apis + - api-level: 34 + arch: x86_64 + target: google_apis + + steps: + - name: Set up JDK 17 for toolchains + uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: 17 + + - name: Set runner-specific environment variables + shell: bash + run: | + echo "ANDROID_HOME=$ANDROID_SDK_ROOT" >> $GITHUB_ENV + echo "BORINGSSL_HOME=${{ runner.temp }}/boringssl" >> $GITHUB_ENV + - uses: actions/checkout@v6 + + - name: Fetch BoringSSL source + uses: actions/download-artifact@v7 + with: + name: boringssl-source + path: ${{ runner.temp }}/boringssl + + - name: Checkout BoringSSL main branch + shell: bash + run: | + cd "$BORINGSSL_HOME" + git checkout --progress --force -B main + - name: Accept Android SDK licenses + run: | + yes | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --licenses || true + - name: Run instrumentation tests on Emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: ${{ matrix.api-level }} + arch: ${{ matrix.arch }} + force-avd-creation: true + disable-spellchecker: true + target: ${{ matrix.target }} + script: ./gradlew :conscrypt-android:connectedAndroidTest -PcheckErrorQueue + uberjar: needs: build diff --git a/common/src/main/java/org/conscrypt/AddressUtils.java b/common/src/main/java/org/conscrypt/AddressUtils.java index 759a2fcbf..f1eba3f34 100644 --- a/common/src/main/java/org/conscrypt/AddressUtils.java +++ b/common/src/main/java/org/conscrypt/AddressUtils.java @@ -16,36 +16,25 @@ package org.conscrypt; -import java.util.regex.Pattern; + /** * Utilities to check whether IP addresses meet some criteria. */ final class AddressUtils { - /* - * Regex that matches valid IPv4 and IPv6 addresses. - */ - private static final String IP_PATTERN = "^(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\\.){" - + "3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9]))|" - + "(?i:(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-" - + "f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][" - + "0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})|:))|(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{" - + "1,4}){1,2})|:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[" - + "0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})|:))|(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-" - + "9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-" - + "9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(?:(?:[0-" - + "9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[" - + "0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|" - + "[1-9]?[0-9])){3}))|:))|(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(" - + "?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:" - + "25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|(?:(?:[0-9a-f]{1,4}:){1}(?:" - + "(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4][0-9]|" - + "1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})" - + ")|:))|(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|" - + "2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]" - + "?[0-9])){3}))|:)))(?:%.+)?$"; - - private static Pattern ipPattern; + + private static final int IPV4_OCTET_COUNT = 4; + private static final int MAX_IPV4_OCTET_VALUE = 255; + private static final int MAX_IPV4_OCTET_DIGITS = 3; + private static final int MAX_IPV4_DOTS = IPV4_OCTET_COUNT - 1; + private static final int MIN_IPV4_ADDRESS_LENGTH = 7; + private static final int MAX_IPV4_ADDRESS_LENGTH = 15; + + private static final int IPV6_TOTAL_GROUPS = 8; + private static final int IPV6_GROUPS_PER_IPV4 = 2; + private static final int MAX_HEX_DIGITS_PER_GROUP = 4; + + private static final int ASCII_CASE_DIFF = 'a' - 'A'; private AddressUtils() {} @@ -58,20 +47,225 @@ static boolean isValidSniHostname(String sniHostname) { } // Must be a FQDN that does not have a trailing dot. - return (sniHostname.equalsIgnoreCase("localhost") || sniHostname.indexOf('.') != -1) - && !isLiteralIpAddress(sniHostname) && !sniHostname.endsWith(".") + return (asciiEqualsIgnoreCase(sniHostname, "localhost") || sniHostname.indexOf('.') != -1) + && !isLiteralIpAddress(sniHostname) + && !sniHostname.endsWith(".") && sniHostname.indexOf('\0') == -1; } + /** Returns true if the supplied hostname is an literal IP address. */ + static boolean isLiteralIpAddress(String hostname) { + if (hostname.isEmpty()) { + return false; + } + return isValidIPv4(hostname, 0, hostname.length(), /* allowLeadingZeros= */ true) + || isValidIPv6(hostname); + } + /** - * Returns true if the supplied hostname is an literal IP address. + * Validates IPv4 address. Expects exactly 4 octets separated by dots, each octet being 0-255. + * Allows leading zeros (up to 3 digits per octet). Parses the substring [start, end) without + * allocation. */ - static boolean isLiteralIpAddress(String hostname) { - /* This is here for backwards compatibility for pre-Honeycomb devices. */ - Pattern ipPattern = AddressUtils.ipPattern; - if (ipPattern == null) { - AddressUtils.ipPattern = ipPattern = Pattern.compile(IP_PATTERN); + private static boolean isValidIPv4(String s, int start, int end, boolean allowLeadingZeros) { + int len = end - start; + if (len < MIN_IPV4_ADDRESS_LENGTH || len > MAX_IPV4_ADDRESS_LENGTH) { + return false; + } + int octets = 0; + int value = 0; + int partLen = 0; + for (int i = start; i < end; i++) { + char c = s.charAt(i); + if (c == '.') { + octets++; + if (partLen == 0 || octets > MAX_IPV4_DOTS) { + return false; + } + value = 0; + partLen = 0; + } else if (isDigit(c)) { + if (!allowLeadingZeros && partLen == 1 && value == 0) { + return false; + } + value = value * 10 + (c - '0'); + partLen++; + if (partLen > MAX_IPV4_OCTET_DIGITS || value > MAX_IPV4_OCTET_VALUE) { + return false; + } + } else { + return false; + } + } + octets++; + return octets == IPV4_OCTET_COUNT; + } + + /** + * Validates IPv6 address. Supports full, compressed (::), and embedded IPv4 formats. Also + * supports optional Zone ID (%zone) at the end. Scans the string in a single pass without + * allocations. + */ + private static boolean isValidIPv6(String s) { + if (s.indexOf(':') == -1) { + return false; + } + int len = s.length(); + int groupCount = 0; + int groupLen = 0; + boolean hasDoubleColon = false; + int groupStart = 0; + + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + + if (c == ':') { + boolean isDoubleColon = (i + 1 < len && s.charAt(i + 1) == ':'); + if (isDoubleColon) { + if (hasDoubleColon) { + return false; // Multiple "::" + } + hasDoubleColon = true; + i++; // Skip second colon + + // Check for triple colon ":::" + if (i + 1 < len && s.charAt(i + 1) == ':') { + return false; + } + + if (groupLen > 0) { + groupCount++; + if (groupCount >= IPV6_TOTAL_GROUPS) { + return false; + } + } + groupLen = 0; + groupStart = i + 1; + } else { + // Single colon validation + if (i == len - 1 + || s.charAt(i + 1) == '%' + || groupLen == 0) { + return false; + } + groupCount++; + if (groupCount > IPV6_TOTAL_GROUPS + || (hasDoubleColon && groupCount >= IPV6_TOTAL_GROUPS)) { + return false; + } + groupLen = 0; + groupStart = i + 1; + } + } else if (c == '.') { + // Embedded IPv4 detected. Find the end of it (either end of string or start of zone + // ID '%'). + int ipv4End = i; + while (ipv4End < len && s.charAt(ipv4End) != '%') { + ipv4End++; + } + // Validate optional Zone ID if present after IPv4 + if (ipv4End < len && !isValidZoneId(s, ipv4End + 1)) { + return false; + } + + if (!isValidIPv4(s, groupStart, ipv4End, /* allowLeadingZeros= */ false)) { + return false; + } + groupCount += IPV6_GROUPS_PER_IPV4; + groupLen = 0; + break; // We have consumed the rest of the IP (and validated zone ID if present) + } else if (c == '%') { + // Standard IPv6 Zone ID + if (!isValidZoneId(s, i + 1)) { + return false; + } + if (groupLen > 0) { + groupCount++; + } + groupLen = 0; + break; // Exit loop, zone ID is validated + } else { + if (!isHexDigit(c)) { + return false; + } + groupLen++; + if (groupLen > MAX_HEX_DIGITS_PER_GROUP) { + return false; + } + } + } + + if (groupLen > 0) { + groupCount++; + } + + return hasDoubleColon ? groupCount < IPV6_TOTAL_GROUPS : groupCount == IPV6_TOTAL_GROUPS; + } + + /** + * Validates the IPv6 Zone ID (Scope ID). A valid Zone ID must not be empty and must not contain + * any line terminators, matching the behavior of the '.' character in the original regular + * expression. + */ + private static boolean isValidZoneId(String s, int start) { + int len = s.length(); + if (start >= len) { + return false; + } + for (int i = start; i < len; i++) { + char c = s.charAt(i); + // Reject Unicode line terminators: + // \n (Newline), \r (Carriage Return), \u0085 (Next Line), + // \u2028 (Line Separator), \u2029 (Paragraph Separator) + if (c == '\n' || c == '\r' || c == '\u0085' || c == '\u2028' || c == '\u2029') { + return false; + } + } + return true; + } + + /** + * Returns true if the character is a basic ASCII digit (0-9). We use this custom implementation + * instead of {@link Character#isDigit(char)} to avoid checking for other Unicode digit + * characters, keeping it strictly to ASCII and avoiding any locale or Unicode overhead. + */ + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } + + /** + * Returns true if the character is a valid hexadecimal digit (0-9, a-f, A-F). This is a simple + * range check that avoids any character class or regex compilation. + */ + private static boolean isHexDigit(char c) { + return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); + } + + private static char toLowerCaseAscii(char c) { + if (c >= 'A' && c <= 'Z') { + return (char) (c + ASCII_CASE_DIFF); + } + return c; + } + + /** + * Compares two ASCII strings case-insensitively. We use this custom implementation instead of + * {@link String#equalsIgnoreCase(String)} to: 1. Avoid dependency on Guava's Ascii class. 2. + * Avoid locale-dependent behavior of String.equalsIgnoreCase (e.g. Turkish 'I' mapping), + * ensuring strictly ASCII comparison. 3. Avoid any object allocations. + */ + private static boolean asciiEqualsIgnoreCase(String s, String expected) { + int len = s.length(); + if (len != expected.length()) { + return false; + } + for (int i = 0; i < len; i++) { + char c1 = s.charAt(i); + char c2 = expected.charAt(i); + if (c1 != c2 && toLowerCaseAscii(c1) != toLowerCaseAscii(c2)) { + return false; + } } - return ipPattern.matcher(hostname).matches(); + return true; } } diff --git a/common/src/main/java/org/conscrypt/Conscrypt.java b/common/src/main/java/org/conscrypt/Conscrypt.java index b7f140792..d86bdaa2a 100644 --- a/common/src/main/java/org/conscrypt/Conscrypt.java +++ b/common/src/main/java/org/conscrypt/Conscrypt.java @@ -361,6 +361,16 @@ private static AbstractConscryptSocket toConscrypt(SSLSocket socket) { return (AbstractConscryptSocket) socket; } + /** + * Sets the prioritized array of key exchange named groups names that can be used over the + * TLS socket. + * + *

See {@link SSLParameters#setNamedGroups(String[])} for more details. + */ + public static void setNamedGroups(SSLSocketFactory factory, String[] namedGroups) { + toConscrypt(factory).setNamedGroups(namedGroups); + } + /** * Sets the prioritized array of key exchange named groups names that can be used over the * TLS socket. diff --git a/common/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java b/common/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java index 41c0f3aa4..58f992836 100644 --- a/common/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java +++ b/common/src/main/java/org/conscrypt/OpenSSLSocketFactoryImpl.java @@ -79,6 +79,10 @@ void setUseEngineSocket(boolean useEngineSocket) { this.useEngineSocket = useEngineSocket; } + void setNamedGroups(String[] namedGroups) { + sslParameters.setNamedGroups(namedGroups); + } + @Override public String[] getDefaultCipherSuites() { return sslParameters.getEnabledCipherSuites(); diff --git a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketTest.java b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketTest.java index 5c20475e2..81d682570 100644 --- a/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketTest.java +++ b/common/src/test/java/org/conscrypt/javax/net/ssl/SSLSocketTest.java @@ -1245,6 +1245,36 @@ public void socket_setNamedGroups_works() throws Exception { context.close(); } + @Test + public void socketFactory_setNamedGroups_works() throws Exception { + TestSSLContext context = TestSSLContext.create(); + SSLSocketFactory clientSocketFactory = context.clientContext.getSocketFactory(); + Conscrypt.setNamedGroups(clientSocketFactory, new String[] {"P-384"}); + + final SSLSocket client = (SSLSocket) context.clientContext.getSocketFactory().createSocket( + context.host, context.port); + + // For the server, we don't set the named groups. P-384 should be + // enabled by default. + final SSLSocket server = (SSLSocket) context.serverSocket.accept(); + Future s = runAsync(() -> { + server.startHandshake(); + return null; + }); + Future c = runAsync(() -> { + client.startHandshake(); + return null; + }); + s.get(); + c.get(); + // This must works even if sslParametersSupportsNamedGroups is false. + assertEquals("P-384", getCurveName(client)); + assertEquals("P-384", getCurveName(server)); + client.close(); + server.close(); + context.close(); + } + @Test public void handshake_namedGroupsDontIntersect_throwsException() throws Exception { TestSSLContext context = TestSSLContext.create(); diff --git a/openjdk/src/test/java/org/conscrypt/AddressUtilsTest.java b/openjdk/src/test/java/org/conscrypt/AddressUtilsTest.java index 9816d0294..c668104b4 100644 --- a/openjdk/src/test/java/org/conscrypt/AddressUtilsTest.java +++ b/openjdk/src/test/java/org/conscrypt/AddressUtilsTest.java @@ -17,8 +17,12 @@ package org.conscrypt; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import java.util.Random; +import java.util.regex.Pattern; +import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -28,43 +32,43 @@ */ @RunWith(JUnit4.class) public class AddressUtilsTest { - @Test - public void test_isValidSniHostname_Success() throws Exception { + @Test + public void test_isValidSniHostname_success() throws Exception { assertTrue(AddressUtils.isValidSniHostname("www.google.com")); } - @Test - public void test_isValidSniHostname_NotFQDN_Failure() throws Exception { + @Test + public void test_isValidSniHostname_notFQDN_failure() throws Exception { assertFalse(AddressUtils.isValidSniHostname("www")); } - @Test - public void test_isValidSniHostname_Localhost_Success() throws Exception { + @Test + public void test_isValidSniHostname_localhost_success() throws Exception { assertTrue(AddressUtils.isValidSniHostname("LOCALhost")); } - @Test - public void test_isValidSniHostname_IPv4_Failure() throws Exception { + @Test + public void test_isValidSniHostname_iPv4_failure() throws Exception { assertFalse(AddressUtils.isValidSniHostname("192.168.0.1")); } - @Test - public void test_isValidSniHostname_IPv6_Failure() throws Exception { + @Test + public void test_isValidSniHostname_iPv6_failure() throws Exception { assertFalse(AddressUtils.isValidSniHostname("2001:db8::1")); } - @Test - public void test_isValidSniHostname_TrailingDot() throws Exception { + @Test + public void test_isValidSniHostname_trailingDot() throws Exception { assertFalse(AddressUtils.isValidSniHostname("www.google.com.")); } - @Test - public void test_isValidSniHostname_NullByte() throws Exception { + @Test + public void test_isValidSniHostname_nullByte() throws Exception { assertFalse(AddressUtils.isValidSniHostname("www\0.google.com")); } - @Test - public void test_isLiteralIpAddress_IPv4_Success() throws Exception { + @Test + public void test_isLiteralIpAddress_iPv4_success() throws Exception { assertTrue(AddressUtils.isLiteralIpAddress("127.0.0.1")); assertTrue(AddressUtils.isLiteralIpAddress("255.255.255.255")); assertTrue(AddressUtils.isLiteralIpAddress("0.0.00.000")); @@ -72,8 +76,8 @@ public void test_isLiteralIpAddress_IPv4_Success() throws Exception { assertTrue(AddressUtils.isLiteralIpAddress("254.249.190.094")); } - @Test - public void test_isLiteralIpAddress_IPv4_ExtraCharacters_Failure() throws Exception { + @Test + public void test_isLiteralIpAddress_iPv4_extraCharacters_failure() throws Exception { assertFalse(AddressUtils.isLiteralIpAddress("127.0.0.1a")); assertFalse(AddressUtils.isLiteralIpAddress(" 255.255.255.255")); assertFalse(AddressUtils.isLiteralIpAddress("0.0.00.0009")); @@ -83,15 +87,15 @@ public void test_isLiteralIpAddress_IPv4_ExtraCharacters_Failure() throws Except assertFalse(AddressUtils.isLiteralIpAddress("192.168.2.1%eth0")); } - @Test - public void test_isLiteralIpAddress_IPv4_NumbersTooLarge_Failure() throws Exception { + @Test + public void test_isLiteralIpAddress_iPv4_numbersTooLarge_failure() throws Exception { assertFalse(AddressUtils.isLiteralIpAddress("256.255.255.255")); assertFalse(AddressUtils.isLiteralIpAddress("255.255.255.256")); assertFalse(AddressUtils.isLiteralIpAddress("192.168.1.260")); } - @Test - public void test_isLiteralIpAddress_IPv6_Success() throws Exception { + @Test + public void test_isLiteralIpAddress_iPv6_success() throws Exception { assertTrue(AddressUtils.isLiteralIpAddress("::1")); assertTrue(AddressUtils.isLiteralIpAddress("2001:Db8::1")); assertTrue(AddressUtils.isLiteralIpAddress("2001:cdbA:0000:0000:0000:0000:3257:9652")); @@ -102,8 +106,8 @@ public void test_isLiteralIpAddress_IPv6_Success() throws Exception { assertTrue(AddressUtils.isLiteralIpAddress("2001:cdba::3257:9652%int2.3!")); } - @Test - public void test_isLiteralIpAddress_IPv6_Failure() throws Exception { + @Test + public void test_isLiteralIpAddress_iPv6_failure() throws Exception { assertFalse(AddressUtils.isLiteralIpAddress(":::1")); assertFalse(AddressUtils.isLiteralIpAddress("::11111")); assertFalse(AddressUtils.isLiteralIpAddress("20011::1111")); @@ -116,4 +120,356 @@ public void test_isLiteralIpAddress_IPv6_Failure() throws Exception { assertFalse(AddressUtils.isLiteralIpAddress("2001:cdba::3257:96521")); assertFalse(AddressUtils.isLiteralIpAddress("2001:cdba::3257:9652%")); } + + @Test + public void test_isLiteralIpAddress_null_throwsNPE() throws Exception { + assertThrows(NullPointerException.class, () -> AddressUtils.isLiteralIpAddress(null)); + } + + private static final String OLD_IP_PATTERN = + // IPv4 part: matches d.d.d.d where d is 0-255 (allows leading zeros) + "^(?:(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9]))|" + // IPv6 part (case-insensitive) + + "(?i:" + // Case 1: 7 prefix groups. Matches full (8 groups) or compressed at end (7 groups + ::) + // e.g., 1:2:3:4:5:6:7:8 or 1:2:3:4:5:6:7:: + + "(?:(?:[0-9a-f]{1,4}:){7}(?:[0-9a-f]{1,4}|:))|" + // Case 2: 6 prefix groups. Matches compressed in middle, compressed at end, or embedded + // IPv4 + // e.g., 1:2:3:4:5:6::8, 1:2:3:4:5:6::, 1:2:3:4:5:6:1.2.3.4 + + "(?:(?:[0-9a-f]{1,4}:){6}(?::[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})|:))|" + // Case 3: 5 prefix groups. + // e.g., 1:2:3:4:5::7:8, 1:2:3:4:5::1.2.3.4, 1:2:3:4:5:: + + "(?:(?:[0-9a-f]{1,4}:){5}(?:(?:(?::[0-9a-f]{1,4}){1,2})|:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3})|:))|" + // Case 4: 4 prefix groups. + + "(?:(?:[0-9a-f]{1,4}:){4}(?:(?:(?::[0-9a-f]{1,4}){1,3})|(?:(?::[0-9a-f]{1,4})?:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|" + // Case 5: 3 prefix groups. + + "(?:(?:[0-9a-f]{1,4}:){3}(?:(?:(?::[0-9a-f]{1,4}){1,4})|(?:(?::[0-9a-f]{1,4}){0,2}:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|" + // Case 6: 2 prefix groups. + + "(?:(?:[0-9a-f]{1,4}:){2}(?:(?:(?::[0-9a-f]{1,4}){1,5})|(?:(?::[0-9a-f]{1,4}){0,3}:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|" + // Case 7: 1 prefix group. + + "(?:(?:[0-9a-f]{1,4}:){1}(?:(?:(?::[0-9a-f]{1,4}){1,6})|(?:(?::[0-9a-f]{1,4}){0,4}:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))|" + // Case 8: 0 prefix groups. Starts with :: + // e.g., ::1, ::1.2.3.4, :: + + "(?::(?:(?:(?::[0-9a-f]{1,4}){1,7})|(?:(?::[0-9a-f]{1,4}){0,5}:(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])){3}))|:))" + // Optional Zone ID (e.g., %eth0) at the end of IPv6 + + ")(?:%.+)?$"; + private static final Pattern OLD_PATTERN = Pattern.compile(OLD_IP_PATTERN); + + private static boolean oldIsLiteralIpAddress(String hostname) { + return OLD_PATTERN.matcher(hostname).matches(); + } + + @Test + public void test_isLiteralIpAddress_differential() { + String[] testCases = { + // IPv4 Success + "127.0.0.1", + "255.255.255.255", + "0.0.00.000", + "192.009.010.19", + "254.249.190.094", + // IPv4 Failure + "127.0.0.1a", + " 255.255.255.255", + "0.0.00.0009", + "192.009z.010.19", + "254.249..094", + "192.168.2.1%1", + "192.168.2.1%eth0", + "256.255.255.255", + "255.255.255.256", + "192.168.1.260", + // IPv6 Success + "::1", + "2001:Db8::1", + "2001:cdbA:0000:0000:0000:0000:3257:9652", + "2001:cdba:0:0:0:0:3257:9652", + "2001:cdBA::3257:9652", + "2001:cdba::3257:9652%1", + "2001:cdba::3257:9652%eth0", + "2001:cdba::3257:9652%int2.3!", + // IPv6 Failure + ":::1", + "::11111", + "20011::1111", + "2001:db8:::1", + "2001:cdba:0000:00000:0000:0000:3257:9652", + "2001:cdbA:0000:0000:0000:0000:0000:3257:9652", + "2001:cdba:0::0:0:0:3257:9652", + "02001:cdba::3257:9652", + "2001:cdba::3257:96521", + "2001:cdba::3257:9652%", + // Additional Edge Cases + "", + " ", + "a", + "1", + "1.", + "1.2", + "1.2.3", + "1.2.3.4.5", + ":", + ":::", + "::::", + "1:", + ":1", + "1::2::", + "::1::2", + "2001:db8:1:2:3:4:5:6:7", + "2001:db8:1:2:3:4:5:6:7:8", + "1:2:3:4:5:6:7:8::", + "2001:db8:1:2:3:4:5:6:1.2.3.4", + "2001:db8:1:2:3:4:5:1.2.3.4", + "2001:db8:1:2:3:4:1.2.3.4.5", + "2001:db8:1:2:3:4:256.1.1.1", + "2001:db8::1.2.3.4%zone", + "2001:db8::1.2.3.4%", + "2001:db8::1.2.3.04", + "localhost", + "www.google.com", + "127.0.0.1.ipv4.google.com", + "1::", + "2001:db8::1%eth%0", + "2001:db8::1%eth 0", + "2001:db8::1%\u000B", + "2001:db8::1%\u000C", + "2001:db8::1%\r", + "2001:db8::1%\n", + "2001:db8::1%\u0085", + "2001:db8::1%\u2028", + "2001:db8::1%\u2029", + // Some random junk + "asdfasdfasdf", + "1:2:3:4:5:6:7:8:9:0", + "::1.2.3.4.5.6", + "2001:db8::1%_unsafe_zone_name", + "2001:db8::1%eth\n0", + "2001:db8::1.2.3.4%eth\n0", + "[2001:db8::1]", + "[::1]", + "2001:db8::1%eth0%1", + "2001:db8::1%eth0\u000C", + "2001:db8::1%%", + "2001:db8::1.2.3.4%%", + "1.2.3.4\u0000", + "2001:db8::1\u0000", + "2001:db8::1%\u0000", + "2001:db8::1%eth0\n", + "127.0.0.1\n", + "2001:db8::1\n", + "::ffff:192.168.1.1", + "::192.168.1.1", + "::1.2.3.4::", + "2001:db8::1.2.3.4::", + // Tricky Zone ID cases + "2001:db8::1%eth%0", + "2001:db8::1.2.3.4%eth%0", + "2001:db8::1%eth%", + "2001:db8::1.2.3.4%eth%", + "1.2.3.4%eth0", + "1.2.3.4%", + "2001:db8::1%", + "2001:db8::1.2.3.4%", + "2001:db8::1%eth0::", + "2001:db8::1.2.3.4%eth0::", + "2001:db8::1.2.3.4%eth0%eth1", + "2001:db8:1:2:3:4:5:6%eth0::1", + "2001:db8::%eth0", + ":.1.2.3.4", + "1::.2", + "2001:db8::.1.2.3.4", + // Tricky group and compression count cases + "1:2:3:4:5:6::7", + "1:2:3:4:5:6:7::", + "::1:2:3:4:5:6:7", + "1:2:3:4:5:6:7:8::", + "::1:2:3:4:5:6:7:8", + "1:2:3:4:5:6:7::8", + // Leading zeros in IPv4/embedded IPv4 + "1.2.3.01", + "0000.0.0.0", + "2001:db8::1.2.3.0", + "2001:db8::1.2.3.00", + "2001:db8::%1.2.3.4", + "2001:db8::1.2.3.4%5.6.7.8", + "::%eth0", + "1:2:3:4:5:6:7::", + "1:2:3:4:5:6:7:8::", + "1:2:3:4:5:6:7:8%eth0", + "1:2:3:4:5:6:7:8:9%eth0", + "2001:db8::1.2.3.01", + "2001:db8::1.2.3.0", + "2001:db8::1.2.3.00", + // Systematic Regex Cases + // Case 1 (7 prefix): + "1:2:3:4:5:6:7:8", + "1:2:3:4:5:6:7::", + // Case 2 (6 prefix): + "1:2:3:4:5:6::8", + "1:2:3:4:5:6:1.2.3.4", + "1:2:3:4:5:6::", + // Case 3 (5 prefix): + "1:2:3:4:5::8", + "1:2:3:4:5::7:8", + "1:2:3:4:5::1.2.3.4", + "1:2:3:4:5::", + // Case 4 (4 prefix): + "1:2:3:4::8", + "1:2:3:4::7:8", + "1:2:3:4::6:7:8", + "1:2:3:4::1.2.3.4", + "1:2:3:4::8:1.2.3.4", + "1:2:3:4::", + // Case 5 (3 prefix): + "1:2:3::8", + "1:2:3::7:8", + "1:2:3::6:7:8", + "1:2:3::5:6:7:8", + "1:2:3::1.2.3.4", + "1:2:3::8:1.2.3.4", + "1:2:3::7:8:1.2.3.4", + "1:2:3::", + // Case 6 (2 prefix): + "1:2::8", + "1:2::7:8", + "1:2::6:7:8", + "1:2::5:6:7:8", + "1:2::4:5:6:7:8", + "1:2::1.2.3.4", + "1:2::8:1.2.3.4", + "1:2::7:8:1.2.3.4", + "1:2::6:7:8:1.2.3.4", + "1:2::", + // Case 7 (1 prefix): + "1::8", + "1::7:8", + "1::6:7:8", + "1::5:6:7:8", + "1::4:5:6:7:8", + "1::3:4:5:6:7:8", + "1::1.2.3.4", + "1::8:1.2.3.4", + "1::7:8:1.2.3.4", + "1::6:7:8:1.2.3.4", + "1::5:6:7:8:1.2.3.4", + "1::", + // Case 8 (0 prefix): + "::8", + "::7:8", + "::6:7:8", + "::5:6:7:8", + "::4:5:6:7:8", + "::3:4:5:6:7:8", + "::2:3:4:5:6:7:8", + "::1.2.3.4", + "::8:1.2.3.4", + "::7:8:1.2.3.4", + "::6:7:8:1.2.3.4", + "::5:6:7:8:1.2.3.4", + "::4:5:6:7:8:1.2.3.4", + "::" + }; + + for (String tc : testCases) { + boolean expected = oldIsLiteralIpAddress(tc); + boolean actual = AddressUtils.isLiteralIpAddress(tc); + Assert.assertEquals("Mismatched result for input: " + tc, expected, actual); + } + } + + @Test + public void test_isLiteralIpAddress_exhaustive() { + String[] prefixes = { + "", + "1:", + "1:2:", + "1:2:3:", + "1:2:3:4:", + "1:2:3:4:5:", + "1:2:3:4:5:6:", + "1:2:3:4:5:6:7:", + "1:2:3:4:5:6:7:8:", + "::", + "1::", + "::1", + "1::2", + "1:2::", + "::1:2", + "1:2::3", + "1::2:3", + "1:2:3::", + "::1:2:3", + "1:2:3::4", + "1::2:3:4", + "1:2:3:4::", + "::1:2:3:4", + "1:2:3:4::5", + "1::2:3:4:5", + "1:2:3:4:5::", + "::1:2:3:4:5", + "1:2:3:4:5::6", + "1::2:3:4:5:6", + "1:2:3:4:5:6::", + "::1:2:3:4:5:6", + "1:2:3:4:5:6::7", + "1::2:3:4:5:6:7", + "1:2:3:4:5:6:7::", + "::1:2:3:4:5:6:7", + "1:2::3::4" + }; + + String[] suffixes = { + "8", "8:9", "8:9:10", "1.2.3.4", "1.2.3.4.5", "1.2.3", "256.1.2.3", "1.2.3.04", "01.2.3.4", "" + }; + + String[] zones = {"", "%eth0", "%", "%eth0%1", "%\n"}; + + for (String prefix : prefixes) { + for (String suffix : suffixes) { + for (String zone : zones) { + String tc = prefix + suffix + zone; + boolean expected = oldIsLiteralIpAddress(tc); + boolean actual = AddressUtils.isLiteralIpAddress(tc); + Assert.assertEquals("Mismatched result for input: " + tc, expected, actual); + } + } + } + } + + @Test + public void test_isLiteralIpAddress_randomFuzz() { + Random rand = new Random(42); + char[] chars = + "0123456789abcdefABCDEF:.% \n\r\0[]gGhHxXyYzZ+-/\u0085\u2028\u2029\u000B\u000C" + .toCharArray(); + for (int i = 0; i < 500000; i++) { + int len = rand.nextInt(50); + StringBuilder sb = new StringBuilder(len); + for (int j = 0; j < len; j++) { + sb.append(chars[rand.nextInt(chars.length)]); + } + String tc = sb.toString(); + boolean expected = oldIsLiteralIpAddress(tc); + boolean actual = AddressUtils.isLiteralIpAddress(tc); + if (expected != actual) { + Assert.fail( + String.format( + "Mismatch for input '%s' (hex: %s): expected %b, actual %b", + tc, toHex(tc), expected, actual)); + } + } + } + + private static String toHex(String s) { + StringBuilder sb = new StringBuilder(); + for (char c : s.toCharArray()) { + sb.append(String.format("\\u%04x", (int) c)); + } + return sb.toString(); + } + } + + + diff --git a/openjdk/src/test/java/org/conscrypt/ConscryptAndroidSuite.java b/openjdk/src/test/java/org/conscrypt/ConscryptAndroidSuite.java index 16a1bbe47..6f99aef83 100644 --- a/openjdk/src/test/java/org/conscrypt/ConscryptAndroidSuite.java +++ b/openjdk/src/test/java/org/conscrypt/ConscryptAndroidSuite.java @@ -16,7 +16,7 @@ package org.conscrypt; -import static org.conscrypt.TestUtils.installConscryptAsDefaultProvider; +import static org.conscrypt.TestUtils.getConscryptProvider; import org.conscrypt.ct.SerializationTest; import org.conscrypt.ct.VerifierTest; @@ -65,6 +65,11 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; +import java.security.Provider; +import java.security.Security; + +import tests.util.ServiceTester; + @RunWith(Suite.class) @Suite.SuiteClasses({ // org.conscrypt tests @@ -151,7 +156,9 @@ }) public class ConscryptAndroidSuite { @BeforeClass - public static void setupStatic() { - installConscryptAsDefaultProvider(); + public static void setupStatic() throws Exception { + Provider conscryptProvider = getConscryptProvider(); + Security.insertProviderAt(conscryptProvider, 1); + ServiceTester.setProviders(new Provider[] {conscryptProvider}); } }