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});
}
}