diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java index c853fd1aec86..8f998fa8743b 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicSession.java @@ -13,10 +13,14 @@ package org.eclipse.jetty.quic.common; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.Arrays; import java.util.Collection; import java.util.EventListener; @@ -423,6 +427,31 @@ private void finishOutwardClose(Throwable failure) } } + /** + *

Returns the peer certificates chain.

+ *

Due to current Quiche C API limitations (that the Rust version does not have), + * only the last certificate in the chain is returned. + * This may change in the future when the C APIs are aligned to the Rust APIs.

+ * + * @return the peer certificates chain (currently only the last certificate in the chain) + */ + public X509Certificate[] getPeerCertificates() + { + try + { + byte[] encoded = quicheConnection.getPeerCertificate(); + if (encoded == null) + return null; + CertificateFactory factory = CertificateFactory.getInstance("X509"); + X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(encoded)); + return new X509Certificate[]{certificate}; + } + catch (CertificateException x) + { + return null; + } + } + @Override public void dump(Appendable out, String indent) throws IOException { diff --git a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java index e7e6f8da59a4..b8a2a731a63e 100644 --- a/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java +++ b/jetty-core/jetty-quic/jetty-quic-common/src/main/java/org/eclipse/jetty/quic/common/QuicStreamEndPoint.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.net.SocketAddress; import java.nio.ByteBuffer; +import java.security.cert.X509Certificate; import java.util.List; import java.util.stream.IntStream; @@ -221,6 +222,15 @@ public Object getTransport() return session; } + @Override + public SslSessionData getSslSessionData() + { + X509Certificate[] peerCertificates = getQuicSession().getPeerCertificates(); + if (peerCertificates == null) + return null; + return SslSessionData.from(null, null, null, peerCertificates); + } + public void onWritable() { if (LOG.isDebugEnabled()) diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java index b426242cbcb7..439c25707d0b 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-common/src/main/java/org/eclipse/jetty/quic/quiche/QuicheConnection.java @@ -152,6 +152,8 @@ public final int feedClearBytesForStream(long streamId, ByteBuffer buffer) throw public abstract CloseInfo getLocalCloseInfo(); + public abstract byte[] getPeerCertificate(); + public static class CloseInfo { private final long error; diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java index 9e67935e59c1..2327aa664948 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-foreign/src/main/java/org/eclipse/jetty/quic/quiche/foreign/ForeignQuicheConnection.java @@ -518,6 +518,7 @@ public static ForeignQuicheConnection tryAccept(QuicheConfig quicheConfig, Token } } + @Override public byte[] getPeerCertificate() { try (AutoLock ignore = lock.lock()) @@ -532,7 +533,7 @@ public byte[] getPeerCertificate() quiche_h.quiche_conn_peer_cert(quicheConn, outSegment, outLenSegment); long outLen = outLenSegment.get(NativeHelper.C_LONG, 0L); - if (outLen == 0L) + if (outLen <= 0L) return null; byte[] out = new byte[(int)outLen]; // dereference outSegment pointer diff --git a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java index db05de71b71e..d633de77e21d 100644 --- a/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java +++ b/jetty-core/jetty-quic/jetty-quic-quiche/jetty-quic-quiche-jna/src/main/java/org/eclipse/jetty/quic/quiche/jna/JnaQuicheConnection.java @@ -413,6 +413,7 @@ public void enableQlog(String filename, String title, String desc) throws IOExce } } + @Override public byte[] getPeerCertificate() { try (AutoLock ignore = lock.lock()) @@ -424,6 +425,8 @@ public byte[] getPeerCertificate() 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(); + if (len <= 0) + return null; return out.getValueAsBytes(len); } } diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/pom.xml b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/pom.xml index b78feb1279f3..682078b859df 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/pom.xml +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/pom.xml @@ -112,7 +112,7 @@ @{argLine} ${jetty.surefire.argLine} - --enable-native-access org.eclipse.jetty.quic.quiche.foreign + --enable-native-access=ALL-UNNAMED diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java index 806ada98f318..a04f32398b8e 100644 --- a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/AbstractTest.java @@ -102,6 +102,14 @@ public static Collection transportsWithPushSupport() return transports; } + public static Collection transportsSecure() + { + EnumSet transports = EnumSet.of(Transport.HTTPS, Transport.H2, Transport.H3); + if ("ci".equals(System.getProperty("env"))) + transports.remove(Transport.H3); + return transports; + } + @BeforeEach public void prepare() { diff --git a/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/NeedClientAuthTest.java b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/NeedClientAuthTest.java new file mode 100644 index 000000000000..1191480d51a4 --- /dev/null +++ b/jetty-ee10/jetty-ee10-tests/jetty-ee10-test-client-transports/src/test/java/org/eclipse/jetty/ee10/test/client/transport/NeedClientAuthTest.java @@ -0,0 +1,60 @@ +// +// ======================================================================== +// 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.ee10.test.client.transport; + +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.eclipse.jetty.client.ContentResponse; +import org.eclipse.jetty.ee10.servlet.ServletContextRequest; +import org.eclipse.jetty.http.HttpStatus; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class NeedClientAuthTest extends AbstractTest +{ + @ParameterizedTest + @MethodSource("transportsSecure") + public void testNeedClientAuth(Transport transport) throws Exception + { + prepareServer(transport, new HttpServlet() + { + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) + { + // Verify that the request attribute is present. + assertNotNull(request.getAttribute(ServletContextRequest.PEER_CERTIFICATES)); + } + }); + sslContextFactoryServer.setNeedClientAuth(true); + server.start(); + + startClient(transport, httpClient -> + { + // Configure the SslContextFactory to send a certificate to the server. + SslContextFactory.Client clientSSL = httpClient.getSslContextFactory(); + clientSSL.setKeyStorePath("src/test/resources/keystore.p12"); + clientSSL.setKeyStorePassword("storepwd"); + clientSSL.setCertAlias("mykey"); + }); + + ContentResponse response = client.newRequest(newURI(transport)).send(); + + assertEquals(HttpStatus.OK_200, response.getStatus()); + } +}