diff --git a/jetty-client/src/test/resources/client_keystore.p12 b/jetty-client/src/test/resources/client_keystore.p12 index 90f71767165..52a94c5591b 100644 Binary files a/jetty-client/src/test/resources/client_keystore.p12 and b/jetty-client/src/test/resources/client_keystore.p12 differ diff --git a/jetty-client/src/test/resources/keystore.p12 b/jetty-client/src/test/resources/keystore.p12 index 4f17bd03790..d96d0667bd6 100644 Binary files a/jetty-client/src/test/resources/keystore.p12 and b/jetty-client/src/test/resources/keystore.p12 differ diff --git a/jetty-client/src/test/resources/readme_keystores.txt b/jetty-client/src/test/resources/readme_keystores.txt index f01c8bf7099..8b81e72c4a3 100644 --- a/jetty-client/src/test/resources/readme_keystores.txt +++ b/jetty-client/src/test/resources/readme_keystores.txt @@ -3,7 +3,7 @@ Since OpenJDK 13.0.2/11.0.6 it is required that CA certificates have the extensi The keystores are generated in the following way: # Generates the server keystore. Note the BasicConstraint=CA:true extension. -$ keytool -v -genkeypair -validity 36500 -keyalg RSA -keysize 2048 -keystore keystore.p12 -storetype pkcs12 -dname "CN=localhost, OU=Jetty, O=Webtide, L=Omaha, S=NE, C=US" -ext bc=ca:true -ext san=ip:127.0.0.1,ip:[::1] +$ keytool -v -genkeypair -validity 36500 -keyalg RSA -keysize 2048 -keystore keystore.p12 -storetype pkcs12 -dname "CN=localhost, OU=Jetty, O=Webtide, L=Omaha, S=NE, C=US" -ext bc=ca:true -ext san=ip:127.0.0.1,ip:[::1],dns:localhost # Export the server certificate. $ keytool -v -export -keystore keystore.p12 -rfc -file server.crt diff --git a/jetty-http2/http2-client/src/test/resources/keystore.p12 b/jetty-http2/http2-client/src/test/resources/keystore.p12 index 5876b9db728..d96d0667bd6 100644 Binary files a/jetty-http2/http2-client/src/test/resources/keystore.p12 and b/jetty-http2/http2-client/src/test/resources/keystore.p12 differ diff --git a/jetty-http3/http3-server/src/main/config/modules/http3.mod b/jetty-http3/http3-server/src/main/config/modules/http3.mod index 74d3ebb09ff..eff3d069164 100644 --- a/jetty-http3/http3-server/src/main/config/modules/http3.mod +++ b/jetty-http3/http3-server/src/main/config/modules/http3.mod @@ -12,6 +12,7 @@ experimental http2 jna quiche +work [lib] lib/http3/*.jar diff --git a/jetty-http3/http3-tests/src/test/java/org/eclipse/jetty/http3/tests/AbstractClientServerTest.java b/jetty-http3/http3-tests/src/test/java/org/eclipse/jetty/http3/tests/AbstractClientServerTest.java index 9c9b9189687..585276ec67b 100644 --- a/jetty-http3/http3-tests/src/test/java/org/eclipse/jetty/http3/tests/AbstractClientServerTest.java +++ b/jetty-http3/http3-tests/src/test/java/org/eclipse/jetty/http3/tests/AbstractClientServerTest.java @@ -13,8 +13,10 @@ package org.eclipse.jetty.http3.tests; +import java.io.InputStream; import java.lang.management.ManagementFactory; import java.net.InetSocketAddress; +import java.security.KeyStore; import java.util.concurrent.TimeUnit; import javax.management.MBeanServer; @@ -35,15 +37,21 @@ import org.eclipse.jetty.jmx.MBeanContainer; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.QueuedThreadPool; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.RegisterExtension; +@ExtendWith(WorkDirExtension.class) public class AbstractClientServerTest { + public WorkDir workDir; + @RegisterExtension final BeforeTestExecutionCallback printMethodName = context -> System.err.printf("Running %s.%s() %s%n", context.getRequiredTestClass().getSimpleName(), context.getRequiredTestMethod().getName(), context.getDisplayName()); @@ -81,6 +89,7 @@ public class AbstractClientServerTest serverThreads.setName("server"); server = new Server(serverThreads); connector = new HTTP3ServerConnector(server, sslContextFactory, serverConnectionFactory); + connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); server.addConnector(connector); MBeanContainer mbeanContainer = new MBeanContainer(ManagementFactory.getPlatformMBeanServer()); server.addBean(mbeanContainer); @@ -88,8 +97,16 @@ public class AbstractClientServerTest protected void startClient() throws Exception { + KeyStore trustStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/keystore.p12")) + { + trustStore.load(is, "storepwd".toCharArray()); + } + http3Client = new HTTP3Client(); - http3Client.getQuicConfiguration().setVerifyPeerCertificates(false); + SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(); + clientSslContextFactory.setTrustStore(trustStore); + http3Client.getClientConnector().setSslContextFactory(clientSslContextFactory); httpClient = new HttpClient(new HttpClientTransportDynamic(new ClientConnectionFactoryOverHTTP3.HTTP3(http3Client))); QueuedThreadPool clientThreads = new QueuedThreadPool(); clientThreads.setName("client"); diff --git a/jetty-http3/http3-tests/src/test/resources/keystore.p12 b/jetty-http3/http3-tests/src/test/resources/keystore.p12 index 0b56dd34ee9..8ab40f72afd 100644 Binary files a/jetty-http3/http3-tests/src/test/resources/keystore.p12 and b/jetty-http3/http3-tests/src/test/resources/keystore.p12 differ diff --git a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java index 4fcd115dd23..0f809ce409b 100644 --- a/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java +++ b/jetty-io/src/main/java/org/eclipse/jetty/io/ClientConnector.java @@ -117,6 +117,7 @@ public class ClientConnector extends ContainerLifeCycle { this.configurator = Objects.requireNonNull(configurator); addBean(configurator); + configurator.addBean(this, false); } /** @@ -588,7 +589,7 @@ public class ClientConnector extends ContainerLifeCycle /** *

Configures a {@link ClientConnector}.

*/ - public static class Configurator + public static class Configurator extends ContainerLifeCycle { /** *

Returns whether the connection to a given {@link SocketAddress} is intrinsically secure.

diff --git a/jetty-quic/quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConnection.java b/jetty-quic/quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConnection.java index 697cc3aa7fb..3edb7e493d6 100644 --- a/jetty-quic/quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConnection.java +++ b/jetty-quic/quic-client/src/main/java/org/eclipse/jetty/quic/client/ClientQuicConnection.java @@ -80,7 +80,11 @@ public class ClientQuicConnection extends QuicConnection QuicheConfig quicheConfig = new QuicheConfig(); quicheConfig.setApplicationProtos(protocols.toArray(String[]::new)); quicheConfig.setDisableActiveMigration(quicConfiguration.isDisableActiveMigration()); - quicheConfig.setVerifyPeer(quicConfiguration.isVerifyPeerCertificates()); + quicheConfig.setVerifyPeer(!connector.getSslContextFactory().isTrustAll()); + Map implCtx = quicConfiguration.getImplementationConfiguration(); + quicheConfig.setTrustedCertsPemPath((String)implCtx.get(QuicClientConnectorConfigurator.TRUSTED_CERTIFICATES_PEM_PATH_KEY)); + quicheConfig.setPrivKeyPemPath((String)implCtx.get(QuicClientConnectorConfigurator.PRIVATE_KEY_PEM_PATH_KEY)); + quicheConfig.setCertChainPemPath((String)implCtx.get(QuicClientConnectorConfigurator.CERTIFICATE_CHAIN_PEM_PATH_KEY)); // Idle timeouts must not be managed by Quiche. quicheConfig.setMaxIdleTimeout(0L); quicheConfig.setInitialMaxData((long)quicConfiguration.getSessionRecvWindow()); @@ -147,6 +151,13 @@ public class ClientQuicConnection extends QuicConnection return null; } + @Override + protected void onFailure(Throwable failure) + { + pendingSessions.values().forEach(session -> outwardClose(session, failure)); + super.onFailure(failure); + } + @Override public boolean onIdleExpired() { diff --git a/jetty-quic/quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicClientConnectorConfigurator.java b/jetty-quic/quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicClientConnectorConfigurator.java index ee8040af351..2d471a85be0 100644 --- a/jetty-quic/quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicClientConnectorConfigurator.java +++ b/jetty-quic/quic-client/src/main/java/org/eclipse/jetty/quic/client/QuicClientConnectorConfigurator.java @@ -19,6 +19,9 @@ import java.nio.channels.DatagramChannel; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.channels.SocketChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; import java.util.Map; import java.util.Objects; import java.util.function.UnaryOperator; @@ -30,6 +33,10 @@ import org.eclipse.jetty.io.EndPoint; import org.eclipse.jetty.io.ManagedSelector; import org.eclipse.jetty.io.SocketChannelEndPoint; import org.eclipse.jetty.quic.common.QuicConfiguration; +import org.eclipse.jetty.quic.quiche.PemExporter; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** *

A QUIC specific {@link ClientConnector.Configurator}.

@@ -41,8 +48,17 @@ import org.eclipse.jetty.quic.common.QuicConfiguration; */ public class QuicClientConnectorConfigurator extends ClientConnector.Configurator { + private static final Logger LOG = LoggerFactory.getLogger(QuicClientConnectorConfigurator.class); + + static final String PRIVATE_KEY_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".privateKeyPemPath"; + static final String CERTIFICATE_CHAIN_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".certificateChainPemPath"; + static final String TRUSTED_CERTIFICATES_PEM_PATH_KEY = QuicClientConnectorConfigurator.class.getName() + ".trustedCertificatesPemPath"; + private final QuicConfiguration configuration = new QuicConfiguration(); private final UnaryOperator configurator; + private Path privateKeyPemPath; + private Path certificateChainPemPath; + private Path trustedCertificatesPemPath; public QuicClientConnectorConfigurator() { @@ -56,7 +72,6 @@ public class QuicClientConnectorConfigurator extends ClientConnector.Configurato configuration.setSessionRecvWindow(16 * 1024 * 1024); configuration.setBidirectionalStreamRecvWindow(8 * 1024 * 1024); configuration.setDisableActiveMigration(true); - configuration.setVerifyPeerCertificates(true); } public QuicConfiguration getQuicConfiguration() @@ -64,6 +79,64 @@ public class QuicClientConnectorConfigurator extends ClientConnector.Configurato return configuration; } + @Override + protected void doStart() throws Exception + { + Path pemWorkDirectory = configuration.getPemWorkDirectory(); + ClientConnector clientConnector = getBean(ClientConnector.class); + SslContextFactory.Client sslContextFactory = clientConnector.getSslContextFactory(); + KeyStore trustStore = sslContextFactory.getTrustStore(); + if (trustStore != null) + { + trustedCertificatesPemPath = PemExporter.exportTrustStore(trustStore, pemWorkDirectory != null ? pemWorkDirectory : Path.of(System.getProperty("java.io.tmpdir"))); + configuration.getImplementationConfiguration().put(TRUSTED_CERTIFICATES_PEM_PATH_KEY, trustedCertificatesPemPath.toString()); + } + String certAlias = sslContextFactory.getCertAlias(); + if (certAlias != null) + { + if (pemWorkDirectory == null) + throw new IllegalStateException("No PEM work directory configured"); + KeyStore keyStore = sslContextFactory.getKeyStore(); + String keyManagerPassword = sslContextFactory.getKeyManagerPassword(); + char[] password = keyManagerPassword == null ? sslContextFactory.getKeyStorePassword().toCharArray() : keyManagerPassword.toCharArray(); + Path[] keyPair = PemExporter.exportKeyPair(keyStore, certAlias, password, pemWorkDirectory); + privateKeyPemPath = keyPair[0]; + certificateChainPemPath = keyPair[1]; + configuration.getImplementationConfiguration().put(PRIVATE_KEY_PEM_PATH_KEY, privateKeyPemPath.toString()); + configuration.getImplementationConfiguration().put(CERTIFICATE_CHAIN_PEM_PATH_KEY, certificateChainPemPath.toString()); + } + super.doStart(); + } + + @Override + protected void doStop() throws Exception + { + super.doStop(); + deleteFile(privateKeyPemPath); + privateKeyPemPath = null; + configuration.getImplementationConfiguration().remove(PRIVATE_KEY_PEM_PATH_KEY); + deleteFile(certificateChainPemPath); + certificateChainPemPath = null; + configuration.getImplementationConfiguration().remove(CERTIFICATE_CHAIN_PEM_PATH_KEY); + deleteFile(trustedCertificatesPemPath); + trustedCertificatesPemPath = null; + configuration.getImplementationConfiguration().remove(TRUSTED_CERTIFICATES_PEM_PATH_KEY); + } + + private void deleteFile(Path file) + { + try + { + if (file != null) + Files.delete(file); + } + catch (IOException x) + { + if (LOG.isDebugEnabled()) + LOG.debug("could not delete {}", file, x); + } + } + @Override public boolean isIntrinsicallySecure(ClientConnector clientConnector, SocketAddress address) { @@ -74,6 +147,7 @@ public class QuicClientConnectorConfigurator extends ClientConnector.Configurato public ChannelWithAddress newChannelWithAddress(ClientConnector clientConnector, SocketAddress address, Map context) throws IOException { context.put(QuicConfiguration.CONTEXT_KEY, configuration); + DatagramChannel channel = DatagramChannel.open(); if (clientConnector.getBindAddress() == null) { diff --git a/jetty-quic/quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientTest.java b/jetty-quic/quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientTest.java index 3ecb95d27d3..cd69f4eda82 100644 --- a/jetty-quic/quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientTest.java +++ b/jetty-quic/quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientTest.java @@ -14,7 +14,9 @@ package org.eclipse.jetty.quic.client; import java.io.IOException; +import java.io.InputStream; import java.io.PrintWriter; +import java.security.KeyStore; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -36,18 +38,24 @@ import org.eclipse.jetty.server.HttpConnectionFactory; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.eclipse.jetty.util.component.LifeCycle; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; +@ExtendWith(WorkDirExtension.class) public class End2EndClientTest { + public WorkDir workDir; + private Server server; private QuicServerConnector connector; private HttpClient client; @@ -61,8 +69,14 @@ public class End2EndClientTest @BeforeEach public void setUp() throws Exception { + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/keystore.p12")) + { + keyStore.load(is, "storepwd".toCharArray()); + } + SslContextFactory.Server sslContextFactory = new SslContextFactory.Server(); - sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); + sslContextFactory.setKeyStore(keyStore); sslContextFactory.setKeyStorePassword("storepwd"); server = new Server(); @@ -71,6 +85,7 @@ public class End2EndClientTest HttpConnectionFactory http1 = new HttpConnectionFactory(httpConfiguration); HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpConfiguration); connector = new QuicServerConnector(server, sslContextFactory, http1, http2); + connector.getQuicConfiguration().setPemWorkDirectory(workDir.getEmptyPathDir()); server.addConnector(connector); server.setHandler(new AbstractHandler() @@ -86,11 +101,13 @@ public class End2EndClientTest server.start(); + ClientConnector clientConnector = new ClientConnector(new QuicClientConnectorConfigurator()); + SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(); + clientSslContextFactory.setTrustStore(keyStore); + clientConnector.setSslContextFactory(clientSslContextFactory); ClientConnectionFactory.Info http1Info = HttpClientConnectionFactory.HTTP11; - ClientConnectionFactoryOverHTTP2.HTTP2 http2Info = new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client()); - QuicClientConnectorConfigurator configurator = new QuicClientConnectorConfigurator(); - configurator.getQuicConfiguration().setVerifyPeerCertificates(false); - HttpClientTransportDynamic transport = new HttpClientTransportDynamic(new ClientConnector(configurator), http1Info, http2Info); + ClientConnectionFactoryOverHTTP2.HTTP2 http2Info = new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector)); + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(clientConnector, http1Info, http2Info); client = new HttpClient(transport); client.start(); } diff --git a/jetty-quic/quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientWithClientCertAuthTest.java b/jetty-quic/quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientWithClientCertAuthTest.java new file mode 100644 index 00000000000..f2160af3b76 --- /dev/null +++ b/jetty-quic/quic-client/src/test/java/org/eclipse/jetty/quic/client/End2EndClientWithClientCertAuthTest.java @@ -0,0 +1,165 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.quic.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.dynamic.HttpClientTransportDynamic; +import org.eclipse.jetty.client.http.HttpClientConnectionFactory; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.ClientConnectionFactoryOverHTTP2; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.io.ClientConnectionFactory; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.quic.server.QuicServerConnector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.eclipse.jetty.util.component.LifeCycle; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(WorkDirExtension.class) +public class End2EndClientWithClientCertAuthTest +{ + public WorkDir workDir; + + private Server server; + private QuicServerConnector connector; + private HttpClient client; + private final String responseContent = "" + + "\n" + + "\t\n" + + "\t\tRequest served\n" + + "\t\n" + + ""; + private SslContextFactory.Server serverSslContextFactory; + + @BeforeEach + public void setUp() throws Exception + { + Path workPath = workDir.getEmptyPathDir(); + Path serverWorkPath = workPath.resolve("server"); + Files.createDirectories(serverWorkPath); + Path clientWorkPath = workPath.resolve("client"); + Files.createDirectories(clientWorkPath); + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/keystore.p12")) + { + keyStore.load(is, "storepwd".toCharArray()); + } + + serverSslContextFactory = new SslContextFactory.Server(); + serverSslContextFactory.setKeyStore(keyStore); + serverSslContextFactory.setKeyStorePassword("storepwd"); + serverSslContextFactory.setTrustStore(keyStore); + serverSslContextFactory.setNeedClientAuth(true); + + server = new Server(); + + HttpConfiguration httpConfiguration = new HttpConfiguration(); + httpConfiguration.addCustomizer(new SecureRequestCustomizer()); + HttpConnectionFactory http1 = new HttpConnectionFactory(httpConfiguration); + HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpConfiguration); + connector = new QuicServerConnector(server, serverSslContextFactory, http1, http2); + connector.getQuicConfiguration().setPemWorkDirectory(serverWorkPath); + server.addConnector(connector); + + server.setHandler(new AbstractHandler() + { + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException + { + baseRequest.setHandled(true); + PrintWriter writer = response.getWriter(); + writer.print(responseContent); + } + }); + + server.start(); + + QuicClientConnectorConfigurator configurator = new QuicClientConnectorConfigurator(); + configurator.getQuicConfiguration().setPemWorkDirectory(clientWorkPath); + ClientConnector clientConnector = new ClientConnector(configurator); + SslContextFactory.Client clientSslContextFactory = new SslContextFactory.Client(); + clientSslContextFactory.setCertAlias("mykey"); + clientSslContextFactory.setKeyStore(keyStore); + clientSslContextFactory.setKeyStorePassword("storepwd"); + clientSslContextFactory.setTrustStore(keyStore); + clientConnector.setSslContextFactory(clientSslContextFactory); + ClientConnectionFactory.Info http1Info = HttpClientConnectionFactory.HTTP11; + ClientConnectionFactoryOverHTTP2.HTTP2 http2Info = new ClientConnectionFactoryOverHTTP2.HTTP2(new HTTP2Client(clientConnector)); + HttpClientTransportDynamic transport = new HttpClientTransportDynamic(clientConnector, http1Info, http2Info); + client = new HttpClient(transport); + client.start(); + } + + @AfterEach + public void tearDown() + { + LifeCycle.stop(client); + LifeCycle.stop(server); + } + + @Test + public void testWorkingClientAuth() throws Exception + { + ContentResponse response = client.newRequest("https://localhost:" + connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + assertThat(response.getStatus(), is(200)); + String contentAsString = response.getContentAsString(); + assertThat(contentAsString, is(responseContent)); + } + + @Test + public void testServerRejectsClientInvalidCert() throws Exception + { + // remove the trust store config from the server + server.stop(); + serverSslContextFactory.setTrustStore(null); + server.start(); + + assertThrows(TimeoutException.class, () -> + { + ContentResponse response = client.newRequest("https://localhost:" + connector.getLocalPort()) + .timeout(5, TimeUnit.SECONDS) + .send(); + }); + } +} diff --git a/jetty-quic/quic-client/src/test/resources/keystore.p12 b/jetty-quic/quic-client/src/test/resources/keystore.p12 index 0b56dd34ee9..8ab40f72afd 100644 Binary files a/jetty-quic/quic-client/src/test/resources/keystore.p12 and b/jetty-quic/quic-client/src/test/resources/keystore.p12 differ diff --git a/jetty-quic/quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConfiguration.java b/jetty-quic/quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConfiguration.java index 8005bb98e6d..ba74b7648d4 100644 --- a/jetty-quic/quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConfiguration.java +++ b/jetty-quic/quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicConfiguration.java @@ -13,7 +13,10 @@ package org.eclipse.jetty.quic.common; +import java.nio.file.Path; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** *

A record that captures QUIC configuration parameters.

@@ -24,12 +27,13 @@ public class QuicConfiguration private List protocols = List.of(); private boolean disableActiveMigration; - private boolean verifyPeerCertificates; private int maxBidirectionalRemoteStreams; private int maxUnidirectionalRemoteStreams; private int sessionRecvWindow; private int bidirectionalStreamRecvWindow; private int unidirectionalStreamRecvWindow; + private Path pemWorkDirectory; + private final Map implementationConfiguration = new HashMap<>(); public List getProtocols() { @@ -51,16 +55,6 @@ public class QuicConfiguration this.disableActiveMigration = disableActiveMigration; } - public boolean isVerifyPeerCertificates() - { - return verifyPeerCertificates; - } - - public void setVerifyPeerCertificates(boolean verifyPeerCertificates) - { - this.verifyPeerCertificates = verifyPeerCertificates; - } - public int getMaxBidirectionalRemoteStreams() { return maxBidirectionalRemoteStreams; @@ -110,4 +104,19 @@ public class QuicConfiguration { this.unidirectionalStreamRecvWindow = unidirectionalStreamRecvWindow; } + + public Path getPemWorkDirectory() + { + return pemWorkDirectory; + } + + public void setPemWorkDirectory(Path pemWorkDirectory) + { + this.pemWorkDirectory = pemWorkDirectory; + } + + public Map getImplementationConfiguration() + { + return implementationConfiguration; + } } diff --git a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/PemExporter.java b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/PemExporter.java new file mode 100644 index 00000000000..cba6a9aeaa5 --- /dev/null +++ b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/PemExporter.java @@ -0,0 +1,130 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.quic.quiche; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.PosixFilePermission; +import java.security.Key; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.util.Base64; +import java.util.Enumeration; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PemExporter +{ + private static final Logger LOG = LoggerFactory.getLogger(PemExporter.class); + + private static final byte[] BEGIN_KEY = "-----BEGIN PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + private static final byte[] END_KEY = "-----END PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); + private static final byte[] BEGIN_CERT = "-----BEGIN CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII); + private static final byte[] END_CERT = "-----END CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII); + private static final byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes(StandardCharsets.US_ASCII); + private static final Base64.Encoder ENCODER = Base64.getMimeEncoder(64, LINE_SEPARATOR); + + private PemExporter() + { + } + + /** + * @return a temp file that gets deleted on exit + */ + public static Path exportTrustStore(KeyStore keyStore, Path targetFolder) throws Exception + { + if (!Files.isDirectory(targetFolder)) + throw new IllegalArgumentException("Target folder is not a directory: " + targetFolder); + + Path path = Files.createTempFile(targetFolder, "truststore-", ".crt"); + try (OutputStream os = Files.newOutputStream(path)) + { + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) + { + String alias = aliases.nextElement(); + Certificate cert = keyStore.getCertificate(alias); + writeAsPEM(os, cert); + } + } + return path; + } + + /** + * @return [0] is the key file, [1] is the cert file. + */ + public static Path[] exportKeyPair(KeyStore keyStore, String alias, char[] keyPassword, Path targetFolder) throws Exception + { + if (!Files.isDirectory(targetFolder)) + throw new IllegalArgumentException("Target folder is not a directory: " + targetFolder); + + Path[] paths = new Path[2]; + paths[1] = targetFolder.resolve(alias + ".crt"); + try (OutputStream os = Files.newOutputStream(paths[1])) + { + Certificate[] certChain = keyStore.getCertificateChain(alias); + for (Certificate cert : certChain) + writeAsPEM(os, cert); + Files.setPosixFilePermissions(paths[1], Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + } + catch (UnsupportedOperationException e) + { + // Expected on Windows. + if (LOG.isDebugEnabled()) + LOG.debug("Unable to set Posix file permissions", e); + } + paths[0] = targetFolder.resolve(alias + ".key"); + try (OutputStream os = Files.newOutputStream(paths[0])) + { + Key key = keyStore.getKey(alias, keyPassword); + writeAsPEM(os, key); + Files.setPosixFilePermissions(paths[0], Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)); + } + catch (UnsupportedOperationException e) + { + // Expected on Windows. + if (LOG.isDebugEnabled()) + LOG.debug("Unable to set Posix file permissions", e); + } + return paths; + } + + private static void writeAsPEM(OutputStream outputStream, Key key) throws IOException + { + byte[] encoded = ENCODER.encode(key.getEncoded()); + outputStream.write(BEGIN_KEY); + outputStream.write(LINE_SEPARATOR); + outputStream.write(encoded); + outputStream.write(LINE_SEPARATOR); + outputStream.write(END_KEY); + outputStream.write(LINE_SEPARATOR); + } + + private static void writeAsPEM(OutputStream outputStream, Certificate certificate) throws CertificateEncodingException, IOException + { + byte[] encoded = ENCODER.encode(certificate.getEncoded()); + outputStream.write(BEGIN_CERT); + outputStream.write(LINE_SEPARATOR); + outputStream.write(encoded); + outputStream.write(LINE_SEPARATOR); + outputStream.write(END_CERT); + outputStream.write(LINE_SEPARATOR); + } +} diff --git a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/Quiche.java b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/Quiche.java index f4d79d6f2c6..c7cf2c4c64d 100644 --- a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/Quiche.java +++ b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/Quiche.java @@ -119,4 +119,164 @@ public interface Quiche return "?? " + err; } } + + // QUIC Transport Error Codes: https://www.iana.org/assignments/quic/quic.xhtml#quic-transport-error-codes + interface quic_error + { + long NO_ERROR = 0, + INTERNAL_ERROR = 1, + CONNECTION_REFUSED = 2, + FLOW_CONTROL_ERROR = 3, + STREAM_LIMIT_ERROR = 4, + STREAM_STATE_ERROR = 5, + FINAL_SIZE_ERROR = 6, + FRAME_ENCODING_ERROR = 7, + TRANSPORT_PARAMETER_ERROR = 8, + CONNECTION_ID_LIMIT_ERROR = 9, + PROTOCOL_VIOLATION = 10, + INVALID_TOKEN = 11, + APPLICATION_ERROR = 12, + CRYPTO_BUFFER_EXCEEDED = 13, + KEY_UPDATE_ERROR = 14, + AEAD_LIMIT_REACHED = 15, + NO_VIABLE_PATH = 16, + VERSION_NEGOTIATION_ERROR = 17; + + static String errToString(long err) + { + if (err == NO_ERROR) + return "NO_ERROR"; + if (err == INTERNAL_ERROR) + return "INTERNAL_ERROR"; + if (err == CONNECTION_REFUSED) + return "CONNECTION_REFUSED"; + if (err == FLOW_CONTROL_ERROR) + return "FLOW_CONTROL_ERROR"; + if (err == STREAM_LIMIT_ERROR) + return "STREAM_LIMIT_ERROR"; + if (err == STREAM_STATE_ERROR) + return "STREAM_STATE_ERROR"; + if (err == FINAL_SIZE_ERROR) + return "FINAL_SIZE_ERROR"; + if (err == FRAME_ENCODING_ERROR) + return "FRAME_ENCODING_ERROR"; + if (err == TRANSPORT_PARAMETER_ERROR) + return "TRANSPORT_PARAMETER_ERROR"; + if (err == CONNECTION_ID_LIMIT_ERROR) + return "CONNECTION_ID_LIMIT_ERROR"; + if (err == PROTOCOL_VIOLATION) + return "PROTOCOL_VIOLATION"; + if (err == INVALID_TOKEN) + return "INVALID_TOKEN"; + if (err == APPLICATION_ERROR) + return "APPLICATION_ERROR"; + if (err == CRYPTO_BUFFER_EXCEEDED) + return "CRYPTO_BUFFER_EXCEEDED"; + if (err == KEY_UPDATE_ERROR) + return "KEY_UPDATE_ERROR"; + if (err == AEAD_LIMIT_REACHED) + return "AEAD_LIMIT_REACHED"; + if (err == NO_VIABLE_PATH) + return "NO_VIABLE_PATH"; + if (err == VERSION_NEGOTIATION_ERROR) + return "VERSION_NEGOTIATION_ERROR"; + if (err >= 0x100 && err <= 0x01FF) + return "CRYPTO_ERROR " + tls_alert.errToString(err - 0x100); + return "?? " + err; + } + } + + // TLS Alerts: https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-6 + interface tls_alert + { + long CLOSE_NOTIFY = 0, + UNEXPECTED_MESSAGE = 10, + BAD_RECORD_MAC = 20, + RECORD_OVERFLOW = 22, + HANDSHAKE_FAILURE = 40, + BAD_CERTIFICATE = 42, + UNSUPPORTED_CERTIFICATE = 43, + CERTIFICATE_REVOKED = 44, + CERTIFICATE_EXPIRED = 45, + CERTIFICATE_UNKNOWN = 46, + ILLEGAL_PARAMETER = 47, + UNKNOWN_CA = 48, + ACCESS_DENIED = 49, + DECODE_ERROR = 50, + DECRYPT_ERROR = 51, + TOO_MANY_CIDS_REQUESTED = 52, + PROTOCOL_VERSION = 70, + INSUFFICIENT_SECURITY = 71, + INTERNAL_ERROR = 80, + INAPPROPRIATE_FALLBACK = 86, + USER_CANCELED = 90, + MISSING_EXTENSION = 109, + UNSUPPORTED_EXTENSION = 110, + UNRECOGNIZED_NAME = 112, + BAD_CERTIFICATE_STATUS_RESPONSE = 113, + UNKNOWN_PSK_IDENTITY = 115, + CERTIFICATE_REQUIRED = 116, + NO_APPLICATION_PROTOCOL = 120; + + static String errToString(long err) + { + if (err == CLOSE_NOTIFY) + return "CLOSE_NOTIFY"; + if (err == UNEXPECTED_MESSAGE) + return "UNEXPECTED_MESSAGE"; + if (err == BAD_RECORD_MAC) + return "BAD_RECORD_MAC"; + if (err == RECORD_OVERFLOW) + return "RECORD_OVERFLOW"; + if (err == HANDSHAKE_FAILURE) + return "HANDSHAKE_FAILURE"; + if (err == BAD_CERTIFICATE) + return "BAD_CERTIFICATE"; + if (err == UNSUPPORTED_CERTIFICATE) + return "UNSUPPORTED_CERTIFICATE"; + if (err == CERTIFICATE_REVOKED) + return "CERTIFICATE_REVOKED"; + if (err == CERTIFICATE_EXPIRED) + return "CERTIFICATE_EXPIRED"; + if (err == CERTIFICATE_UNKNOWN) + return "CERTIFICATE_UNKNOWN"; + if (err == ILLEGAL_PARAMETER) + return "ILLEGAL_PARAMETER"; + if (err == UNKNOWN_CA) + return "UNKNOWN_CA"; + if (err == ACCESS_DENIED) + return "ACCESS_DENIED"; + if (err == DECODE_ERROR) + return "DECODE_ERROR"; + if (err == DECRYPT_ERROR) + return "DECRYPT_ERROR"; + if (err == TOO_MANY_CIDS_REQUESTED) + return "TOO_MANY_CIDS_REQUESTED"; + if (err == PROTOCOL_VERSION) + return "PROTOCOL_VERSION"; + if (err == INSUFFICIENT_SECURITY) + return "INSUFFICIENT_SECURITY"; + if (err == INTERNAL_ERROR) + return "INTERNAL_ERROR"; + if (err == INAPPROPRIATE_FALLBACK) + return "INAPPROPRIATE_FALLBACK"; + if (err == USER_CANCELED) + return "USER_CANCELED"; + if (err == MISSING_EXTENSION) + return "MISSING_EXTENSION"; + if (err == UNSUPPORTED_EXTENSION) + return "UNSUPPORTED_EXTENSION"; + if (err == UNRECOGNIZED_NAME) + return "UNRECOGNIZED_NAME"; + if (err == BAD_CERTIFICATE_STATUS_RESPONSE) + return "BAD_CERTIFICATE_STATUS_RESPONSE"; + if (err == UNKNOWN_PSK_IDENTITY) + return "UNKNOWN_PSK_IDENTITY"; + if (err == CERTIFICATE_REQUIRED) + return "CERTIFICATE_REQUIRED"; + if (err == NO_APPLICATION_PROTOCOL) + return "NO_APPLICATION_PROTOCOL"; + return "?? " + err; + } + } } diff --git a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConfig.java b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConfig.java index 33b36a41ea7..08fcff2d4d3 100644 --- a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConfig.java +++ b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConfig.java @@ -35,6 +35,7 @@ public class QuicheConfig private int version = Quiche.QUICHE_PROTOCOL_VERSION; private Boolean verifyPeer; + private String trustedCertsPemPath; private String certChainPemPath; private String privKeyPemPath; private String[] applicationProtos; @@ -65,6 +66,11 @@ public class QuicheConfig return verifyPeer; } + public String getTrustedCertsPemPath() + { + return trustedCertsPemPath; + } + public String getCertChainPemPath() { return certChainPemPath; @@ -150,6 +156,11 @@ public class QuicheConfig this.verifyPeer = verify; } + public void setTrustedCertsPemPath(String trustedCertsPemPath) + { + this.trustedCertsPemPath = trustedCertsPemPath; + } + public void setCertChainPemPath(String path) { this.certChainPemPath = path; diff --git a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java index 2a32da8738f..a9cd0ccf532 100644 --- a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java +++ b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java @@ -150,6 +150,8 @@ public abstract class QuicheConnection public abstract CloseInfo getRemoteCloseInfo(); + public abstract CloseInfo getLocalCloseInfo(); + public static class CloseInfo { private final long error; diff --git a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/SSLKeyPair.java b/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/SSLKeyPair.java deleted file mode 100644 index 17a8ad36538..00000000000 --- a/jetty-quic/quic-quiche/quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/SSLKeyPair.java +++ /dev/null @@ -1,100 +0,0 @@ -// -// ======================================================================== -// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. -// -// This program and the accompanying materials are made available under the -// terms of the Eclipse Public License v. 2.0 which is available at -// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 -// which is available at https://www.apache.org/licenses/LICENSE-2.0. -// -// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 -// ======================================================================== -// - -package org.eclipse.jetty.quic.quiche; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.security.Key; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.UnrecoverableKeyException; -import java.security.cert.Certificate; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.util.Base64; - -public class SSLKeyPair -{ - private static final byte[] BEGIN_KEY = "-----BEGIN PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); - private static final byte[] END_KEY = "-----END PRIVATE KEY-----".getBytes(StandardCharsets.US_ASCII); - private static final byte[] BEGIN_CERT = "-----BEGIN CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII); - private static final byte[] END_CERT = "-----END CERTIFICATE-----".getBytes(StandardCharsets.US_ASCII); - private static final byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes(StandardCharsets.US_ASCII); - private static final int LINE_LENGTH = 64; - - private final Base64.Encoder encoder = Base64.getMimeEncoder(LINE_LENGTH, LINE_SEPARATOR); - private final Key key; - private final Certificate[] certChain; - private final String alias; - - public SSLKeyPair(File storeFile, String storeType, char[] storePassword, String alias, char[] keyPassword) throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException, IOException, CertificateException - { - KeyStore keyStore = KeyStore.getInstance(storeType); - try (FileInputStream fis = new FileInputStream(storeFile)) - { - keyStore.load(fis, storePassword); - this.alias = alias; - this.key = keyStore.getKey(alias, keyPassword); - this.certChain = keyStore.getCertificateChain(alias); - } - } - - /** - * @return [0] is the key file, [1] is the cert file. - */ - public File[] export(File targetFolder) throws Exception - { - File[] files = new File[2]; - files[0] = new File(targetFolder, alias + ".key"); - files[1] = new File(targetFolder, alias + ".crt"); - - try (FileOutputStream fos = new FileOutputStream(files[0])) - { - writeAsPEM(fos, key); - } - try (FileOutputStream fos = new FileOutputStream(files[1])) - { - for (Certificate cert : certChain) - writeAsPEM(fos, cert); - } - return files; - } - - private void writeAsPEM(OutputStream outputStream, Key key) throws IOException - { - byte[] encoded = encoder.encode(key.getEncoded()); - outputStream.write(BEGIN_KEY); - outputStream.write(LINE_SEPARATOR); - outputStream.write(encoded); - outputStream.write(LINE_SEPARATOR); - outputStream.write(END_KEY); - outputStream.write(LINE_SEPARATOR); - } - - private void writeAsPEM(OutputStream outputStream, Certificate certificate) throws CertificateEncodingException, IOException - { - byte[] encoded = encoder.encode(certificate.getEncoded()); - outputStream.write(BEGIN_CERT); - outputStream.write(LINE_SEPARATOR); - outputStream.write(encoded); - outputStream.write(LINE_SEPARATOR); - outputStream.write(END_CERT); - outputStream.write(LINE_SEPARATOR); - } -} diff --git a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/ForeignIncubatorQuicheConnection.java b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/ForeignIncubatorQuicheConnection.java index d8b4f87dc22..e3f8057f5b7 100644 --- a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/ForeignIncubatorQuicheConnection.java +++ b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/ForeignIncubatorQuicheConnection.java @@ -28,7 +28,9 @@ import jdk.incubator.foreign.CLinker; import jdk.incubator.foreign.MemoryAddress; import jdk.incubator.foreign.MemorySegment; import jdk.incubator.foreign.ResourceScope; +import org.eclipse.jetty.quic.quiche.Quiche; import org.eclipse.jetty.quic.quiche.Quiche.quiche_error; +import org.eclipse.jetty.quic.quiche.Quiche.quic_error; import org.eclipse.jetty.quic.quiche.QuicheConfig; import org.eclipse.jetty.quic.quiche.QuicheConnection; import org.eclipse.jetty.util.BufferUtil; @@ -148,7 +150,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection MemorySegment localSockaddr = sockaddr.convert(local, scope); MemorySegment peerSockaddr = sockaddr.convert(peer, scope); - MemoryAddress quicheConn = quiche_h.quiche_connect(CLinker.toCString(peer.getHostName(), scope), scid, scid.byteSize(), localSockaddr, localSockaddr.byteSize(), peerSockaddr, peerSockaddr.byteSize(), libQuicheConfig); + MemoryAddress quicheConn = quiche_h.quiche_connect(CLinker.toCString(peer.getHostString(), scope), scid, scid.byteSize(), localSockaddr, localSockaddr.byteSize(), peerSockaddr, peerSockaddr.byteSize(), libQuicheConfig); ForeignIncubatorQuicheConnection connection = new ForeignIncubatorQuicheConnection(quicheConn, libQuicheConfig, scope); keepScope = true; return connection; @@ -170,13 +172,29 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection if (verifyPeer != null) quiche_h.quiche_config_verify_peer(quicheConfig, verifyPeer ? C_TRUE : C_FALSE); + String trustedCertsPemPath = config.getTrustedCertsPemPath(); + if (trustedCertsPemPath != null) + { + int rc = quiche_h.quiche_config_load_verify_locations_from_file(quicheConfig, CLinker.toCString(trustedCertsPemPath, scope).address()); + if (rc < 0) + throw new IOException("Error loading trusted certificates file " + trustedCertsPemPath + " : " + Quiche.quiche_error.errToString(rc)); + } + String certChainPemPath = config.getCertChainPemPath(); if (certChainPemPath != null) - quiche_h.quiche_config_load_cert_chain_from_pem_file(quicheConfig, CLinker.toCString(certChainPemPath, scope).address()); + { + int rc = quiche_h.quiche_config_load_cert_chain_from_pem_file(quicheConfig, CLinker.toCString(certChainPemPath, scope).address()); + if (rc < 0) + throw new IOException("Error loading certificate chain file " + certChainPemPath + " : " + Quiche.quiche_error.errToString(rc)); + } String privKeyPemPath = config.getPrivKeyPemPath(); if (privKeyPemPath != null) - quiche_h.quiche_config_load_priv_key_from_pem_file(quicheConfig, CLinker.toCString(privKeyPemPath, scope).address()); + { + int rc = quiche_h.quiche_config_load_priv_key_from_pem_file(quicheConfig, CLinker.toCString(privKeyPemPath, scope).address()); + if (rc < 0) + throw new IOException("Error loading private key file " + privKeyPemPath + " : " + Quiche.quiche_error.errToString(rc)); + } String[] applicationProtos = config.getApplicationProtos(); if (applicationProtos != null) @@ -483,6 +501,31 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection } } + public byte[] getPeerCertificate() + { + try (AutoLock ignore = lock.lock()) + { + if (quicheConn == null) + throw new IllegalStateException("connection was released"); + + try (ResourceScope scope = ResourceScope.newConfinedScope()) + { + MemorySegment outSegment = MemorySegment.allocateNative(CLinker.C_POINTER, scope); + MemorySegment outLenSegment = MemorySegment.allocateNative(CLinker.C_LONG, scope); + quiche_h.quiche_conn_peer_cert(quicheConn, outSegment.address(), outLenSegment.address()); + + long outLen = getLong(outLenSegment); + if (outLen == 0L) + return null; + byte[] out = new byte[(int)outLen]; + // dereference outSegment pointer + MemoryAddress memoryAddress = MemoryAddress.ofLong(getLong(outSegment)); + memoryAddress.asSegment(outLen, ResourceScope.globalScope()).asByteBuffer().get(out); + return out; + } + } + } + @Override protected List iterableStreamIds(boolean write) { @@ -541,8 +584,11 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection received = quiche_h.quiche_conn_recv(quicheConn, bufferSegment.address(), buffer.remaining(), recvInfo.address()); } } + // If quiche_conn_recv() fails, quiche_conn_local_error() can be called to get the standard error. if (received < 0) - throw new IOException("failed to receive packet; err=" + quiche_error.errToString(received)); + throw new IOException("failed to receive packet;" + + " quiche_err=" + quiche_error.errToString(received) + + " quic_err=" + quic_error.errToString(getLocalCloseInfo().error())); buffer.position((int)(buffer.position() + received)); return (int)received; } @@ -579,7 +625,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection if (written == quiche_error.QUICHE_ERR_DONE) return 0; if (written < 0L) - throw new IOException("failed to send packet; err=" + quiche_error.errToString(written)); + throw new IOException("failed to send packet; quiche_err=" + quiche_error.errToString(written)); buffer.position((int)(prevPosition + written)); return (int)written; } @@ -762,7 +808,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection if (value < 0) { if (LOG.isDebugEnabled()) - LOG.debug("could not read window capacity for stream {} err={}", streamId, quiche_error.errToString(value)); + LOG.debug("could not read window capacity for stream {} quiche_err={}", streamId, quiche_error.errToString(value)); } return value; } @@ -821,7 +867,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection if (written == quiche_error.QUICHE_ERR_DONE) return 0; if (written < 0L) - throw new IOException("failed to write to stream " + streamId + "; err=" + quiche_error.errToString(written)); + throw new IOException("failed to write to stream " + streamId + "; quiche_err=" + quiche_error.errToString(written)); buffer.position((int)(buffer.position() + written)); return (int)written; } @@ -862,7 +908,7 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection if (read == quiche_error.QUICHE_ERR_DONE) return isStreamFinished(streamId) ? -1 : 0; if (read < 0L) - throw new IOException("failed to read from stream " + streamId + "; err=" + quiche_error.errToString(read)); + throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); buffer.position((int)(buffer.position() + read)); return (int)read; } @@ -918,6 +964,45 @@ public class ForeignIncubatorQuicheConnection extends QuicheConnection } } + @Override + public CloseInfo getLocalCloseInfo() + { + try (AutoLock ignore = lock.lock()) + { + if (quicheConn == null) + throw new IllegalStateException("connection was released"); + try (ResourceScope scope = ResourceScope.newConfinedScope()) + { + MemorySegment app = MemorySegment.allocateNative(CLinker.C_CHAR, scope); + MemorySegment error = MemorySegment.allocateNative(CLinker.C_LONG, scope); + MemorySegment reason = MemorySegment.allocateNative(CLinker.C_POINTER, scope); + MemorySegment reasonLength = MemorySegment.allocateNative(CLinker.C_LONG, scope); + if (quiche_h.quiche_conn_local_error(quicheConn, app.address(), error.address(), reason.address(), reasonLength.address()) != C_FALSE) + { + long errorValue = getLong(error); + long reasonLengthValue = getLong(reasonLength); + + String reasonValue; + if (reasonLengthValue == 0L) + { + reasonValue = null; + } + else + { + byte[] reasonBytes = new byte[(int)reasonLengthValue]; + // dereference reason pointer + MemoryAddress memoryAddress = MemoryAddress.ofLong(getLong(reason)); + memoryAddress.asSegment(reasonLengthValue, ResourceScope.globalScope()).asByteBuffer().get(reasonBytes); + reasonValue = new String(reasonBytes, StandardCharsets.UTF_8); + } + + return new CloseInfo(errorValue, reasonValue); + } + return null; + } + } + } + private static void putLong(MemorySegment memorySegment, long value) { memorySegment.asByteBuffer().order(ByteOrder.nativeOrder()).putLong(value); diff --git a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/quiche_h.java b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/quiche_h.java index fb9803c066c..4e89ae1a303 100644 --- a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/quiche_h.java +++ b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/main/java/org/eclipse/jetty/quic/quiche/foreign/incubator/quiche_h.java @@ -52,6 +52,12 @@ public class quiche_h FunctionDescriptor.ofVoid(C_POINTER, C_INT) ); + private static final MethodHandle quiche_config_load_verify_locations_from_file$MH = downcallHandle( + "quiche_config_load_verify_locations_from_file", + "(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)I", + FunctionDescriptor.of(C_INT, C_POINTER, C_POINTER) + ); + private static final MethodHandle quiche_config_load_cert_chain_from_pem_file$MH = downcallHandle( "quiche_config_load_cert_chain_from_pem_file", "(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)I", @@ -250,12 +256,24 @@ public class quiche_h FunctionDescriptor.of(C_CHAR, C_POINTER) ); + private static final MethodHandle quiche_conn_peer_cert$MH = downcallHandle( + "quiche_conn_peer_cert", + "(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)V", + FunctionDescriptor.ofVoid(C_POINTER, C_POINTER, C_POINTER) + ); + private static final MethodHandle quiche_conn_peer_error$MH = downcallHandle( "quiche_conn_peer_error", "(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)B", FunctionDescriptor.of(C_CHAR, C_POINTER, C_POINTER, C_POINTER, C_POINTER, C_POINTER) ); + private static final MethodHandle quiche_conn_local_error$MH = downcallHandle( + "quiche_conn_local_error", + "(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)B", + FunctionDescriptor.of(C_CHAR, C_POINTER, C_POINTER, C_POINTER, C_POINTER, C_POINTER) + ); + private static final MethodHandle quiche_conn_stats$MH = downcallHandle( "quiche_conn_stats", "(Ljdk/incubator/foreign/MemoryAddress;Ljdk/incubator/foreign/MemoryAddress;)V", @@ -364,6 +382,18 @@ public class quiche_h } } + public static int quiche_config_load_verify_locations_from_file(MemoryAddress config, MemoryAddress path) + { + try + { + return (int) quiche_config_load_verify_locations_from_file$MH.invokeExact(config, path); + } + catch (Throwable ex) + { + throw new AssertionError("should not reach here", ex); + } + } + public static int quiche_config_load_cert_chain_from_pem_file(MemoryAddress config, MemoryAddress path) { try @@ -688,6 +718,18 @@ public class quiche_h } } + public static void quiche_conn_peer_cert(MemoryAddress conn, MemoryAddress out, MemoryAddress out_len) + { + try + { + quiche_conn_peer_cert$MH.invokeExact(conn, out, out_len); + } + catch (Throwable ex) + { + throw new AssertionError("should not reach here", ex); + } + } + public static byte quiche_conn_peer_error(MemoryAddress conn, MemoryAddress is_app, MemoryAddress error_code, MemoryAddress reason, MemoryAddress reason_len) { try @@ -700,6 +742,18 @@ public class quiche_h } } + public static byte quiche_conn_local_error(MemoryAddress conn, MemoryAddress is_app, MemoryAddress error_code, MemoryAddress reason, MemoryAddress reason_len) + { + try + { + return (byte) quiche_conn_local_error$MH.invokeExact(conn, is_app, error_code, reason, reason_len); + } + catch (Throwable ex) + { + throw new AssertionError("should not reach here", ex); + } + } + public static long quiche_conn_stream_capacity(MemoryAddress conn, long stream_id) { try diff --git a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/java/org/eclipse/jetty/quic/quiche/foreign/incubator/LowLevelQuicheClientCertTest.java b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/java/org/eclipse/jetty/quic/quiche/foreign/incubator/LowLevelQuicheClientCertTest.java new file mode 100644 index 00000000000..379b85dd3ea --- /dev/null +++ b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/java/org/eclipse/jetty/quic/quiche/foreign/incubator/LowLevelQuicheClientCertTest.java @@ -0,0 +1,249 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.quic.quiche.foreign.incubator; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import org.eclipse.jetty.quic.quiche.PemExporter; +import org.eclipse.jetty.quic.quiche.QuicheConfig; +import org.eclipse.jetty.quic.quiche.QuicheConnection; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; + +@ExtendWith(WorkDirExtension.class) +public class LowLevelQuicheClientCertTest +{ + public WorkDir workDir; + + private final Collection connectionsToDisposeOf = new ArrayList<>(); + + private InetSocketAddress clientSocketAddress; + private InetSocketAddress serverSocketAddress; + private QuicheConfig clientQuicheConfig; + private QuicheConfig serverQuicheConfig; + private ForeignIncubatorQuicheConnection.TokenMinter tokenMinter; + private ForeignIncubatorQuicheConnection.TokenValidator tokenValidator; + private Certificate[] serverCertificateChain; + + @BeforeEach + protected void setUp() throws Exception + { + clientSocketAddress = new InetSocketAddress("localhost", 9999); + serverSocketAddress = new InetSocketAddress("localhost", 8888); + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/keystore.p12")) + { + keyStore.load(is, "storepwd".toCharArray()); + } + Path targetFolder = workDir.getEmptyPathDir(); + Path[] keyPair = PemExporter.exportKeyPair(keyStore, "mykey", "storepwd".toCharArray(), targetFolder); + Path trustStorePath = PemExporter.exportTrustStore(keyStore, targetFolder); + + clientQuicheConfig = new QuicheConfig(); + clientQuicheConfig.setApplicationProtos("http/0.9"); + clientQuicheConfig.setDisableActiveMigration(true); + clientQuicheConfig.setPrivKeyPemPath(keyPair[0].toString()); + clientQuicheConfig.setCertChainPemPath(keyPair[1].toString()); + clientQuicheConfig.setVerifyPeer(true); + clientQuicheConfig.setTrustedCertsPemPath(trustStorePath.toString()); + clientQuicheConfig.setMaxIdleTimeout(1_000L); + clientQuicheConfig.setInitialMaxData(10_000_000L); + clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L); + clientQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L); + clientQuicheConfig.setInitialMaxStreamDataUni(10_000_000L); + clientQuicheConfig.setInitialMaxStreamsUni(100L); + clientQuicheConfig.setInitialMaxStreamsBidi(100L); + clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); + + serverCertificateChain = keyStore.getCertificateChain("mykey"); + serverQuicheConfig = new QuicheConfig(); + serverQuicheConfig.setPrivKeyPemPath(keyPair[0].toString()); + serverQuicheConfig.setCertChainPemPath(keyPair[1].toString()); + serverQuicheConfig.setApplicationProtos("http/0.9"); + serverQuicheConfig.setVerifyPeer(true); + serverQuicheConfig.setTrustedCertsPemPath(trustStorePath.toString()); + serverQuicheConfig.setMaxIdleTimeout(1_000L); + serverQuicheConfig.setInitialMaxData(10_000_000L); + serverQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L); + serverQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L); + serverQuicheConfig.setInitialMaxStreamDataUni(10_000_000L); + serverQuicheConfig.setInitialMaxStreamsUni(100L); + serverQuicheConfig.setInitialMaxStreamsBidi(100L); + serverQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); + + tokenMinter = new TestTokenMinter(); + tokenValidator = new TestTokenValidator(); + } + + @AfterEach + protected void tearDown() + { + connectionsToDisposeOf.forEach(ForeignIncubatorQuicheConnection::dispose); + connectionsToDisposeOf.clear(); + } + + @Test + public void testClientCert() throws Exception + { + // establish connection + Map.Entry entry = connectClientToServer(); + ForeignIncubatorQuicheConnection clientQuicheConnection = entry.getKey(); + ForeignIncubatorQuicheConnection serverQuicheConnection = entry.getValue(); + + // assert that the client certificate was correctly received by the server + byte[] receivedClientCertificate = serverQuicheConnection.getPeerCertificate(); + byte[] configuredClientCertificate = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(configuredClientCertificate, receivedClientCertificate), is(true)); + + // assert that the server certificate was correctly received by the client + byte[] receivedServerCertificate = clientQuicheConnection.getPeerCertificate(); + byte[] configuredServerCertificate = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(configuredServerCertificate, receivedServerCertificate), is(true)); + } + + private void drainServerToFeedClient(Map.Entry entry, int expectedSize) throws IOException + { + ForeignIncubatorQuicheConnection clientQuicheConnection = entry.getKey(); + ForeignIncubatorQuicheConnection serverQuicheConnection = entry.getValue(); + ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN); + + int drained = serverQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(expectedSize)); + buffer.flip(); + int fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress); + assertThat(fed, is(expectedSize)); + } + + private void drainClientToFeedServer(Map.Entry entry, int expectedSize) throws IOException + { + ForeignIncubatorQuicheConnection clientQuicheConnection = entry.getKey(); + ForeignIncubatorQuicheConnection serverQuicheConnection = entry.getValue(); + ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN); + + int drained = clientQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(expectedSize)); + buffer.flip(); + int fed = serverQuicheConnection.feedCipherBytes(buffer, serverSocketAddress, clientSocketAddress); + assertThat(fed, is(expectedSize)); + } + + private Map.Entry connectClientToServer() throws IOException + { + ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN); + ByteBuffer buffer2 = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN); + + ForeignIncubatorQuicheConnection clientQuicheConnection = ForeignIncubatorQuicheConnection.connect(clientQuicheConfig, clientSocketAddress, serverSocketAddress); + connectionsToDisposeOf.add(clientQuicheConnection); + + int drained = clientQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(1200)); + buffer.flip(); + + ForeignIncubatorQuicheConnection serverQuicheConnection = ForeignIncubatorQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, serverSocketAddress, clientSocketAddress); + assertThat(serverQuicheConnection, is(nullValue())); + boolean negotiated = ForeignIncubatorQuicheConnection.negotiate(tokenMinter, buffer, buffer2); + assertThat(negotiated, is(true)); + buffer2.flip(); + + int fed = clientQuicheConnection.feedCipherBytes(buffer2, clientSocketAddress, serverSocketAddress); + assertThat(fed, is(79)); + + buffer.clear(); + drained = clientQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(1200)); + buffer.flip(); + + serverQuicheConnection = ForeignIncubatorQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, serverSocketAddress, clientSocketAddress); + assertThat(serverQuicheConnection, is(not(nullValue()))); + connectionsToDisposeOf.add(serverQuicheConnection); + + buffer.clear(); + drained = serverQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(1200)); + buffer.flip(); + + fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress); + assertThat(fed, is(1200)); + + assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(false)); + + AbstractMap.SimpleImmutableEntry entry = new AbstractMap.SimpleImmutableEntry<>(clientQuicheConnection, serverQuicheConnection); + + int protosLen = 0; + for (String proto : clientQuicheConfig.getApplicationProtos()) + protosLen += 1 + proto.getBytes(StandardCharsets.UTF_8).length; + + // 1st round + drainServerToFeedClient(entry, 451 + protosLen); + assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); + + drainClientToFeedServer(entry, 1200); + assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); + + // 2nd round (needed b/c of client cert) + drainServerToFeedClient(entry, 71); + assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); + + drainClientToFeedServer(entry, 222); + assertThat(serverQuicheConnection.isConnectionEstablished(), is(true)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); + + return entry; + } + + private static class TestTokenMinter implements QuicheConnection.TokenMinter + { + @Override + public byte[] mint(byte[] dcid, int len) + { + return ByteBuffer.allocate(len).put(dcid, 0, len).array(); + } + } + + private static class TestTokenValidator implements QuicheConnection.TokenValidator + { + @Override + public byte[] validate(byte[] token, int len) + { + return ByteBuffer.allocate(len).put(token, 0, len).array(); + } + } +} diff --git a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/java/org/eclipse/jetty/quic/quiche/foreign/incubator/LowLevelQuicheTest.java b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/java/org/eclipse/jetty/quic/quiche/foreign/incubator/LowLevelQuicheTest.java index 57c16423291..ba535e48db2 100644 --- a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/java/org/eclipse/jetty/quic/quiche/foreign/incubator/LowLevelQuicheTest.java +++ b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/java/org/eclipse/jetty/quic/quiche/foreign/incubator/LowLevelQuicheTest.java @@ -13,25 +13,30 @@ package org.eclipse.jetty.quic.quiche.foreign.incubator; -import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.Certificate; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Objects; import org.eclipse.jetty.quic.quiche.QuicheConfig; import org.eclipse.jetty.quic.quiche.QuicheConnection; -import org.eclipse.jetty.quic.quiche.SSLKeyPair; +import org.eclipse.jetty.quic.quiche.PemExporter; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN; import static org.hamcrest.MatcherAssert.assertThat; @@ -39,8 +44,11 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; +@ExtendWith(WorkDirExtension.class) public class LowLevelQuicheTest { + public WorkDir workDir; + private final Collection connectionsToDisposeOf = new ArrayList<>(); private InetSocketAddress clientSocketAddress; @@ -49,6 +57,7 @@ public class LowLevelQuicheTest private QuicheConfig serverQuicheConfig; private ForeignIncubatorQuicheConnection.TokenMinter tokenMinter; private ForeignIncubatorQuicheConnection.TokenValidator tokenValidator; + private Certificate[] serverCertificateChain; @BeforeEach protected void setUp() throws Exception @@ -56,10 +65,18 @@ public class LowLevelQuicheTest clientSocketAddress = new InetSocketAddress("localhost", 9999); serverSocketAddress = new InetSocketAddress("localhost", 8888); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/keystore.p12")) + { + keyStore.load(is, "storepwd".toCharArray()); + } + Path targetFolder = workDir.getEmptyPathDir(); + clientQuicheConfig = new QuicheConfig(); clientQuicheConfig.setApplicationProtos("http/0.9"); clientQuicheConfig.setDisableActiveMigration(true); - clientQuicheConfig.setVerifyPeer(false); + clientQuicheConfig.setVerifyPeer(true); + clientQuicheConfig.setTrustedCertsPemPath(PemExporter.exportTrustStore(keyStore, targetFolder).toString()); clientQuicheConfig.setMaxIdleTimeout(1_000L); clientQuicheConfig.setInitialMaxData(10_000_000L); clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L); @@ -69,11 +86,11 @@ public class LowLevelQuicheTest clientQuicheConfig.setInitialMaxStreamsBidi(100L); clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); - SSLKeyPair serverKeyPair = new SSLKeyPair(Paths.get(Objects.requireNonNull(getClass().getResource("/keystore.p12")).toURI()).toFile(), "PKCS12", "storepwd".toCharArray(), "mykey", "storepwd".toCharArray()); - File[] pemFiles = serverKeyPair.export(new File(System.getProperty("java.io.tmpdir"))); + serverCertificateChain = keyStore.getCertificateChain("mykey"); serverQuicheConfig = new QuicheConfig(); - serverQuicheConfig.setPrivKeyPemPath(pemFiles[0].getPath()); - serverQuicheConfig.setCertChainPemPath(pemFiles[1].getPath()); + Path[] keyPair = PemExporter.exportKeyPair(keyStore, "mykey", "storepwd".toCharArray(), targetFolder); + serverQuicheConfig.setPrivKeyPemPath(keyPair[0].toString()); + serverQuicheConfig.setCertChainPemPath(keyPair[1].toString()); serverQuicheConfig.setApplicationProtos("http/0.9"); serverQuicheConfig.setVerifyPeer(false); serverQuicheConfig.setMaxIdleTimeout(1_000L); @@ -134,6 +151,14 @@ public class LowLevelQuicheTest // assert that stream 0 is finished on server assertThat(serverQuicheConnection.isStreamFinished(0), is(true)); + + // assert that there is not client certificate + assertThat(serverQuicheConnection.getPeerCertificate(), nullValue()); + + // assert that the server certificate was correctly received by the client + byte[] peerCertificate = clientQuicheConnection.getPeerCertificate(); + byte[] serverCert = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(serverCert, peerCertificate), is(true)); } @Test @@ -167,6 +192,14 @@ public class LowLevelQuicheTest // assert that stream 0 is finished on server assertThat(serverQuicheConnection.isStreamFinished(0), is(true)); + + // assert that there is not client certificate + assertThat(serverQuicheConnection.getPeerCertificate(), nullValue()); + + // assert that the server certificate was correctly received by the client + byte[] peerCertificate = clientQuicheConnection.getPeerCertificate(); + byte[] serverCert = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(serverCert, peerCertificate), is(true)); } @Test @@ -182,6 +215,14 @@ public class LowLevelQuicheTest assertThat(clientQuicheConnection.getNegotiatedProtocol(), is("€")); assertThat(serverQuicheConnection.getNegotiatedProtocol(), is("€")); + + // assert that there is not client certificate + assertThat(serverQuicheConnection.getPeerCertificate(), nullValue()); + + // assert that the server certificate was correctly received by the client + byte[] peerCertificate = clientQuicheConnection.getPeerCertificate(); + byte[] serverCert = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(serverCert, peerCertificate), is(true)); } private void drainServerToFeedClient(Map.Entry entry, int expectedSize) throws IOException @@ -257,7 +298,7 @@ public class LowLevelQuicheTest for (String proto : clientQuicheConfig.getApplicationProtos()) protosLen += 1 + proto.getBytes(StandardCharsets.UTF_8).length; - drainServerToFeedClient(entry, 300 + protosLen); + drainServerToFeedClient(entry, 420 + protosLen); assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); diff --git a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/resources/keystore.p12 b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/resources/keystore.p12 index 0b56dd34ee9..8ab40f72afd 100644 Binary files a/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/resources/keystore.p12 and b/jetty-quic/quic-quiche/quic-quiche-foreign-incubator/src/test/resources/keystore.p12 differ diff --git a/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java b/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java index 730c55414f6..ca150fe7695 100644 --- a/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java +++ b/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java @@ -22,7 +22,9 @@ import java.security.SecureRandom; import java.util.ArrayList; import java.util.List; +import org.eclipse.jetty.quic.quiche.Quiche; import org.eclipse.jetty.quic.quiche.Quiche.quiche_error; +import org.eclipse.jetty.quic.quiche.Quiche.quic_error; import org.eclipse.jetty.quic.quiche.QuicheConfig; import org.eclipse.jetty.quic.quiche.QuicheConnection; import org.eclipse.jetty.util.BufferUtil; @@ -114,7 +116,7 @@ public class JnaQuicheConnection extends QuicheConnection SizedStructure localSockaddr = sockaddr.convert(local); SizedStructure peerSockaddr = sockaddr.convert(peer); - LibQuiche.quiche_conn quicheConn = LibQuiche.INSTANCE.quiche_connect(peer.getHostName(), scid, new size_t(scid.length), localSockaddr.getStructure(), localSockaddr.getSize(), peerSockaddr.getStructure(), peerSockaddr.getSize(), libQuicheConfig); + LibQuiche.quiche_conn quicheConn = LibQuiche.INSTANCE.quiche_connect(peer.getHostString(), scid, new size_t(scid.length), localSockaddr.getStructure(), localSockaddr.getSize(), peerSockaddr.getStructure(), peerSockaddr.getSize(), libQuicheConfig); return new JnaQuicheConnection(quicheConn, libQuicheConfig); } @@ -128,13 +130,29 @@ public class JnaQuicheConnection extends QuicheConnection if (verifyPeer != null) LibQuiche.INSTANCE.quiche_config_verify_peer(quicheConfig, verifyPeer); + String trustedCertsPemPath = config.getTrustedCertsPemPath(); + if (trustedCertsPemPath != null) + { + int rc = LibQuiche.INSTANCE.quiche_config_load_verify_locations_from_file(quicheConfig, trustedCertsPemPath); + if (rc != 0) + throw new IOException("Error loading trusted certificates file " + trustedCertsPemPath + " : " + Quiche.quiche_error.errToString(rc)); + } + String certChainPemPath = config.getCertChainPemPath(); if (certChainPemPath != null) - LibQuiche.INSTANCE.quiche_config_load_cert_chain_from_pem_file(quicheConfig, certChainPemPath); + { + int rc = LibQuiche.INSTANCE.quiche_config_load_cert_chain_from_pem_file(quicheConfig, certChainPemPath); + if (rc < 0) + throw new IOException("Error loading certificate chain file " + certChainPemPath + " : " + Quiche.quiche_error.errToString(rc)); + } String privKeyPemPath = config.getPrivKeyPemPath(); if (privKeyPemPath != null) - LibQuiche.INSTANCE.quiche_config_load_priv_key_from_pem_file(quicheConfig, privKeyPemPath); + { + int rc = LibQuiche.INSTANCE.quiche_config_load_priv_key_from_pem_file(quicheConfig, privKeyPemPath); + if (rc < 0) + throw new IOException("Error loading private key file " + privKeyPemPath + " : " + Quiche.quiche_error.errToString(rc)); + } String[] applicationProtos = config.getApplicationProtos(); if (applicationProtos != null) @@ -385,8 +403,29 @@ public class JnaQuicheConnection extends QuicheConnection public void enableQlog(String filename, String title, String desc) throws IOException { - if (!LibQuiche.INSTANCE.quiche_conn_set_qlog_path(quicheConn, filename, title, desc)) - throw new IOException("unable to set qlog path to " + filename); + try (AutoLock ignore = lock.lock()) + { + if (quicheConn == null) + throw new IllegalStateException("connection was released"); + + if (!LibQuiche.INSTANCE.quiche_conn_set_qlog_path(quicheConn, filename, title, desc)) + throw new IOException("unable to set qlog path to " + filename); + } + } + + public byte[] getPeerCertificate() + { + try (AutoLock ignore = lock.lock()) + { + if (quicheConn == null) + throw new IllegalStateException("connection was released"); + + char_pointer out = new char_pointer(); + size_t_pointer out_len = new size_t_pointer(); + LibQuiche.INSTANCE.quiche_conn_peer_cert(quicheConn, out, out_len); + int len = out_len.getPointee().intValue(); + return out.getValueAsBytes(len); + } } @Override @@ -429,9 +468,12 @@ public class JnaQuicheConnection extends QuicheConnection SizedStructure peerSockaddr = sockaddr.convert(peer); info.from = peerSockaddr.getStructure().byReference(); info.from_len = peerSockaddr.getSize(); + // If quiche_conn_recv() fails, quiche_conn_local_error() can be called to get the standard error. int received = LibQuiche.INSTANCE.quiche_conn_recv(quicheConn, buffer, new size_t(buffer.remaining()), info).intValue(); if (received < 0) - throw new IOException("failed to receive packet; err=" + quiche_error.errToString(received)); + throw new IOException("failed to receive packet;" + + " quiche_err=" + quiche_error.errToString(received) + + " quic_err=" + quic_error.errToString(getLocalCloseInfo().error())); buffer.position(buffer.position() + received); return received; } @@ -460,7 +502,7 @@ public class JnaQuicheConnection extends QuicheConnection if (written == quiche_error.QUICHE_ERR_DONE) return 0; if (written < 0L) - throw new IOException("failed to send packet; err=" + quiche_error.errToString(written)); + throw new IOException("failed to send packet; quiche_err=" + quiche_error.errToString(written)); int prevPosition = buffer.position(); buffer.position(prevPosition + written); return written; @@ -620,7 +662,7 @@ public class JnaQuicheConnection extends QuicheConnection if (value < 0) { if (LOG.isDebugEnabled()) - LOG.debug("could not read window capacity for stream {} err={}", streamId, quiche_error.errToString(value)); + LOG.debug("could not read window capacity for stream {} quiche_err={}", streamId, quiche_error.errToString(value)); } return value; } @@ -652,7 +694,7 @@ public class JnaQuicheConnection extends QuicheConnection if (written == quiche_error.QUICHE_ERR_DONE) return 0; if (written < 0L) - throw new IOException("failed to write to stream " + streamId + "; err=" + quiche_error.errToString(written)); + throw new IOException("failed to write to stream " + streamId + "; quiche_err=" + quiche_error.errToString(written)); buffer.position(buffer.position() + written); return written; } @@ -670,7 +712,7 @@ public class JnaQuicheConnection extends QuicheConnection if (read == quiche_error.QUICHE_ERR_DONE) return isStreamFinished(streamId) ? -1 : 0; if (read < 0L) - throw new IOException("failed to read from stream " + streamId + "; err=" + quiche_error.errToString(read)); + throw new IOException("failed to read from stream " + streamId + "; quiche_err=" + quiche_error.errToString(read)); buffer.position(buffer.position() + read); return read; } @@ -703,4 +745,21 @@ public class JnaQuicheConnection extends QuicheConnection return null; } } + + @Override + public CloseInfo getLocalCloseInfo() + { + try (AutoLock ignore = lock.lock()) + { + if (quicheConn == null) + throw new IllegalStateException("connection was released"); + bool_pointer app = new bool_pointer(); + uint64_t_pointer error = new uint64_t_pointer(); + char_pointer reason = new char_pointer(); + size_t_pointer reasonLength = new size_t_pointer(); + if (LibQuiche.INSTANCE.quiche_conn_local_error(quicheConn, app, error, reason, reasonLength)) + return new CloseInfo(error.getValue(), reason.getValueAsString((int)reasonLength.getValue(), LibQuiche.CHARSET)); + return null; + } + } } diff --git a/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/LibQuiche.java b/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/LibQuiche.java index 813d0d2f8ac..1cb5e5530b4 100644 --- a/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/LibQuiche.java +++ b/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/LibQuiche.java @@ -86,6 +86,9 @@ public interface LibQuiche extends Library // Configures whether to verify the peer's certificate. void quiche_config_verify_peer(quiche_config config, boolean v); + // Specifies a file where trusted CA certificates are stored for the purposes of certificate verification. + int quiche_config_load_verify_locations_from_file(quiche_config config, String path); + // Configures the list of supported application protocols. int quiche_config_set_application_protos(quiche_config config, byte[] protos, size_t protos_len); @@ -425,6 +428,9 @@ public interface LibQuiche extends Library // Returns true if the connection was closed due to the idle timeout. boolean quiche_conn_is_timed_out(quiche_conn conn); + // Returns the peer's leaf certificate (if any) as a DER-encoded buffer. + void quiche_conn_peer_cert(quiche_conn conn, char_pointer out, size_t_pointer out_len); + // Returns true if a connection error was received, and updates the provided // parameters accordingly. boolean quiche_conn_peer_error(quiche_conn conn, diff --git a/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/char_pointer.java b/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/char_pointer.java index 19f310c5cdc..97871528104 100644 --- a/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/char_pointer.java +++ b/jetty-quic/quic-quiche/quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/char_pointer.java @@ -15,12 +15,24 @@ package org.eclipse.jetty.quic.quiche.jna; import java.nio.charset.Charset; +import com.sun.jna.Pointer; import com.sun.jna.ptr.PointerByReference; public class char_pointer extends PointerByReference { public String getValueAsString(int len, Charset charset) { - return new String(getValue().getByteArray(0, len), charset); + Pointer value = getValue(); + if (value == null) + return null; + return new String(value.getByteArray(0, len), charset); + } + + public byte[] getValueAsBytes(int len) + { + Pointer value = getValue(); + if (value == null) + return null; + return value.getByteArray(0, len); } } diff --git a/jetty-quic/quic-quiche/quic-quiche-jna/src/test/java/org/eclipse/jetty/quic/quiche/jna/LowLevelQuicheClientCertTest.java b/jetty-quic/quic-quiche/quic-quiche-jna/src/test/java/org/eclipse/jetty/quic/quiche/jna/LowLevelQuicheClientCertTest.java new file mode 100644 index 00000000000..d42e784a093 --- /dev/null +++ b/jetty-quic/quic-quiche/quic-quiche-jna/src/test/java/org/eclipse/jetty/quic/quiche/jna/LowLevelQuicheClientCertTest.java @@ -0,0 +1,248 @@ +// +// ======================================================================== +// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0 +// which is available at https://www.apache.org/licenses/LICENSE-2.0. +// +// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0 +// ======================================================================== +// + +package org.eclipse.jetty.quic.quiche.jna; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import org.eclipse.jetty.quic.quiche.PemExporter; +import org.eclipse.jetty.quic.quiche.QuicheConfig; +import org.eclipse.jetty.quic.quiche.QuicheConnection; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; +import static org.hamcrest.core.Is.is; + +@ExtendWith(WorkDirExtension.class) +public class LowLevelQuicheClientCertTest +{ + public WorkDir workDir; + + private final Collection connectionsToDisposeOf = new ArrayList<>(); + + private InetSocketAddress clientSocketAddress; + private InetSocketAddress serverSocketAddress; + private QuicheConfig clientQuicheConfig; + private QuicheConfig serverQuicheConfig; + private JnaQuicheConnection.TokenMinter tokenMinter; + private JnaQuicheConnection.TokenValidator tokenValidator; + private Certificate[] serverCertificateChain; + + @BeforeEach + protected void setUp() throws Exception + { + clientSocketAddress = new InetSocketAddress("localhost", 9999); + serverSocketAddress = new InetSocketAddress("localhost", 8888); + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/keystore.p12")) + { + keyStore.load(is, "storepwd".toCharArray()); + } + Path targetFolder = workDir.getEmptyPathDir(); + Path[] keyPair = PemExporter.exportKeyPair(keyStore, "mykey", "storepwd".toCharArray(), targetFolder); + Path trustStorePath = PemExporter.exportTrustStore(keyStore, targetFolder); + + clientQuicheConfig = new QuicheConfig(); + clientQuicheConfig.setApplicationProtos("http/0.9"); + clientQuicheConfig.setDisableActiveMigration(true); + clientQuicheConfig.setPrivKeyPemPath(keyPair[0].toString()); + clientQuicheConfig.setCertChainPemPath(keyPair[1].toString()); + clientQuicheConfig.setVerifyPeer(true); + clientQuicheConfig.setTrustedCertsPemPath(trustStorePath.toString()); + clientQuicheConfig.setMaxIdleTimeout(1_000L); + clientQuicheConfig.setInitialMaxData(10_000_000L); + clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L); + clientQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L); + clientQuicheConfig.setInitialMaxStreamDataUni(10_000_000L); + clientQuicheConfig.setInitialMaxStreamsUni(100L); + clientQuicheConfig.setInitialMaxStreamsBidi(100L); + clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); + + serverCertificateChain = keyStore.getCertificateChain("mykey"); + serverQuicheConfig = new QuicheConfig(); + serverQuicheConfig.setPrivKeyPemPath(keyPair[0].toString()); + serverQuicheConfig.setCertChainPemPath(keyPair[1].toString()); + serverQuicheConfig.setApplicationProtos("http/0.9"); + serverQuicheConfig.setVerifyPeer(true); + serverQuicheConfig.setTrustedCertsPemPath(trustStorePath.toString()); + serverQuicheConfig.setMaxIdleTimeout(1_000L); + serverQuicheConfig.setInitialMaxData(10_000_000L); + serverQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L); + serverQuicheConfig.setInitialMaxStreamDataBidiRemote(10_000_000L); + serverQuicheConfig.setInitialMaxStreamDataUni(10_000_000L); + serverQuicheConfig.setInitialMaxStreamsUni(100L); + serverQuicheConfig.setInitialMaxStreamsBidi(100L); + serverQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); + + tokenMinter = new TestTokenMinter(); + tokenValidator = new TestTokenValidator(); + } + + @AfterEach + protected void tearDown() + { + connectionsToDisposeOf.forEach(JnaQuicheConnection::dispose); + connectionsToDisposeOf.clear(); + } + + @Test + public void testClientCert() throws Exception + { + // establish connection + Map.Entry entry = connectClientToServer(); + JnaQuicheConnection clientQuicheConnection = entry.getKey(); + JnaQuicheConnection serverQuicheConnection = entry.getValue(); + + // assert that the client certificate was correctly received by the server + byte[] receivedClientCertificate = serverQuicheConnection.getPeerCertificate(); + byte[] configuredClientCertificate = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(configuredClientCertificate, receivedClientCertificate), is(true)); + + // assert that the server certificate was correctly received by the client + byte[] receivedServerCertificate = clientQuicheConnection.getPeerCertificate(); + byte[] configuredServerCertificate = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(configuredServerCertificate, receivedServerCertificate), is(true)); + } + + private void drainServerToFeedClient(Map.Entry entry, int expectedSize) throws IOException + { + JnaQuicheConnection clientQuicheConnection = entry.getKey(); + JnaQuicheConnection serverQuicheConnection = entry.getValue(); + ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN); + + int drained = serverQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(expectedSize)); + buffer.flip(); + int fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress); + assertThat(fed, is(expectedSize)); + } + + private void drainClientToFeedServer(Map.Entry entry, int expectedSize) throws IOException + { + JnaQuicheConnection clientQuicheConnection = entry.getKey(); + JnaQuicheConnection serverQuicheConnection = entry.getValue(); + ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN); + + int drained = clientQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(expectedSize)); + buffer.flip(); + int fed = serverQuicheConnection.feedCipherBytes(buffer, serverSocketAddress, clientSocketAddress); + assertThat(fed, is(expectedSize)); + } + + private Map.Entry connectClientToServer() throws IOException + { + ByteBuffer buffer = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN); + ByteBuffer buffer2 = ByteBuffer.allocate(QUICHE_MIN_CLIENT_INITIAL_LEN); + + JnaQuicheConnection clientQuicheConnection = JnaQuicheConnection.connect(clientQuicheConfig, clientSocketAddress, serverSocketAddress); + connectionsToDisposeOf.add(clientQuicheConnection); + + int drained = clientQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(1200)); + buffer.flip(); + + JnaQuicheConnection serverQuicheConnection = JnaQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, serverSocketAddress, clientSocketAddress); + assertThat(serverQuicheConnection, is(nullValue())); + boolean negotiated = JnaQuicheConnection.negotiate(tokenMinter, buffer, buffer2); + assertThat(negotiated, is(true)); + buffer2.flip(); + + int fed = clientQuicheConnection.feedCipherBytes(buffer2, clientSocketAddress, serverSocketAddress); + assertThat(fed, is(79)); + + buffer.clear(); + drained = clientQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(1200)); + buffer.flip(); + + serverQuicheConnection = JnaQuicheConnection.tryAccept(serverQuicheConfig, tokenValidator, buffer, serverSocketAddress, clientSocketAddress); + assertThat(serverQuicheConnection, is(not(nullValue()))); + connectionsToDisposeOf.add(serverQuicheConnection); + + buffer.clear(); + drained = serverQuicheConnection.drainCipherBytes(buffer); + assertThat(drained, is(1200)); + buffer.flip(); + + fed = clientQuicheConnection.feedCipherBytes(buffer, clientSocketAddress, serverSocketAddress); + assertThat(fed, is(1200)); + + assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(false)); + + AbstractMap.SimpleImmutableEntry entry = new AbstractMap.SimpleImmutableEntry<>(clientQuicheConnection, serverQuicheConnection); + + int protosLen = 0; + for (String proto : clientQuicheConfig.getApplicationProtos()) + protosLen += 1 + proto.getBytes(LibQuiche.CHARSET).length; + + // 1st round + drainServerToFeedClient(entry, 451 + protosLen); + assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); + + drainClientToFeedServer(entry, 1200); + assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); + + // 2nd round (needed b/c of client cert) + drainServerToFeedClient(entry, 72); + assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); + + drainClientToFeedServer(entry, 222); + assertThat(serverQuicheConnection.isConnectionEstablished(), is(true)); + assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); + + return entry; + } + + private static class TestTokenMinter implements QuicheConnection.TokenMinter + { + @Override + public byte[] mint(byte[] dcid, int len) + { + return ByteBuffer.allocate(len).put(dcid, 0, len).array(); + } + } + + private static class TestTokenValidator implements QuicheConnection.TokenValidator + { + @Override + public byte[] validate(byte[] token, int len) + { + return ByteBuffer.allocate(len).put(token, 0, len).array(); + } + } +} diff --git a/jetty-quic/quic-quiche/quic-quiche-jna/src/test/java/org/eclipse/jetty/quic/quiche/jna/LowLevelQuicheTest.java b/jetty-quic/quic-quiche/quic-quiche-jna/src/test/java/org/eclipse/jetty/quic/quiche/jna/LowLevelQuicheTest.java index 25bbd4fdb9b..f5b2ffc0b0e 100644 --- a/jetty-quic/quic-quiche/quic-quiche-jna/src/test/java/org/eclipse/jetty/quic/quiche/jna/LowLevelQuicheTest.java +++ b/jetty-quic/quic-quiche/quic-quiche-jna/src/test/java/org/eclipse/jetty/quic/quiche/jna/LowLevelQuicheTest.java @@ -13,23 +13,29 @@ package org.eclipse.jetty.quic.quiche.jna; -import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.InetSocketAddress; import java.nio.ByteBuffer; -import java.nio.file.Paths; +import java.nio.file.Path; +import java.security.KeyStore; +import java.security.cert.Certificate; import java.util.AbstractMap; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; -import java.util.Objects; + import org.eclipse.jetty.quic.quiche.QuicheConfig; import org.eclipse.jetty.quic.quiche.QuicheConnection; -import org.eclipse.jetty.quic.quiche.SSLKeyPair; +import org.eclipse.jetty.quic.quiche.PemExporter; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDir; +import org.eclipse.jetty.toolchain.test.jupiter.WorkDirExtension; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import static org.eclipse.jetty.quic.quiche.Quiche.QUICHE_MIN_CLIENT_INITIAL_LEN; import static org.hamcrest.MatcherAssert.assertThat; @@ -37,8 +43,11 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.core.Is.is; +@ExtendWith(WorkDirExtension.class) public class LowLevelQuicheTest { + public WorkDir workDir; + private final Collection connectionsToDisposeOf = new ArrayList<>(); private InetSocketAddress clientSocketAddress; @@ -47,6 +56,7 @@ public class LowLevelQuicheTest private QuicheConfig serverQuicheConfig; private JnaQuicheConnection.TokenMinter tokenMinter; private JnaQuicheConnection.TokenValidator tokenValidator; + private Certificate[] serverCertificateChain; @BeforeEach protected void setUp() throws Exception @@ -54,10 +64,18 @@ public class LowLevelQuicheTest clientSocketAddress = new InetSocketAddress("localhost", 9999); serverSocketAddress = new InetSocketAddress("localhost", 8888); + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + try (InputStream is = getClass().getResourceAsStream("/keystore.p12")) + { + keyStore.load(is, "storepwd".toCharArray()); + } + Path targetFolder = workDir.getEmptyPathDir(); + clientQuicheConfig = new QuicheConfig(); clientQuicheConfig.setApplicationProtos("http/0.9"); clientQuicheConfig.setDisableActiveMigration(true); - clientQuicheConfig.setVerifyPeer(false); + clientQuicheConfig.setVerifyPeer(true); + clientQuicheConfig.setTrustedCertsPemPath(PemExporter.exportTrustStore(keyStore, targetFolder).toString()); clientQuicheConfig.setMaxIdleTimeout(1_000L); clientQuicheConfig.setInitialMaxData(10_000_000L); clientQuicheConfig.setInitialMaxStreamDataBidiLocal(10_000_000L); @@ -67,11 +85,11 @@ public class LowLevelQuicheTest clientQuicheConfig.setInitialMaxStreamsBidi(100L); clientQuicheConfig.setCongestionControl(QuicheConfig.CongestionControl.CUBIC); - SSLKeyPair serverKeyPair = new SSLKeyPair(Paths.get(Objects.requireNonNull(getClass().getResource("/keystore.p12")).toURI()).toFile(), "PKCS12", "storepwd".toCharArray(), "mykey", "storepwd".toCharArray()); - File[] pemFiles = serverKeyPair.export(new File(System.getProperty("java.io.tmpdir"))); + serverCertificateChain = keyStore.getCertificateChain("mykey"); serverQuicheConfig = new QuicheConfig(); - serverQuicheConfig.setPrivKeyPemPath(pemFiles[0].getPath()); - serverQuicheConfig.setCertChainPemPath(pemFiles[1].getPath()); + Path[] keyPair = PemExporter.exportKeyPair(keyStore, "mykey", "storepwd".toCharArray(), targetFolder); + serverQuicheConfig.setPrivKeyPemPath(keyPair[0].toString()); + serverQuicheConfig.setCertChainPemPath(keyPair[1].toString()); serverQuicheConfig.setApplicationProtos("http/0.9"); serverQuicheConfig.setVerifyPeer(false); serverQuicheConfig.setMaxIdleTimeout(1_000L); @@ -132,6 +150,14 @@ public class LowLevelQuicheTest // assert that stream 0 is finished on server assertThat(serverQuicheConnection.isStreamFinished(0), is(true)); + + // assert that there is not client certificate + assertThat(serverQuicheConnection.getPeerCertificate(), nullValue()); + + // assert that the server certificate was correctly received by the client + byte[] peerCertificate = clientQuicheConnection.getPeerCertificate(); + byte[] serverCert = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(serverCert, peerCertificate), is(true)); } @Test @@ -165,6 +191,14 @@ public class LowLevelQuicheTest // assert that stream 0 is finished on server assertThat(serverQuicheConnection.isStreamFinished(0), is(true)); + + // assert that there is not client certificate + assertThat(serverQuicheConnection.getPeerCertificate(), nullValue()); + + // assert that the server certificate was correctly received by the client + byte[] peerCertificate = clientQuicheConnection.getPeerCertificate(); + byte[] serverCert = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(serverCert, peerCertificate), is(true)); } @Test @@ -180,6 +214,14 @@ public class LowLevelQuicheTest assertThat(clientQuicheConnection.getNegotiatedProtocol(), is("€")); assertThat(serverQuicheConnection.getNegotiatedProtocol(), is("€")); + + // assert that there is not client certificate + assertThat(serverQuicheConnection.getPeerCertificate(), nullValue()); + + // assert that the server certificate was correctly received by the client + byte[] peerCertificate = clientQuicheConnection.getPeerCertificate(); + byte[] serverCert = serverCertificateChain[0].getEncoded(); + assertThat(Arrays.equals(serverCert, peerCertificate), is(true)); } private void drainServerToFeedClient(Map.Entry entry, int expectedSize) throws IOException @@ -255,7 +297,7 @@ public class LowLevelQuicheTest for (String proto : clientQuicheConfig.getApplicationProtos()) protosLen += 1 + proto.getBytes(LibQuiche.CHARSET).length; - drainServerToFeedClient(entry, 300 + protosLen); + drainServerToFeedClient(entry, 420 + protosLen); assertThat(serverQuicheConnection.isConnectionEstablished(), is(false)); assertThat(clientQuicheConnection.isConnectionEstablished(), is(true)); diff --git a/jetty-quic/quic-quiche/quic-quiche-jna/src/test/resources/keystore.p12 b/jetty-quic/quic-quiche/quic-quiche-jna/src/test/resources/keystore.p12 index 0b56dd34ee9..8ab40f72afd 100644 Binary files a/jetty-quic/quic-quiche/quic-quiche-jna/src/test/resources/keystore.p12 and b/jetty-quic/quic-quiche/quic-quiche-jna/src/test/resources/keystore.p12 differ diff --git a/jetty-quic/quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnector.java b/jetty-quic/quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnector.java index 55788eac966..a23c680a4e9 100644 --- a/jetty-quic/quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnector.java +++ b/jetty-quic/quic-server/src/main/java/org/eclipse/jetty/quic/server/QuicServerConnector.java @@ -13,13 +13,14 @@ package org.eclipse.jetty.quic.server; -import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectableChannel; import java.nio.channels.SelectionKey; import java.nio.file.Files; +import java.nio.file.Path; +import java.security.KeyStore; import java.util.EventListener; import java.util.List; import java.util.Set; @@ -36,8 +37,8 @@ import org.eclipse.jetty.quic.common.QuicConfiguration; import org.eclipse.jetty.quic.common.QuicSession; import org.eclipse.jetty.quic.common.QuicSessionContainer; import org.eclipse.jetty.quic.common.QuicStreamEndPoint; +import org.eclipse.jetty.quic.quiche.PemExporter; import org.eclipse.jetty.quic.quiche.QuicheConfig; -import org.eclipse.jetty.quic.quiche.SSLKeyPair; import org.eclipse.jetty.server.AbstractNetworkConnector; import org.eclipse.jetty.server.ConnectionFactory; import org.eclipse.jetty.server.Server; @@ -60,8 +61,9 @@ public class QuicServerConnector extends AbstractNetworkConnector private final QuicSessionContainer container = new QuicSessionContainer(); private final ServerDatagramSelectorManager selectorManager; private final SslContextFactory.Server sslContextFactory; - private File privateKeyFile; - private File certificateChainFile; + private Path privateKeyPemPath; + private Path certificateChainPemPath; + private Path trustedCertificatesPemPath; private volatile DatagramChannel datagramChannel; private volatile int localPort = -1; private int inputBufferSize = 2048; @@ -89,7 +91,6 @@ public class QuicServerConnector extends AbstractNetworkConnector // One bidirectional stream to simulate the TCP stream, and no unidirectional streams. quicConfiguration.setMaxBidirectionalRemoteStreams(1); quicConfiguration.setMaxUnidirectionalRemoteStreams(0); - quicConfiguration.setVerifyPeerCertificates(false); } public QuicConfiguration getQuicConfiguration() @@ -163,19 +164,32 @@ public class QuicServerConnector extends AbstractNetworkConnector throw new IllegalStateException("Invalid KeyStore: no aliases"); String alias = sslContextFactory.getCertAlias(); if (alias == null) - alias = aliases.stream().findFirst().orElse("mykey"); - char[] keyStorePassword = sslContextFactory.getKeyStorePassword().toCharArray(); + alias = aliases.stream().findFirst().orElseThrow(); String keyManagerPassword = sslContextFactory.getKeyManagerPassword(); - SSLKeyPair keyPair = new SSLKeyPair( - sslContextFactory.getKeyStoreResource().getFile(), - sslContextFactory.getKeyStoreType(), - keyStorePassword, - alias, - keyManagerPassword == null ? keyStorePassword : keyManagerPassword.toCharArray() - ); - File[] pemFiles = keyPair.export(new File(System.getProperty("java.io.tmpdir"))); - privateKeyFile = pemFiles[0]; - certificateChainFile = pemFiles[1]; + char[] password = keyManagerPassword == null ? sslContextFactory.getKeyStorePassword().toCharArray() : keyManagerPassword.toCharArray(); + KeyStore keyStore = sslContextFactory.getKeyStore(); + Path certificateWorkPath = findPemWorkDirectory(); + Path[] keyPair = PemExporter.exportKeyPair(keyStore, alias, password, certificateWorkPath); + privateKeyPemPath = keyPair[0]; + certificateChainPemPath = keyPair[1]; + KeyStore trustStore = sslContextFactory.getTrustStore(); + if (trustStore != null) + trustedCertificatesPemPath = PemExporter.exportTrustStore(trustStore, certificateWorkPath); + } + + private Path findPemWorkDirectory() + { + Path pemWorkDirectory = getQuicConfiguration().getPemWorkDirectory(); + if (pemWorkDirectory != null) + return pemWorkDirectory; + String jettyBase = System.getProperty("jetty.base"); + if (jettyBase != null) + { + pemWorkDirectory = Path.of(jettyBase).resolve("work"); + if (Files.exists(pemWorkDirectory)) + return pemWorkDirectory; + } + throw new IllegalStateException("No PEM work directory configured"); } @Override @@ -211,9 +225,10 @@ public class QuicServerConnector extends AbstractNetworkConnector QuicheConfig newQuicheConfig() { QuicheConfig quicheConfig = new QuicheConfig(); - quicheConfig.setPrivKeyPemPath(privateKeyFile.getPath()); - quicheConfig.setCertChainPemPath(certificateChainFile.getPath()); - quicheConfig.setVerifyPeer(quicConfiguration.isVerifyPeerCertificates()); + quicheConfig.setPrivKeyPemPath(privateKeyPemPath.toString()); + quicheConfig.setCertChainPemPath(certificateChainPemPath.toString()); + quicheConfig.setTrustedCertsPemPath(trustedCertificatesPemPath == null ? null : trustedCertificatesPemPath.toString()); + quicheConfig.setVerifyPeer(sslContextFactory.getNeedClientAuth() || sslContextFactory.getWantClientAuth()); // Idle timeouts must not be managed by Quiche. quicheConfig.setMaxIdleTimeout(0L); quicheConfig.setInitialMaxData((long)quicConfiguration.getSessionRecvWindow()); @@ -240,8 +255,12 @@ public class QuicServerConnector extends AbstractNetworkConnector @Override protected void doStop() throws Exception { - deleteFile(privateKeyFile); - deleteFile(certificateChainFile); + deleteFile(privateKeyPemPath); + privateKeyPemPath = null; + deleteFile(certificateChainPemPath); + certificateChainPemPath = null; + deleteFile(trustedCertificatesPemPath); + trustedCertificatesPemPath = null; // We want the DatagramChannel to be stopped by the SelectorManager. super.doStop(); @@ -254,12 +273,12 @@ public class QuicServerConnector extends AbstractNetworkConnector selectorManager.removeEventListener(l); } - private void deleteFile(File file) + private void deleteFile(Path file) { try { if (file != null) - Files.delete(file.toPath()); + Files.delete(file); } catch (IOException x) { diff --git a/jetty-quic/quic-server/src/test/resources/keystore.p12 b/jetty-quic/quic-server/src/test/resources/keystore.p12 index 0b56dd34ee9..8ab40f72afd 100644 Binary files a/jetty-quic/quic-server/src/test/resources/keystore.p12 and b/jetty-quic/quic-server/src/test/resources/keystore.p12 differ diff --git a/jetty-server/src/test/resources/keystore.p12 b/jetty-server/src/test/resources/keystore.p12 index 01a1aea534e..d96d0667bd6 100644 Binary files a/jetty-server/src/test/resources/keystore.p12 and b/jetty-server/src/test/resources/keystore.p12 differ diff --git a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java index 128646f3491..967d332e9ce 100644 --- a/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java +++ b/tests/test-distribution/src/test/java/org/eclipse/jetty/tests/distribution/DistributionTests.java @@ -1140,7 +1140,7 @@ public class DistributionTests extends AbstractJettyHomeTest assertTrue(run2.awaitConsoleLogsFor("Started Server@", 10, TimeUnit.SECONDS)); HTTP3Client http3Client = new HTTP3Client(); - http3Client.getQuicConfiguration().setVerifyPeerCertificates(false); + http3Client.getClientConnector().setSslContextFactory(new SslContextFactory.Client(true)); this.client = new HttpClient(new HttpClientTransportOverHTTP3(http3Client)); this.client.start(); ContentResponse response = this.client.newRequest("localhost", h3Port) diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpChannelAssociationTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpChannelAssociationTest.java index 159f945ab68..bd76d1b9ea3 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpChannelAssociationTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpChannelAssociationTest.java @@ -183,7 +183,6 @@ public class HttpChannelAssociationTest extends AbstractTest HTTP3Client http3Client = new HTTP3Client(); http3Client.getClientConnector().setSelectors(1); http3Client.getClientConnector().setSslContextFactory(scenario.newClientSslContextFactory()); - http3Client.getQuicConfiguration().setVerifyPeerCertificates(false); return new HttpClientTransportOverHTTP3(http3Client) { @Override diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java index f1d3fe4e073..a9d1fd31e5c 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/HttpClientLoadTest.java @@ -16,6 +16,7 @@ package org.eclipse.jetty.http.client; import java.io.IOException; import java.io.InterruptedIOException; import java.nio.ByteBuffer; +import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -56,7 +57,6 @@ import org.eclipse.jetty.util.ProcessorUtils; import org.eclipse.jetty.util.ssl.SslContextFactory; import org.eclipse.jetty.util.thread.Scheduler; import org.hamcrest.Matchers; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; import org.slf4j.Logger; @@ -389,7 +389,9 @@ public class HttpClientLoadTest extends AbstractTest clientThreads.setName("client"); scenario.client.setExecutor(clientThreads); scenario.client.start(); - if (transport == Transport.H3) - { - Assumptions.assumeTrue(false, "certificate verification not yet supported in quic"); - // TODO: the lines below should be enough, but they don't work. To be investigated. - HttpClientTransportOverHTTP3 http3Transport = (HttpClientTransportOverHTTP3)scenario.client.getTransport(); - http3Transport.getHTTP3Client().getQuicConfiguration().setVerifyPeerCertificates(true); - } - assertThrows(ExecutionException.class, () -> + // H3 times out b/c it is QUIC's way of figuring out a connection cannot be established. + Class expectedType = transport == Transport.H3 ? TimeoutException.class : ExecutionException.class; + assertThrows(expectedType, () -> { // Use an IP address not present in the certificate. int serverPort = scenario.getServerPort().orElse(0); diff --git a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportScenario.java b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportScenario.java index 7773d08a0e0..74209a9392d 100644 --- a/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportScenario.java +++ b/tests/test-http-client-transport/src/test/java/org/eclipse/jetty/http/client/TransportScenario.java @@ -14,9 +14,11 @@ package org.eclipse.jetty.http.client; import java.io.IOException; +import java.io.InputStream; import java.lang.management.ManagementFactory; import java.nio.file.Files; import java.nio.file.Path; +import java.security.KeyStore; import java.util.ArrayList; import java.util.List; import java.util.OptionalInt; @@ -122,7 +124,9 @@ public class TransportScenario case FCGI: return new ServerConnector(server, 1, 1, provideServerConnectionFactory(transport)); case H3: - return new HTTP3ServerConnector(server, sslContextFactory, provideServerConnectionFactory(transport)); + HTTP3ServerConnector http3ServerConnector = new HTTP3ServerConnector(server, sslContextFactory, provideServerConnectionFactory(transport)); + http3ServerConnector.getQuicConfiguration().setPemWorkDirectory(Path.of(System.getProperty("java.io.tmpdir"))); + return http3ServerConnector; case UNIX_DOMAIN: UnixDomainServerConnector connector = new UnixDomainServerConnector(server, provideServerConnectionFactory(transport)); connector.setUnixDomainPath(unixDomainPath); @@ -175,7 +179,6 @@ public class TransportScenario ClientConnector clientConnector = http3Client.getClientConnector(); clientConnector.setSelectors(1); clientConnector.setSslContextFactory(sslContextFactory); - http3Client.getQuicConfiguration().setVerifyPeerCertificates(false); return new HttpClientTransportOverHTTP3(http3Client); } case FCGI: @@ -384,10 +387,23 @@ public class TransportScenario private void configureSslContextFactory(SslContextFactory sslContextFactory) { - sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12"); - sslContextFactory.setKeyStorePassword("storepwd"); - sslContextFactory.setUseCipherSuitesOrder(true); - sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); + try + { + KeyStore keystore = KeyStore.getInstance("PKCS12"); + try (InputStream is = Files.newInputStream(Path.of("src/test/resources/keystore.p12"))) + { + keystore.load(is, "storepwd".toCharArray()); + } + sslContextFactory.setTrustStore(keystore); + sslContextFactory.setKeyStore(keystore); + sslContextFactory.setKeyStorePassword("storepwd"); + sslContextFactory.setUseCipherSuitesOrder(true); + sslContextFactory.setCipherComparator(HTTP2Cipher.COMPARATOR); + } + catch (Exception e) + { + throw new RuntimeException(e); + } } public void stopClient() throws Exception diff --git a/tests/test-http-client-transport/src/test/resources/keystore.p12 b/tests/test-http-client-transport/src/test/resources/keystore.p12 index 8934437fa14..d96d0667bd6 100644 Binary files a/tests/test-http-client-transport/src/test/resources/keystore.p12 and b/tests/test-http-client-transport/src/test/resources/keystore.p12 differ