Move SSLCertificateSocketFactoryTest out of CtsNetTestCases
Problem: - SSLCertificateSocketFactory has a bug causing socket leaks in certain exception scenarios. - SSLCertificateSocketFactoryTest triggers this, leading to leaked sockets during test runs. - When run within CtsNetTestCases, the network traffic from these leaked sockets interferes with TrafficStatsTest#testTrafficStatsForLocalhost, which monitors traffic by UID within that same suite. - This interference causes TrafficStatsTest to be flaky. Solution: - Moving SSLCertificateSocketFactoryTest to a different suite ensures its leaked socket traffic is no longer associated with the same UID context monitored by TrafficStatsTest. - This isolation prevents the test interference and resolves the flakiness. Flag: TEST_ONLY Test: atest CtsLibcoreNetTestCases Bug: 393471251 Change-Id: I1cc626ba281939999a7d6978668eefea8531ffec
This commit is contained in:
parent
8baa85d1ca
commit
a8f2ea5b37
1 changed files with 0 additions and 374 deletions
|
|
@ -1,374 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2008 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package android.net.cts;
|
||||
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import android.net.SSLCertificateSocketFactory;
|
||||
import android.platform.test.annotations.AppModeFull;
|
||||
|
||||
import libcore.javax.net.ssl.SSLConfigurationAsserts;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLException;
|
||||
import javax.net.ssl.SSLPeerUnverifiedException;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class SSLCertificateSocketFactoryTest {
|
||||
// TEST_HOST should point to a web server with a valid TLS certificate.
|
||||
private static final String TEST_HOST = "www.google.com";
|
||||
private static final int HTTPS_PORT = 443;
|
||||
private HostnameVerifier mDefaultVerifier;
|
||||
private SSLCertificateSocketFactory mSocketFactory;
|
||||
private InetAddress mLocalAddress;
|
||||
// InetAddress obtained by resolving TEST_HOST.
|
||||
private InetAddress mTestHostAddress;
|
||||
// SocketAddress combining mTestHostAddress and HTTPS_PORT.
|
||||
private List<SocketAddress> mTestSocketAddresses;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
// Expected state before each test method is that
|
||||
// HttpsURLConnection.getDefaultHostnameVerifier() will return the system default.
|
||||
mDefaultVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
|
||||
mSocketFactory = (SSLCertificateSocketFactory)
|
||||
SSLCertificateSocketFactory.getDefault(1000 /* handshakeTimeoutMillis */);
|
||||
assertNotNull(mSocketFactory);
|
||||
InetAddress[] addresses;
|
||||
try {
|
||||
addresses = InetAddress.getAllByName(TEST_HOST);
|
||||
} catch (UnknownHostException uhe) {
|
||||
throw new AssertionError(
|
||||
"Unable to test SSLCertificateSocketFactory: cannot resolve " + TEST_HOST, uhe);
|
||||
}
|
||||
|
||||
mTestSocketAddresses = Arrays.stream(addresses)
|
||||
.map(addr -> new InetSocketAddress(addr, HTTPS_PORT))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Find the local and remote IP addresses which will be used to connect to TEST_HOST.
|
||||
try {
|
||||
Socket testSocket = new Socket(TEST_HOST, HTTPS_PORT);
|
||||
mLocalAddress = testSocket.getLocalAddress();
|
||||
mTestHostAddress = testSocket.getInetAddress();
|
||||
testSocket.close();
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(""
|
||||
+ "Unable to test SSLCertificateSocketFactory: cannot connect to "
|
||||
+ TEST_HOST, ioe);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the system default hostname verifier after each test.
|
||||
@After
|
||||
public void restoreDefaultHostnameVerifier() {
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultConfiguration() throws Exception {
|
||||
SSLConfigurationAsserts.assertSSLSocketFactoryDefaultConfiguration(mSocketFactory);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessProperties() {
|
||||
mSocketFactory.getSupportedCipherSuites();
|
||||
mSocketFactory.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the {@code createSocket()} cases which are expected to fail with {@code IOException}.
|
||||
*/
|
||||
@Test
|
||||
@AppModeFull(reason = "Socket cannot bind in instant app mode")
|
||||
public void createSocket_io_error_expected() {
|
||||
// Connect to the localhost HTTPS port. Should result in connection refused IOException
|
||||
// because no service should be listening on that port.
|
||||
InetAddress localhostAddress = InetAddress.getLoopbackAddress();
|
||||
try {
|
||||
mSocketFactory.createSocket(localhostAddress, HTTPS_PORT);
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
// Same, but also binding to a local address.
|
||||
try {
|
||||
mSocketFactory.createSocket(localhostAddress, HTTPS_PORT, localhostAddress, 0);
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
// expected
|
||||
}
|
||||
|
||||
// Same, wrapping an existing plain socket which is in an unconnected state.
|
||||
try {
|
||||
Socket socket = new Socket();
|
||||
mSocketFactory.createSocket(socket, "localhost", HTTPS_PORT, true);
|
||||
fail();
|
||||
} catch (IOException e) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests hostname verification for
|
||||
* {@link SSLCertificateSocketFactory#createSocket(String, int)}.
|
||||
*
|
||||
* <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
|
||||
* and whose peer TLS certificate has been verified to have the correct hostname.
|
||||
*
|
||||
* <p>{@link SSLCertificateSocketFactory} is documented to verify hostnames using
|
||||
* the {@link HostnameVerifier} returned by
|
||||
* {@link HttpsURLConnection#getDefaultHostnameVerifier}, so this test connects twice,
|
||||
* once with the system default {@link HostnameVerifier} which is expected to succeed,
|
||||
* and once after installing a {@link NegativeHostnameVerifier} which will cause
|
||||
* {@link #verifyHostname} to throw a {@link SSLPeerUnverifiedException}.
|
||||
*
|
||||
* <p>These tests only test the hostname verification logic in SSLCertificateSocketFactory,
|
||||
* other TLS failure modes and the default HostnameVerifier are tested elsewhere, see
|
||||
* {@link com.squareup.okhttp.internal.tls.HostnameVerifierTest} and
|
||||
* https://android.googlesource.com/platform/external/boringssl/+/refs/heads/master/src/ssl/test
|
||||
*
|
||||
* <p>Tests the following behaviour:-
|
||||
* <ul>
|
||||
* <li>TEST_SERVER is available and has a valid TLS certificate
|
||||
* <li>{@code createSocket()} verifies the remote hostname is correct using
|
||||
* {@link HttpsURLConnection#getDefaultHostnameVerifier}
|
||||
* <li>{@link SSLPeerUnverifiedException} is thrown when the remote hostname is invalid
|
||||
* </ul>
|
||||
*
|
||||
* <p>See also http://b/2807618.
|
||||
*/
|
||||
@Test
|
||||
public void createSocket_simple_with_hostname_verification() throws Exception {
|
||||
Socket socket = mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT);
|
||||
assertConnectedSocket(socket);
|
||||
socket.close();
|
||||
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
|
||||
try {
|
||||
mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT);
|
||||
fail();
|
||||
} catch (SSLPeerUnverifiedException expected) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests hostname verification for
|
||||
* {@link SSLCertificateSocketFactory#createSocket(Socket, String, int, boolean)}.
|
||||
*
|
||||
* <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
|
||||
* and whose peer TLS certificate has been verified to have the correct hostname.
|
||||
*
|
||||
* <p>The TLS socket returned is wrapped around the plain socket passed into
|
||||
* {@code createSocket()}.
|
||||
*
|
||||
* <p>See {@link #createSocket_simple_with_hostname_verification()} for test methodology.
|
||||
*/
|
||||
@Test
|
||||
public void createSocket_wrapped_with_hostname_verification() throws Exception {
|
||||
Socket underlying = new Socket(TEST_HOST, HTTPS_PORT);
|
||||
Socket socket = mSocketFactory.createSocket(underlying, TEST_HOST, HTTPS_PORT, true);
|
||||
assertConnectedSocket(socket);
|
||||
socket.close();
|
||||
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
|
||||
try {
|
||||
underlying = new Socket(TEST_HOST, HTTPS_PORT);
|
||||
mSocketFactory.createSocket(underlying, TEST_HOST, HTTPS_PORT, true);
|
||||
fail();
|
||||
} catch (SSLPeerUnverifiedException expected) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests hostname verification for
|
||||
* {@link SSLCertificateSocketFactory#createSocket(String, int, InetAddress, int)}.
|
||||
*
|
||||
* <p>This method should return a socket which is fully connected (i.e. TLS handshake complete)
|
||||
* and whose peer TLS certificate has been verified to have the correct hostname.
|
||||
*
|
||||
* <p>The TLS socket returned is also bound to the local address determined in {@link #setUp} to
|
||||
* be used for connections to TEST_HOST, and a wildcard port.
|
||||
*
|
||||
* <p>See {@link #createSocket_simple_with_hostname_verification()} for test methodology.
|
||||
*/
|
||||
@Test
|
||||
@AppModeFull(reason = "Socket cannot bind in instant app mode")
|
||||
public void createSocket_bound_with_hostname_verification() throws Exception {
|
||||
Socket socket = mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT, mLocalAddress, 0);
|
||||
assertConnectedSocket(socket);
|
||||
socket.close();
|
||||
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
|
||||
try {
|
||||
mSocketFactory.createSocket(TEST_HOST, HTTPS_PORT, mLocalAddress, 0);
|
||||
fail();
|
||||
} catch (SSLPeerUnverifiedException expected) {
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests hostname verification for
|
||||
* {@link SSLCertificateSocketFactory#createSocket(InetAddress, int)}.
|
||||
*
|
||||
* <p>This method should return a socket which the documentation describes as "unconnected",
|
||||
* which actually means that the socket is fully connected at the TCP layer but TLS handshaking
|
||||
* and hostname verification have not yet taken place.
|
||||
*
|
||||
* <p>Behaviour is tested by installing a {@link NegativeHostnameVerifier} and by calling
|
||||
* {@link #assertConnectedSocket} to ensure TLS handshaking but no hostname verification takes
|
||||
* place. Next, {@link #verifyHostname} is called to ensure that hostname verification is using
|
||||
* the {@link HostnameVerifier} returned by
|
||||
* {@link HttpsURLConnection#getDefaultHostnameVerifier} as documented.
|
||||
*
|
||||
* <p>Tests the following behaviour:-
|
||||
* <ul>
|
||||
* <li>TEST_SERVER is available and has a valid TLS certificate
|
||||
* <li>{@code createSocket()} does not verify the remote hostname
|
||||
* <li>Calling {@link #verifyHostname} on the returned socket throws
|
||||
* {@link SSLPeerUnverifiedException} if the remote hostname is invalid
|
||||
* </ul>
|
||||
*/
|
||||
@Test
|
||||
public void createSocket_simple_no_hostname_verification() throws Exception{
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
|
||||
Socket socket = mSocketFactory.createSocket(mTestHostAddress, HTTPS_PORT);
|
||||
// Need to provide the expected hostname here or the TLS handshake will
|
||||
// be unable to supply SNI to the remote host.
|
||||
mSocketFactory.setHostname(socket, TEST_HOST);
|
||||
assertConnectedSocket(socket);
|
||||
try {
|
||||
verifyHostname(socket, TEST_HOST);
|
||||
fail();
|
||||
} catch (SSLPeerUnverifiedException expected) {
|
||||
// expected
|
||||
}
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
|
||||
verifyHostname(socket, TEST_HOST);
|
||||
socket.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests hostname verification for
|
||||
* {@link SSLCertificateSocketFactory#createSocket(InetAddress, int, InetAddress, int)}.
|
||||
*
|
||||
* <p>This method should return a socket which the documentation describes as "unconnected",
|
||||
* which actually means that the socket is fully connected at the TCP layer but TLS handshaking
|
||||
* and hostname verification have not yet taken place.
|
||||
*
|
||||
* <p>The TLS socket returned is also bound to the local address determined in {@link #setUp} to
|
||||
* be used for connections to TEST_HOST, and a wildcard port.
|
||||
*
|
||||
* <p>See {@link #createSocket_simple_no_hostname_verification()} for test methodology.
|
||||
*/
|
||||
@Test
|
||||
@AppModeFull(reason = "Socket cannot bind in instant app mode")
|
||||
public void createSocket_bound_no_hostname_verification() throws Exception{
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(new NegativeHostnameVerifier());
|
||||
Socket socket =
|
||||
mSocketFactory.createSocket(mTestHostAddress, HTTPS_PORT, mLocalAddress, 0);
|
||||
// Need to provide the expected hostname here or the TLS handshake will
|
||||
// be unable to supply SNI to the peer.
|
||||
mSocketFactory.setHostname(socket, TEST_HOST);
|
||||
assertConnectedSocket(socket);
|
||||
try {
|
||||
verifyHostname(socket, TEST_HOST);
|
||||
fail();
|
||||
} catch (SSLPeerUnverifiedException expected) {
|
||||
// expected
|
||||
}
|
||||
HttpsURLConnection.setDefaultHostnameVerifier(mDefaultVerifier);
|
||||
verifyHostname(socket, TEST_HOST);
|
||||
socket.close();
|
||||
}
|
||||
|
||||
// This is copied from SSLCertificateSocketFactory#verifyHostname given that
|
||||
// the method is @hide.
|
||||
private static void verifyHostname(Socket socket, String hostname) throws Exception {
|
||||
if (!(socket instanceof SSLSocket)) {
|
||||
throw new IllegalArgumentException("Attempt to verify non-SSL socket");
|
||||
}
|
||||
|
||||
// The code at the start of OpenSSLSocketImpl.startHandshake()
|
||||
// ensures that the call is idempotent, so we can safely call it.
|
||||
SSLSocket ssl = (SSLSocket) socket;
|
||||
ssl.startHandshake();
|
||||
|
||||
SSLSession session = ssl.getSession();
|
||||
if (session == null) {
|
||||
throw new SSLException("Cannot verify SSL socket without session");
|
||||
}
|
||||
if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) {
|
||||
throw new SSLPeerUnverifiedException("Cannot verify hostname: " + hostname);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts a socket is fully connected to the expected peer.
|
||||
*
|
||||
* <p>For the variants of createSocket which verify the remote hostname,
|
||||
* {@code socket} should already be fully connected.
|
||||
*
|
||||
* <p>For the non-verifying variants, retrieving the input stream will trigger a TLS handshake
|
||||
* and so may throw an exception, for example if the peer's certificate is invalid.
|
||||
*
|
||||
* <p>Does no hostname verification.
|
||||
*/
|
||||
private void assertConnectedSocket(Socket socket) throws Exception {
|
||||
assertNotNull(socket);
|
||||
assertTrue(socket.isConnected());
|
||||
assertNotNull(socket.getInputStream());
|
||||
assertNotNull(socket.getOutputStream());
|
||||
assertTrue(mTestSocketAddresses.contains(socket.getRemoteSocketAddress()));
|
||||
}
|
||||
|
||||
/**
|
||||
* A HostnameVerifier which always returns false to simulate a server returning a
|
||||
* certificate which does not match the expected hostname.
|
||||
*/
|
||||
private static class NegativeHostnameVerifier implements HostnameVerifier {
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession sslSession) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue