Skip to content

Commit

Permalink
4.x: TLS peer certs (helidon-io#8316)
Browse files Browse the repository at this point in the history
* TLS peer certs

* No need to cache CN header
  • Loading branch information
danielkec authored and hrstoyanov committed Mar 12, 2024
1 parent b9131d1 commit 581ea71
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -105,7 +105,7 @@ static List<X509Certificate> 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
Expand Down
5 changes: 5 additions & 0 deletions common/socket/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -20,22 +20,21 @@
import java.security.Principal;
import java.security.cert.Certificate;
import java.util.Optional;
import java.util.function.Supplier;

import io.helidon.common.LazyValue;

class PeerInfoImpl implements PeerInfo {
private final LazyValue<SocketAddress> socketAddress;
private final LazyValue<String> host;
private final LazyValue<Integer> port;
private final Supplier<Optional<Principal>> principalSupplier;
private final Supplier<Optional<Certificate[]>> certificateSupplier;
private final LazyValue<Optional<Principal>> principalSupplier;
private final LazyValue<Optional<Certificate[]>> certificateSupplier;

private PeerInfoImpl(LazyValue<SocketAddress> socketAddress,
LazyValue<String> host,
LazyValue<Integer> port,
Supplier<Optional<Principal>> principalSupplier,
Supplier<Optional<Certificate[]>> certificateSupplier) {
LazyValue<Optional<Principal>> principalSupplier,
LazyValue<Optional<Certificate[]>> certificateSupplier) {
this.socketAddress = socketAddress;
this.host = host;
this.port = port;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -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();
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -110,4 +131,22 @@ Optional<Principal> tlsPrincipal() {
Optional<Certificate[]> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Principal> act, String expectedName) {
assertTrue(act.isPresent());
assertThat(act.get().getName(), is(expectedName));
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions webserver/tests/mtls/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,10 @@
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.helidon.logging</groupId>
<artifactId>helidon-logging-jul</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
6 changes: 3 additions & 3 deletions webserver/tests/mtls/src/main/resources/logging.properties
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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

0 comments on commit 581ea71

Please sign in to comment.