From 581ea71b07febd53290a5e04310e82b820652282 Mon Sep 17 00:00:00 2001 From: Daniel Kec Date: Tue, 27 Feb 2024 09:36:42 +0100 Subject: [PATCH] 4.x: TLS peer certs (#8316) * TLS peer certs * No need to cache CN header --- .../java/io/helidon/common/pki/PkiUtil.java | 4 +- common/socket/pom.xml | 5 + .../helidon/common/socket/PeerInfoImpl.java | 29 +++-- .../io/helidon/common/socket/TlsSocket.java | 45 +++++++- .../helidon/common/socket/TlsSocketTest.java | 103 ++++++++++++++++++ .../tls/TlsReloadableX509KeyManager.java | 5 +- webserver/tests/mtls/pom.xml | 5 + .../src/main/resources/logging.properties | 6 +- 8 files changed, 176 insertions(+), 26 deletions(-) create mode 100644 common/socket/src/test/java/io/helidon/common/socket/TlsSocketTest.java diff --git a/common/key-util/src/main/java/io/helidon/common/pki/PkiUtil.java b/common/key-util/src/main/java/io/helidon/common/pki/PkiUtil.java index cad9da77810..97649df856f 100644 --- a/common/key-util/src/main/java/io/helidon/common/pki/PkiUtil.java +++ b/common/key-util/src/main/java/io/helidon/common/pki/PkiUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017, 2022 Oracle and/or its affiliates. + * Copyright (c) 2017, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,7 +105,7 @@ static List loadCertificates(KeyStore keyStore) { certs.add(cert); if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { - LOGGER.log(System.Logger.Level.DEBUG, "Added certificate under alis " + LOGGER.log(System.Logger.Level.DEBUG, "Added certificate under alias " + alias + " for " + cert diff --git a/common/socket/pom.xml b/common/socket/pom.xml index f0aa72304ce..016691b3747 100644 --- a/common/socket/pom.xml +++ b/common/socket/pom.xml @@ -51,6 +51,11 @@ hamcrest-all test + + org.mockito + mockito-core + test + diff --git a/common/socket/src/main/java/io/helidon/common/socket/PeerInfoImpl.java b/common/socket/src/main/java/io/helidon/common/socket/PeerInfoImpl.java index 238f45d0604..2f832aa11c8 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/PeerInfoImpl.java +++ b/common/socket/src/main/java/io/helidon/common/socket/PeerInfoImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import java.security.Principal; import java.security.cert.Certificate; import java.util.Optional; -import java.util.function.Supplier; import io.helidon.common.LazyValue; @@ -28,14 +27,14 @@ class PeerInfoImpl implements PeerInfo { private final LazyValue socketAddress; private final LazyValue host; private final LazyValue port; - private final Supplier> principalSupplier; - private final Supplier> certificateSupplier; + private final LazyValue> principalSupplier; + private final LazyValue> certificateSupplier; private PeerInfoImpl(LazyValue socketAddress, LazyValue host, LazyValue port, - Supplier> principalSupplier, - Supplier> certificateSupplier) { + LazyValue> principalSupplier, + LazyValue> certificateSupplier) { this.socketAddress = socketAddress; this.host = host; this.port = port; @@ -47,36 +46,34 @@ static PeerInfo createLocal(PlainSocket socket) { return new PeerInfoImpl(LazyValue.create(socket::localSocketAddress), LazyValue.create(socket::localHost), LazyValue.create(socket::localPort), - Optional::empty, - Optional::empty); + LazyValue.create(Optional.empty()), + LazyValue.create(Optional.empty())); } static PeerInfoImpl createLocal(TlsSocket socket) { // remote socket - all lazy values, as they cannot change (and they require creating another object) - // tls related - there can be re-negotiation of tls, to be safe I use a supplier return new PeerInfoImpl(LazyValue.create(socket::localSocketAddress), LazyValue.create(socket::localHost), LazyValue.create(socket::localPort), - socket::tlsPrincipal, - socket::tlsCertificates); + LazyValue.create(socket::tlsPrincipal), + LazyValue.create(socket::tlsCertificates)); } static PeerInfoImpl createRemote(TlsSocket socket) { // remote socket - all lazy values, as they cannot change (and they require creating another object) - // tls related - there can be re-negotiation of tls, to be safe I use a supplier return new PeerInfoImpl(LazyValue.create(socket::remoteSocketAddress), LazyValue.create(socket::remoteHost), LazyValue.create(socket::remotePort), - socket::tlsPeerPrincipal, - socket::tlsPeerCertificates); + LazyValue.create(socket::tlsPeerPrincipal), + LazyValue.create(socket::tlsPeerCertificates)); } static PeerInfoImpl createRemote(PlainSocket socket) { return new PeerInfoImpl(LazyValue.create(socket::remoteSocketAddress), LazyValue.create(socket::remoteHost), LazyValue.create(socket::remotePort), - Optional::empty, - Optional::empty); + LazyValue.create(Optional.empty()), + LazyValue.create(Optional.empty())); } @Override diff --git a/common/socket/src/main/java/io/helidon/common/socket/TlsSocket.java b/common/socket/src/main/java/io/helidon/common/socket/TlsSocket.java index 9a423911b36..15cf37aaaf5 100644 --- a/common/socket/src/main/java/io/helidon/common/socket/TlsSocket.java +++ b/common/socket/src/main/java/io/helidon/common/socket/TlsSocket.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.security.Principal; import java.security.cert.Certificate; +import java.util.Arrays; import java.util.Optional; import javax.net.ssl.SSLPeerUnverifiedException; @@ -28,11 +29,15 @@ */ public final class TlsSocket extends PlainSocket { private final SSLSocket sslSocket; + private volatile PeerInfo localPeer; + private volatile PeerInfo remotePeer; + private volatile byte[] lastSslSessionId; private TlsSocket(SSLSocket socket, String channelId, String serverChannelId) { super(socket, channelId, serverChannelId); this.sslSocket = socket; + this.lastSslSessionId = socket.getSession().getId(); } /** @@ -63,12 +68,28 @@ public static TlsSocket client(SSLSocket delegate, @Override public PeerInfo remotePeer() { - return PeerInfoImpl.createRemote(this); + if (renegotiated()) { + remotePeer = null; + localPeer = null; + } + + if (remotePeer == null) { + this.remotePeer = PeerInfoImpl.createRemote(this); + } + return this.remotePeer; } @Override public PeerInfo localPeer() { - return PeerInfoImpl.createLocal(this); + if (renegotiated()) { + remotePeer = null; + localPeer = null; + } + + if (localPeer == null) { + this.localPeer = PeerInfoImpl.createLocal(this); + } + return this.localPeer; } @Override @@ -110,4 +131,22 @@ Optional tlsPrincipal() { Optional tlsCertificates() { return Optional.of(sslSocket.getSession().getLocalCertificates()); } + + /** + * Check if TLS renegotiation happened, + * if so ssl session id would have changed. + * + * @return true if tls was renegotiated + */ + boolean renegotiated() { + byte[] currentSessionId = sslSocket.getSession().getId(); + + // Intentionally avoiding locking and MessageDigest.isEqual + if (Arrays.equals(currentSessionId, lastSslSessionId)) { + return false; + } + + lastSslSessionId = currentSessionId; + return true; + } } diff --git a/common/socket/src/test/java/io/helidon/common/socket/TlsSocketTest.java b/common/socket/src/test/java/io/helidon/common/socket/TlsSocketTest.java new file mode 100644 index 00000000000..b70717797fd --- /dev/null +++ b/common/socket/src/test/java/io/helidon/common/socket/TlsSocketTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 Oracle and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.helidon.common.socket; + +import java.security.Principal; +import java.util.Optional; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class TlsSocketTest { + + private final SSLSocket sslSocket; + private final SSLSession sslSession1; + private final SSLSession sslSession2; + + public TlsSocketTest() throws SSLPeerUnverifiedException { + sslSocket = mock(SSLSocket.class); + sslSession1 = mock(SSLSession.class); + sslSession2 = mock(SSLSession.class); + + Principal principal1 = mock(Principal.class); + Principal principal2 = mock(Principal.class); + + when(principal1.getName()).thenReturn("Frank"); + when(principal2.getName()).thenReturn("Jack"); + + when(sslSession1.getId()).thenReturn(new byte[] {'a', 'b', 'c'}); + when(sslSession2.getId()).thenReturn(new byte[] {'d', 'e', 'f'}); + + when(sslSession1.getPeerPrincipal()).thenReturn(principal1); + when(sslSession2.getPeerPrincipal()).thenReturn(principal2); + } + + @Test + void renegotiationDetectionBySslSessionId() { + when(sslSocket.getSession()).thenReturn(sslSession1); + TlsSocket serverSocket = TlsSocket.server(sslSocket, "test1", "test2"); + + assertFalse(serverSocket.renegotiated()); + + // renegotiate + when(sslSocket.getSession()).thenReturn(sslSession2); + + assertTrue(serverSocket.renegotiated()); + + // detection is not reentrant + assertFalse(serverSocket.renegotiated()); + } + + @Test + void lazyPeerInfo() { + when(sslSocket.getSession()).thenReturn(sslSession1); + TlsSocket serverSocket = TlsSocket.server(sslSocket, "test1", "test2"); + + assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Frank"); + assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Frank"); + } + + @Test + void lazyPeerInfoRenegotiated() { + when(sslSocket.getSession()).thenReturn(sslSession1); + TlsSocket serverSocket = TlsSocket.server(sslSocket, "test1", "test2"); + + assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Frank"); + assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Frank"); + + // renegotiate + when(sslSocket.getSession()).thenReturn(sslSession2); + + assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Jack"); + assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Jack"); + } + + private void assertPrincipal(Optional act, String expectedName) { + assertTrue(act.isPresent()); + assertThat(act.get().getName(), is(expectedName)); + } +} diff --git a/common/tls/src/main/java/io/helidon/common/tls/TlsReloadableX509KeyManager.java b/common/tls/src/main/java/io/helidon/common/tls/TlsReloadableX509KeyManager.java index b4ddc016408..dadb085f7ce 100644 --- a/common/tls/src/main/java/io/helidon/common/tls/TlsReloadableX509KeyManager.java +++ b/common/tls/src/main/java/io/helidon/common/tls/TlsReloadableX509KeyManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023 Oracle and/or its affiliates. + * Copyright (c) 2023, 2024 Oracle and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,10 @@ import java.security.cert.X509Certificate; import java.util.Objects; +import javax.net.ssl.X509ExtendedKeyManager; import javax.net.ssl.X509KeyManager; -class TlsReloadableX509KeyManager implements X509KeyManager, TlsReloadableComponent { +class TlsReloadableX509KeyManager extends X509ExtendedKeyManager implements TlsReloadableComponent { private static final System.Logger LOGGER = System.getLogger(TlsReloadableX509KeyManager.class.getName()); private volatile X509KeyManager keyManager; diff --git a/webserver/tests/mtls/pom.xml b/webserver/tests/mtls/pom.xml index be33d7f76fc..36cd959afd3 100644 --- a/webserver/tests/mtls/pom.xml +++ b/webserver/tests/mtls/pom.xml @@ -48,5 +48,10 @@ hamcrest-all test + + io.helidon.logging + helidon-logging-jul + test + diff --git a/webserver/tests/mtls/src/main/resources/logging.properties b/webserver/tests/mtls/src/main/resources/logging.properties index e68d9bc9384..fa69e662be4 100644 --- a/webserver/tests/mtls/src/main/resources/logging.properties +++ b/webserver/tests/mtls/src/main/resources/logging.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2022, 2023 Oracle and/or its affiliates. +# Copyright (c) 2022, 2024 Oracle and/or its affiliates. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # -handlers=java.util.logging.ConsoleHandler -java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n +handlers=io.helidon.logging.jul.HelidonConsoleHandler +java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n # Global logging level. Can be overridden by specific loggers .level=INFO