| /* | |
| * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| import java.io.FileInputStream; | |
| import java.io.IOException; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| import java.net.SocketException; | |
| import java.net.URI; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpResponse; | |
| import java.net.http.HttpResponse.BodyHandlers; | |
| import java.security.KeyStore; | |
| import java.util.concurrent.Executor; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.ThreadFactory; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import javax.net.ssl.SSLContext; | |
| import javax.net.ssl.SSLHandshakeException; | |
| import javax.net.ssl.SSLParameters; | |
| import javax.net.ssl.TrustManagerFactory; | |
| import com.sun.net.httpserver.HttpExchange; | |
| import com.sun.net.httpserver.HttpHandler; | |
| import com.sun.net.httpserver.HttpsConfigurator; | |
| import com.sun.net.httpserver.HttpsParameters; | |
| import com.sun.net.httpserver.HttpsServer; | |
| import jdk.internal.util.OperatingSystem; | |
| import jdk.test.lib.net.SimpleSSLContext; | |
| import jdk.test.lib.net.URIBuilder; | |
| import org.junit.jupiter.api.Test; | |
| import org.junit.jupiter.params.ParameterizedTest; | |
| import org.junit.jupiter.params.provider.ValueSource; | |
| import static java.net.http.HttpClient.Builder.NO_PROXY; | |
| import static java.net.http.HttpClient.Version.HTTP_1_1; | |
| import static org.junit.jupiter.api.Assertions.assertEquals; | |
| import static org.junit.jupiter.api.Assertions.assertFalse; | |
| import static org.junit.jupiter.api.Assertions.assertNotNull; | |
| import static org.junit.jupiter.api.Assertions.assertTrue; | |
| import static org.junit.jupiter.api.Assertions.fail; | |
| /* | |
| * @test | |
| * @bug 8326381 | |
| * @summary verifies that the setNeedClientAuth() and setWantClientAuth() | |
| * methods on HttpsParameters class work as expected | |
| * @modules java.base/jdk.internal.util | |
| * @library /test/lib | |
| * @build jdk.test.lib.net.SimpleSSLContext jdk.test.lib.net.URIBuilder | |
| * @run junit HttpsParametersClientAuthTest | |
| */ | |
| public class HttpsParametersClientAuthTest { | |
| private static final boolean IS_WINDOWS = OperatingSystem.isWindows(); | |
| private static final AtomicInteger TID = new AtomicInteger(); | |
| private static final ThreadFactory SRV_THREAD_FACTORY = (r) -> { | |
| final Thread t = new Thread(r); | |
| t.setDaemon(true); | |
| t.setName("server-thread-" + TID.incrementAndGet()); | |
| return t; | |
| }; | |
| /** | |
| * verifies default values of {@link HttpsParameters#setNeedClientAuth(boolean)} | |
| * and {@link HttpsParameters#setWantClientAuth(boolean)} methods | |
| */ | |
| @Test | |
| public void testDefaultClientAuth() throws Exception { | |
| // test default values | |
| HttpsParameters defaultParams = new Params(); | |
| assertFalse(defaultParams.getNeedClientAuth(), | |
| "needClientAuth was expected to be false but wasn't"); | |
| assertFalse(defaultParams.getWantClientAuth(), | |
| "wantClientAuth was expected to be false but wasn't"); | |
| } | |
| /** | |
| * sets {@link HttpsParameters#setNeedClientAuth(boolean)} and verifies | |
| * that subsequent calls to {@link HttpsParameters#getNeedClientAuth()} returns | |
| * the set value and {@link HttpsParameters#getWantClientAuth()} returns false | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = {true, false}) | |
| public void testNeedClientAuth(final boolean initialWantClientAuth) throws Exception { | |
| HttpsParameters needClientAuthParams = new Params(); | |
| // first set wantClientAuth to an initial value to verify that it later gets reset | |
| needClientAuthParams.setWantClientAuth(initialWantClientAuth); | |
| // needClientAuth = true and thus wantClientAuth = false | |
| needClientAuthParams.setNeedClientAuth(true); | |
| assertTrue(needClientAuthParams.getNeedClientAuth(), | |
| "needClientAuth was expected to be true but wasn't"); | |
| assertFalse(needClientAuthParams.getWantClientAuth(), | |
| "wantClientAuth was expected to be false but wasn't"); | |
| // now set needClientAuth = false and verify that both needClientAuth and wantClientAuth | |
| // are now false | |
| needClientAuthParams.setNeedClientAuth(false); | |
| assertFalse(needClientAuthParams.getNeedClientAuth(), | |
| "needClientAuth was expected to be false but wasn't"); | |
| assertFalse(needClientAuthParams.getWantClientAuth(), | |
| "wantClientAuth was expected to be false but wasn't"); | |
| } | |
| /** | |
| * sets {@link HttpsParameters#setWantClientAuth(boolean)} and verifies | |
| * that subsequent calls to {@link HttpsParameters#getWantClientAuth()} returns | |
| * the set value and {@link HttpsParameters#getNeedClientAuth()} returns false | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = {true, false}) | |
| public void testWantClientAuth(final boolean initialNeedClientAuth) throws Exception { | |
| HttpsParameters wantClientAuthParams = new Params(); | |
| // first set needClientAuth to an initial value to verify that it later gets reset | |
| wantClientAuthParams.setNeedClientAuth(initialNeedClientAuth); | |
| // wantClientAuth = true and thus needClientAuth = false | |
| wantClientAuthParams.setWantClientAuth(true); | |
| assertTrue(wantClientAuthParams.getWantClientAuth(), | |
| "wantClientAuth was expected to be true but wasn't"); | |
| assertFalse(wantClientAuthParams.getNeedClientAuth(), | |
| "needClientAuth was expected to be false but wasn't"); | |
| // now set wantClientAuth = false and verify that both wantClientAuth and needClientAuth | |
| // are now false | |
| wantClientAuthParams.setWantClientAuth(false); | |
| assertFalse(wantClientAuthParams.getWantClientAuth(), | |
| "wantClientAuth was expected to be false but wasn't"); | |
| assertFalse(wantClientAuthParams.getNeedClientAuth(), | |
| "needClientAuth was expected to be false but wasn't"); | |
| } | |
| /** | |
| * Starts a {@link HttpsServer} by | |
| * {@linkplain HttpsParameters#setNeedClientAuth(boolean) setting needClientAuth} as | |
| * {@code true}. The client is then configured either to present the client certificates | |
| * during the TLS handshake or configured not to present them. In the case where the | |
| * client presents the client certificates, the HTTP request issued by the client is | |
| * expected to pass. In the other case where the client doesn't present the certificates, | |
| * the test verifies that the HTTP request fails due to a connection error | |
| * (caused by TLS handshake failure) | |
| * | |
| * @param presentClientCerts true if the client should present certificates | |
| * during TLS handshake, false otherwise | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = {true, false}) | |
| public void testServerNeedClientAuth(final boolean presentClientCerts) throws Exception { | |
| // SSLContext which contains both the key and the trust material and will be used | |
| // by the server | |
| final SSLContext serverSSLCtx = new SimpleSSLContext().get(); | |
| assertNotNull(serverSSLCtx, "could not create SSLContext"); | |
| final HttpsConfigurator configurator = new HttpsConfigurator(serverSSLCtx) { | |
| @Override | |
| public void configure(final HttpsParameters params) { | |
| // we intentionally don't call params.setSSLParameters() | |
| // and instead call params.setNeedClientAuth() | |
| params.setNeedClientAuth(true); // "require" the client to present certs | |
| } | |
| }; | |
| try (final ExecutorService executor = Executors.newCachedThreadPool(SRV_THREAD_FACTORY)) { | |
| final HttpsServer server = createHttpsServer(executor, configurator); | |
| server.start(); | |
| System.out.println("started server at " + server.getAddress()); | |
| try { | |
| final HttpClient.Builder builder = createClientBuilder(); | |
| // if the client is expected to present client certificates, then | |
| // we construct a SSLContext which has both key and trust material. | |
| // otherwise we construct a SSLContext that only has trust material | |
| // and thus won't present certificates during TLS handshake | |
| final SSLContext clientSSLCtx = presentClientCerts | |
| ? serverSSLCtx : onlyTrustStoreContext(); | |
| // construct the client using the SSLContext | |
| try (final HttpClient client = builder.sslContext(clientSSLCtx) | |
| .build()) { | |
| // issue a request | |
| final URI reqURI = URIBuilder.newBuilder() | |
| .scheme("https") | |
| .host(server.getAddress().getAddress()) | |
| .port(server.getAddress().getPort()) | |
| .path("/") | |
| .build(); | |
| System.out.println("issuing request to " + reqURI); | |
| final HttpResponse<Void> resp; | |
| try { | |
| resp = client.send(HttpRequest.newBuilder(reqURI).build(), | |
| BodyHandlers.discarding()); | |
| if (!presentClientCerts) { | |
| // request was expected to fail since the server was configured to force | |
| // the client to present the client cert, but the client didn't | |
| // present any | |
| fail("request was expected to fail, but didn't"); | |
| } | |
| assertEquals(200, resp.statusCode(), "unexpected response code"); | |
| // verify the client did present the certs | |
| assertTrue(resp.sslSession().isPresent(), "missing SSLSession on response"); | |
| assertNotNull(resp.sslSession().get().getLocalCertificates(), | |
| "client was expected to present certs to the server, but didn't"); | |
| } catch (IOException ioe) { | |
| if (presentClientCerts) { | |
| // wasn't expected to fail, just let the exception propagate | |
| throw ioe; | |
| } | |
| // verify it failed due to right reason | |
| Throwable cause = ioe.getCause(); | |
| while (cause != null) { | |
| // either of SocketException or SSLHandshakeException are OK. | |
| // additionally on Windows we accept even IOException | |
| // (caused by WSAECONNABORTED) | |
| if (cause instanceof SocketException se) { | |
| final String msg = se.getMessage(); | |
| assertTrue(msg != null && msg.contains("Connection reset"), | |
| "unexpected message in SocketException: " + msg); | |
| System.out.println("received the expected exception: " + se); | |
| break; | |
| } else if (cause instanceof SSLHandshakeException she) { | |
| final String msg = she.getMessage(); | |
| assertTrue(msg != null && msg.contains("certificate_required"), | |
| "unexpected message in SSLHandshakeException: " + msg); | |
| System.out.println("received the expected exception: " + she); | |
| break; | |
| } else if (IS_WINDOWS && cause instanceof IOException winIOE) { | |
| // on Windows we sometimes receive this exception, which is | |
| // acceptable | |
| System.out.println("(windows) received the expected exception: " | |
| + winIOE); | |
| break; | |
| } | |
| cause = cause.getCause(); | |
| } | |
| if (cause == null) { | |
| // didn't find expected exception, rethrow original exception | |
| throw ioe; | |
| } | |
| } | |
| } | |
| } finally { | |
| System.out.println("Stopping server at " + server.getAddress()); | |
| server.stop(0 /* delay */); | |
| } | |
| } | |
| } | |
| /** | |
| * Starts a {@link HttpsServer} by | |
| * {@linkplain HttpsParameters#setWantClientAuth(boolean) setting wantClientAuth} as | |
| * {@code true}. The client is then configured to either present the client certificates | |
| * during the TLS handshake or configured not to present them. In both these cases the | |
| * HTTP request issued by the client is expected to pass. | |
| * | |
| * @param presentClientCerts true if the client should present certificates | |
| * during TLS handshake, false otherwise | |
| */ | |
| @ParameterizedTest | |
| @ValueSource(booleans = {true, false}) | |
| public void testServerWantClientAuth(final boolean presentClientCerts) throws Exception { | |
| // SSLContext which contains both the key and the trust material and will be used | |
| // by the server | |
| final SSLContext serverSSLCtx = new SimpleSSLContext().get(); | |
| assertNotNull(serverSSLCtx, "could not create SSLContext"); | |
| final HttpsConfigurator configurator = new HttpsConfigurator(serverSSLCtx) { | |
| @Override | |
| public void configure(final HttpsParameters params) { | |
| // we intentionally don't call params.setSSLParameters() | |
| // and instead call params.setWantClientAuth() | |
| params.setWantClientAuth(true); // "request" the client to present certs | |
| } | |
| }; | |
| try (final ExecutorService executor = Executors.newCachedThreadPool(SRV_THREAD_FACTORY)) { | |
| final HttpsServer server = createHttpsServer(executor, configurator); | |
| server.start(); | |
| System.out.println("started server at " + server.getAddress()); | |
| try { | |
| final HttpClient.Builder builder = createClientBuilder(); | |
| // if the client is expected to present client certificates, then | |
| // we construct a SSLContext which has both key and trust material. | |
| // otherwise we construct a SSLContext that only has trust material | |
| // and thus won't present certificates during TLS handshake | |
| final SSLContext clientSSLCtx = presentClientCerts | |
| ? serverSSLCtx : onlyTrustStoreContext(); | |
| // construct the client using the SSLContext | |
| try (final HttpClient client = builder.sslContext(clientSSLCtx) | |
| .build()) { | |
| // issue a request | |
| final URI reqURI = URIBuilder.newBuilder() | |
| .scheme("https") | |
| .host(server.getAddress().getAddress()) | |
| .port(server.getAddress().getPort()) | |
| .path("/") | |
| .build(); | |
| System.out.println("issuing request to " + reqURI); | |
| final HttpResponse<Void> resp = client.send( | |
| HttpRequest.newBuilder(reqURI).build(), BodyHandlers.discarding()); | |
| assertEquals(200, resp.statusCode(), "unexpected response code"); | |
| if (presentClientCerts) { | |
| // verify the client did present the certs | |
| assertTrue(resp.sslSession().isPresent(), "missing SSLSession on response"); | |
| assertNotNull(resp.sslSession().get().getLocalCertificates(), | |
| "client was expected to present certs to the server, but didn't"); | |
| } | |
| } | |
| } finally { | |
| System.out.println("Stopping server at " + server.getAddress()); | |
| server.stop(0 /* delay */); | |
| } | |
| } | |
| } | |
| private static HttpsServer createHttpsServer(final Executor executor, | |
| final HttpsConfigurator configurator) | |
| throws IOException { | |
| final InetAddress loopback = InetAddress.getLoopbackAddress(); | |
| final HttpsServer server = HttpsServer.create( | |
| new InetSocketAddress(loopback, 0), 0 /* backlog */); | |
| server.setExecutor(executor); | |
| server.setHttpsConfigurator(configurator); | |
| server.createContext("/", new AllOKHandler()); | |
| return server; | |
| } | |
| private static HttpClient.Builder createClientBuilder() { | |
| return HttpClient.newBuilder().version(HTTP_1_1) | |
| .proxy(NO_PROXY); | |
| } | |
| /** | |
| * Creates and returns a {@link SSLContext} which only has trust material | |
| * and doesn't have any test specific keys. | |
| */ | |
| private static SSLContext onlyTrustStoreContext() throws Exception { | |
| final KeyStore keyStore = loadTestKeyStore(); | |
| final TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX"); | |
| tmf.init(keyStore); | |
| final SSLContext ctx = SSLContext.getInstance("TLS"); | |
| // initialize with only trust managers | |
| ctx.init(null, tmf.getTrustManagers(), null); | |
| return ctx; | |
| } | |
| private static KeyStore loadTestKeyStore() throws Exception { | |
| final String testKeys = System.getProperty("test.src") | |
| + "/" | |
| + "../../../../../../test/lib/jdk/test/lib/net/testkeys"; | |
| try (final FileInputStream fis = new FileInputStream(testKeys)) { | |
| final char[] passphrase = "passphrase".toCharArray(); | |
| final KeyStore ks = KeyStore.getInstance("PKCS12"); | |
| ks.load(fis, passphrase); | |
| return ks; | |
| } | |
| } | |
| // no-op implementations of the abstract methods of HttpsParameters | |
| private static final class Params extends HttpsParameters { | |
| @Override | |
| public HttpsConfigurator getHttpsConfigurator() { | |
| // no-op | |
| return null; | |
| } | |
| @Override | |
| public InetSocketAddress getClientAddress() { | |
| // no-op | |
| return null; | |
| } | |
| @Override | |
| public void setSSLParameters(SSLParameters params) { | |
| // no-op | |
| } | |
| } | |
| // A HttpHandler which just returns 200 response code | |
| private static final class AllOKHandler implements HttpHandler { | |
| private static final int NO_RESPONSE_BODY = -1; | |
| @Override | |
| public void handle(final HttpExchange exchange) throws IOException { | |
| System.out.println("responding to request: " + exchange.getRequestURI()); | |
| exchange.sendResponseHeaders(200, NO_RESPONSE_BODY); | |
| } | |
| } | |
| } |