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 extends Exception> 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