Skip to content

Commit d14513c

Browse files
authored
4.x: TLS peer certs (helidon-io#8316)
* TLS peer certs * No need to cache CN header
1 parent 06359da commit d14513c

8 files changed

Lines changed: 176 additions & 26 deletions

File tree

common/key-util/src/main/java/io/helidon/common/pki/PkiUtil.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2017, 2022 Oracle and/or its affiliates.
2+
* Copyright (c) 2017, 2024 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -105,7 +105,7 @@ static List<X509Certificate> loadCertificates(KeyStore keyStore) {
105105
certs.add(cert);
106106

107107
if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) {
108-
LOGGER.log(System.Logger.Level.DEBUG, "Added certificate under alis "
108+
LOGGER.log(System.Logger.Level.DEBUG, "Added certificate under alias "
109109
+ alias
110110
+ " for "
111111
+ cert

common/socket/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@
5151
<artifactId>hamcrest-all</artifactId>
5252
<scope>test</scope>
5353
</dependency>
54+
<dependency>
55+
<groupId>org.mockito</groupId>
56+
<artifactId>mockito-core</artifactId>
57+
<scope>test</scope>
58+
</dependency>
5459
</dependencies>
5560

5661
<build>

common/socket/src/main/java/io/helidon/common/socket/PeerInfoImpl.java

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 Oracle and/or its affiliates.
2+
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,22 +20,21 @@
2020
import java.security.Principal;
2121
import java.security.cert.Certificate;
2222
import java.util.Optional;
23-
import java.util.function.Supplier;
2423

2524
import io.helidon.common.LazyValue;
2625

2726
class PeerInfoImpl implements PeerInfo {
2827
private final LazyValue<SocketAddress> socketAddress;
2928
private final LazyValue<String> host;
3029
private final LazyValue<Integer> port;
31-
private final Supplier<Optional<Principal>> principalSupplier;
32-
private final Supplier<Optional<Certificate[]>> certificateSupplier;
30+
private final LazyValue<Optional<Principal>> principalSupplier;
31+
private final LazyValue<Optional<Certificate[]>> certificateSupplier;
3332

3433
private PeerInfoImpl(LazyValue<SocketAddress> socketAddress,
3534
LazyValue<String> host,
3635
LazyValue<Integer> port,
37-
Supplier<Optional<Principal>> principalSupplier,
38-
Supplier<Optional<Certificate[]>> certificateSupplier) {
36+
LazyValue<Optional<Principal>> principalSupplier,
37+
LazyValue<Optional<Certificate[]>> certificateSupplier) {
3938
this.socketAddress = socketAddress;
4039
this.host = host;
4140
this.port = port;
@@ -47,36 +46,34 @@ static PeerInfo createLocal(PlainSocket socket) {
4746
return new PeerInfoImpl(LazyValue.create(socket::localSocketAddress),
4847
LazyValue.create(socket::localHost),
4948
LazyValue.create(socket::localPort),
50-
Optional::empty,
51-
Optional::empty);
49+
LazyValue.create(Optional.empty()),
50+
LazyValue.create(Optional.empty()));
5251
}
5352

5453
static PeerInfoImpl createLocal(TlsSocket socket) {
5554
// remote socket - all lazy values, as they cannot change (and they require creating another object)
56-
// tls related - there can be re-negotiation of tls, to be safe I use a supplier
5755
return new PeerInfoImpl(LazyValue.create(socket::localSocketAddress),
5856
LazyValue.create(socket::localHost),
5957
LazyValue.create(socket::localPort),
60-
socket::tlsPrincipal,
61-
socket::tlsCertificates);
58+
LazyValue.create(socket::tlsPrincipal),
59+
LazyValue.create(socket::tlsCertificates));
6260
}
6361

6462
static PeerInfoImpl createRemote(TlsSocket socket) {
6563
// remote socket - all lazy values, as they cannot change (and they require creating another object)
66-
// tls related - there can be re-negotiation of tls, to be safe I use a supplier
6764
return new PeerInfoImpl(LazyValue.create(socket::remoteSocketAddress),
6865
LazyValue.create(socket::remoteHost),
6966
LazyValue.create(socket::remotePort),
70-
socket::tlsPeerPrincipal,
71-
socket::tlsPeerCertificates);
67+
LazyValue.create(socket::tlsPeerPrincipal),
68+
LazyValue.create(socket::tlsPeerCertificates));
7269
}
7370

7471
static PeerInfoImpl createRemote(PlainSocket socket) {
7572
return new PeerInfoImpl(LazyValue.create(socket::remoteSocketAddress),
7673
LazyValue.create(socket::remoteHost),
7774
LazyValue.create(socket::remotePort),
78-
Optional::empty,
79-
Optional::empty);
75+
LazyValue.create(Optional.empty()),
76+
LazyValue.create(Optional.empty()));
8077
}
8178

8279
@Override

common/socket/src/main/java/io/helidon/common/socket/TlsSocket.java

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2022 Oracle and/or its affiliates.
2+
* Copyright (c) 2022, 2024 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.security.Principal;
2020
import java.security.cert.Certificate;
21+
import java.util.Arrays;
2122
import java.util.Optional;
2223

2324
import javax.net.ssl.SSLPeerUnverifiedException;
@@ -28,11 +29,15 @@
2829
*/
2930
public final class TlsSocket extends PlainSocket {
3031
private final SSLSocket sslSocket;
32+
private volatile PeerInfo localPeer;
33+
private volatile PeerInfo remotePeer;
34+
private volatile byte[] lastSslSessionId;
3135

3236
private TlsSocket(SSLSocket socket, String channelId, String serverChannelId) {
3337
super(socket, channelId, serverChannelId);
3438

3539
this.sslSocket = socket;
40+
this.lastSslSessionId = socket.getSession().getId();
3641
}
3742

3843
/**
@@ -63,12 +68,28 @@ public static TlsSocket client(SSLSocket delegate,
6368

6469
@Override
6570
public PeerInfo remotePeer() {
66-
return PeerInfoImpl.createRemote(this);
71+
if (renegotiated()) {
72+
remotePeer = null;
73+
localPeer = null;
74+
}
75+
76+
if (remotePeer == null) {
77+
this.remotePeer = PeerInfoImpl.createRemote(this);
78+
}
79+
return this.remotePeer;
6780
}
6881

6982
@Override
7083
public PeerInfo localPeer() {
71-
return PeerInfoImpl.createLocal(this);
84+
if (renegotiated()) {
85+
remotePeer = null;
86+
localPeer = null;
87+
}
88+
89+
if (localPeer == null) {
90+
this.localPeer = PeerInfoImpl.createLocal(this);
91+
}
92+
return this.localPeer;
7293
}
7394

7495
@Override
@@ -110,4 +131,22 @@ Optional<Principal> tlsPrincipal() {
110131
Optional<Certificate[]> tlsCertificates() {
111132
return Optional.of(sslSocket.getSession().getLocalCertificates());
112133
}
134+
135+
/**
136+
* Check if TLS renegotiation happened,
137+
* if so ssl session id would have changed.
138+
*
139+
* @return true if tls was renegotiated
140+
*/
141+
boolean renegotiated() {
142+
byte[] currentSessionId = sslSocket.getSession().getId();
143+
144+
// Intentionally avoiding locking and MessageDigest.isEqual
145+
if (Arrays.equals(currentSessionId, lastSslSessionId)) {
146+
return false;
147+
}
148+
149+
lastSslSessionId = currentSessionId;
150+
return true;
151+
}
113152
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Copyright (c) 2024 Oracle and/or its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.helidon.common.socket;
18+
19+
import java.security.Principal;
20+
import java.util.Optional;
21+
22+
import javax.net.ssl.SSLPeerUnverifiedException;
23+
import javax.net.ssl.SSLSession;
24+
import javax.net.ssl.SSLSocket;
25+
26+
import org.junit.jupiter.api.Test;
27+
28+
import static org.hamcrest.MatcherAssert.assertThat;
29+
import static org.hamcrest.Matchers.is;
30+
import static org.junit.jupiter.api.Assertions.assertFalse;
31+
import static org.junit.jupiter.api.Assertions.assertTrue;
32+
import static org.mockito.Mockito.mock;
33+
import static org.mockito.Mockito.when;
34+
35+
class TlsSocketTest {
36+
37+
private final SSLSocket sslSocket;
38+
private final SSLSession sslSession1;
39+
private final SSLSession sslSession2;
40+
41+
public TlsSocketTest() throws SSLPeerUnverifiedException {
42+
sslSocket = mock(SSLSocket.class);
43+
sslSession1 = mock(SSLSession.class);
44+
sslSession2 = mock(SSLSession.class);
45+
46+
Principal principal1 = mock(Principal.class);
47+
Principal principal2 = mock(Principal.class);
48+
49+
when(principal1.getName()).thenReturn("Frank");
50+
when(principal2.getName()).thenReturn("Jack");
51+
52+
when(sslSession1.getId()).thenReturn(new byte[] {'a', 'b', 'c'});
53+
when(sslSession2.getId()).thenReturn(new byte[] {'d', 'e', 'f'});
54+
55+
when(sslSession1.getPeerPrincipal()).thenReturn(principal1);
56+
when(sslSession2.getPeerPrincipal()).thenReturn(principal2);
57+
}
58+
59+
@Test
60+
void renegotiationDetectionBySslSessionId() {
61+
when(sslSocket.getSession()).thenReturn(sslSession1);
62+
TlsSocket serverSocket = TlsSocket.server(sslSocket, "test1", "test2");
63+
64+
assertFalse(serverSocket.renegotiated());
65+
66+
// renegotiate
67+
when(sslSocket.getSession()).thenReturn(sslSession2);
68+
69+
assertTrue(serverSocket.renegotiated());
70+
71+
// detection is not reentrant
72+
assertFalse(serverSocket.renegotiated());
73+
}
74+
75+
@Test
76+
void lazyPeerInfo() {
77+
when(sslSocket.getSession()).thenReturn(sslSession1);
78+
TlsSocket serverSocket = TlsSocket.server(sslSocket, "test1", "test2");
79+
80+
assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Frank");
81+
assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Frank");
82+
}
83+
84+
@Test
85+
void lazyPeerInfoRenegotiated() {
86+
when(sslSocket.getSession()).thenReturn(sslSession1);
87+
TlsSocket serverSocket = TlsSocket.server(sslSocket, "test1", "test2");
88+
89+
assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Frank");
90+
assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Frank");
91+
92+
// renegotiate
93+
when(sslSocket.getSession()).thenReturn(sslSession2);
94+
95+
assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Jack");
96+
assertPrincipal(serverSocket.remotePeer().tlsPrincipal(), "Jack");
97+
}
98+
99+
private void assertPrincipal(Optional<Principal> act, String expectedName) {
100+
assertTrue(act.isPresent());
101+
assertThat(act.get().getName(), is(expectedName));
102+
}
103+
}

common/tls/src/main/java/io/helidon/common/tls/TlsReloadableX509KeyManager.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023 Oracle and/or its affiliates.
2+
* Copyright (c) 2023, 2024 Oracle and/or its affiliates.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,9 +22,10 @@
2222
import java.security.cert.X509Certificate;
2323
import java.util.Objects;
2424

25+
import javax.net.ssl.X509ExtendedKeyManager;
2526
import javax.net.ssl.X509KeyManager;
2627

27-
class TlsReloadableX509KeyManager implements X509KeyManager, TlsReloadableComponent {
28+
class TlsReloadableX509KeyManager extends X509ExtendedKeyManager implements TlsReloadableComponent {
2829
private static final System.Logger LOGGER = System.getLogger(TlsReloadableX509KeyManager.class.getName());
2930

3031
private volatile X509KeyManager keyManager;

webserver/tests/mtls/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,10 @@
4848
<artifactId>hamcrest-all</artifactId>
4949
<scope>test</scope>
5050
</dependency>
51+
<dependency>
52+
<groupId>io.helidon.logging</groupId>
53+
<artifactId>helidon-logging-jul</artifactId>
54+
<scope>test</scope>
55+
</dependency>
5156
</dependencies>
5257
</project>

webserver/tests/mtls/src/main/resources/logging.properties

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#
2-
# Copyright (c) 2022, 2023 Oracle and/or its affiliates.
2+
# Copyright (c) 2022, 2024 Oracle and/or its affiliates.
33
#
44
# Licensed under the Apache License, Version 2.0 (the "License");
55
# you may not use this file except in compliance with the License.
@@ -13,8 +13,8 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
#
16-
handlers=java.util.logging.ConsoleHandler
17-
java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS.%1$tL %5$s%6$s%n
16+
handlers=io.helidon.logging.jul.HelidonConsoleHandler
17+
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
1818
# Global logging level. Can be overridden by specific loggers
1919
.level=INFO
2020

0 commit comments

Comments
 (0)