Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-11.0.x
This commit is contained in:
commit
97ce1daa0b
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
Binary file not shown.
|
@ -12,6 +12,7 @@ experimental
|
|||
http2
|
||||
jna
|
||||
quiche
|
||||
work
|
||||
|
||||
[lib]
|
||||
lib/http3/*.jar
|
||||
|
|
|
@ -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");
|
||||
|
|
Binary file not shown.
|
@ -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
|
|||
/**
|
||||
* <p>Configures a {@link ClientConnector}.</p>
|
||||
*/
|
||||
public static class Configurator
|
||||
public static class Configurator extends ContainerLifeCycle
|
||||
{
|
||||
/**
|
||||
* <p>Returns whether the connection to a given {@link SocketAddress} is intrinsically secure.</p>
|
||||
|
|
|
@ -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<String, Object> 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()
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
* <p>A QUIC specific {@link ClientConnector.Configurator}.</p>
|
||||
|
@ -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<Connection> 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<String, Object> context) throws IOException
|
||||
{
|
||||
context.put(QuicConfiguration.CONTEXT_KEY, configuration);
|
||||
|
||||
DatagramChannel channel = DatagramChannel.open();
|
||||
if (clientConnector.getBindAddress() == null)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 = "" +
|
||||
"<html>\n" +
|
||||
"\t<body>\n" +
|
||||
"\t\tRequest served\n" +
|
||||
"\t</body>\n" +
|
||||
"</html>";
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -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;
|
||||
|
||||
/**
|
||||
* <p>A record that captures QUIC configuration parameters.</p>
|
||||
|
@ -24,12 +27,13 @@ public class QuicConfiguration
|
|||
|
||||
private List<String> 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<String, Object> implementationConfiguration = new HashMap<>();
|
||||
|
||||
public List<String> 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<String, Object> getImplementationConfiguration()
|
||||
{
|
||||
return implementationConfiguration;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<String> 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -150,6 +150,8 @@ public abstract class QuicheConnection
|
|||
|
||||
public abstract CloseInfo getRemoteCloseInfo();
|
||||
|
||||
public abstract CloseInfo getLocalCloseInfo();
|
||||
|
||||
public static class CloseInfo
|
||||
{
|
||||
private final long error;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<Long> 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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<ForeignIncubatorQuicheConnection> 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<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> 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<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> 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<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> 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<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> 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<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<ForeignIncubatorQuicheConnection> 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<ForeignIncubatorQuicheConnection, ForeignIncubatorQuicheConnection> 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));
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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<sockaddr> localSockaddr = sockaddr.convert(local);
|
||||
SizedStructure<sockaddr> 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,9 +403,30 @@ public class JnaQuicheConnection extends QuicheConnection
|
|||
|
||||
public void enableQlog(String filename, String title, String desc) throws IOException
|
||||
{
|
||||
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
|
||||
protected List<Long> iterableStreamIds(boolean write)
|
||||
|
@ -429,9 +468,12 @@ public class JnaQuicheConnection extends QuicheConnection
|
|||
SizedStructure<sockaddr> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<JnaQuicheConnection> 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<JnaQuicheConnection, JnaQuicheConnection> 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<JnaQuicheConnection, JnaQuicheConnection> 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<JnaQuicheConnection, JnaQuicheConnection> 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<JnaQuicheConnection, JnaQuicheConnection> 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<JnaQuicheConnection, JnaQuicheConnection> 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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<JnaQuicheConnection> 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<JnaQuicheConnection, JnaQuicheConnection> 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));
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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)
|
||||
{
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -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)
|
||||
|
|
|
@ -183,7 +183,6 @@ public class HttpChannelAssociationTest extends AbstractTest<TransportScenario>
|
|||
HTTP3Client http3Client = new HTTP3Client();
|
||||
http3Client.getClientConnector().setSelectors(1);
|
||||
http3Client.getClientConnector().setSslContextFactory(scenario.newClientSslContextFactory());
|
||||
http3Client.getQuicConfiguration().setVerifyPeerCertificates(false);
|
||||
return new HttpClientTransportOverHTTP3(http3Client)
|
||||
{
|
||||
@Override
|
||||
|
|
|
@ -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<HttpClientLoadTest.LoadTran
|
|||
case FCGI:
|
||||
return new ServerConnector(server, null, null, byteBufferPool, 1, selectors, provideServerConnectionFactory(transport));
|
||||
case H3:
|
||||
return new HTTP3ServerConnector(server, null, null, byteBufferPool, sslContextFactory, provideServerConnectionFactory(transport));
|
||||
HTTP3ServerConnector http3ServerConnector = new HTTP3ServerConnector(server, null, null, byteBufferPool, sslContextFactory, provideServerConnectionFactory(transport));
|
||||
http3ServerConnector.getQuicConfiguration().setPemWorkDirectory(Path.of(System.getProperty("java.io.tmpdir")));
|
||||
return http3ServerConnector;
|
||||
case UNIX_DOMAIN:
|
||||
UnixDomainServerConnector unixSocketConnector = new UnixDomainServerConnector(server, null, null, byteBufferPool, 1, selectors, provideServerConnectionFactory(transport));
|
||||
unixSocketConnector.setUnixDomainPath(unixDomainPath);
|
||||
|
|
|
@ -47,7 +47,6 @@ import org.eclipse.jetty.http.HttpScheme;
|
|||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.http.HttpURI;
|
||||
import org.eclipse.jetty.http2.FlowControlStrategy;
|
||||
import org.eclipse.jetty.http3.client.http.HttpClientTransportOverHTTP3;
|
||||
import org.eclipse.jetty.server.Request;
|
||||
import org.eclipse.jetty.server.SecureRequestCustomizer;
|
||||
import org.eclipse.jetty.server.handler.AbstractHandler;
|
||||
|
@ -364,15 +363,10 @@ public class HttpClientTest extends AbstractTest<TransportScenario>
|
|||
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);
|
||||
|
|
|
@ -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,11 +387,24 @@ public class TransportScenario
|
|||
|
||||
private void configureSslContextFactory(SslContextFactory sslContextFactory)
|
||||
{
|
||||
sslContextFactory.setKeyStorePath("src/test/resources/keystore.p12");
|
||||
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
|
||||
{
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue