Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Support OPTIONAL SSL in the ResumptionController
Motivation:
If TLS is `OPTIONAL` then there won't always be a verified peer.
This means there will be no calls on the server `TrustManager` to check if the client is trusted.
That makes it look like a resumed session to the resumption controller.
But there won't be any verified peer on the session, and trying to get the peer certificates will throw an exception.

Modification:
- Make the `ResumptionController` consider if the engine is in client mode, or if it requires client authentication, and only propagate `SSLPeerUnverifiedException` if so.
- Make the `ResumptionController` swallow the `SSLPeerUnverifiedException` when client auth is OPTIONAL or NONE.
- Add a test for this scenario.

Result:
The `ResumableX509ExtendedTrustManager` interface can now be used together with `ClientAuth.OPTIONAL`.
  • Loading branch information
chrisvest committed Oct 17, 2024
commit 7eb599ce048186c822249ea114a91bb8d489c561
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package io.netty.handler.ssl;

import io.netty.util.internal.ObjectUtil;
import io.netty.util.internal.PlatformDependent;
import io.netty.util.internal.SuppressJava6Requirement;

Expand All @@ -29,6 +28,7 @@
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509ExtendedTrustManager;

Expand Down Expand Up @@ -67,14 +67,24 @@ public void remove(SSLEngine engine) {
public boolean validateResumeIfNeeded(SSLEngine engine)
throws CertificateException, SSLPeerUnverifiedException {
ResumableX509ExtendedTrustManager tm;
boolean valid = engine.getSession().isValid();
if (valid && (tm = resumableTm.get()) != null) {
Certificate[] peerCertificates = engine.getSession().getPeerCertificates();

SSLSession session = engine.getSession();
boolean valid = session.isValid();
if (valid && (engine.getUseClientMode() || engine.getNeedClientAuth() || engine.getWantClientAuth()) &&
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add a comment clarifying the goal of this conditional?

getUseClientMode -> I assume this is to cover the case of "always do this on the client"

getNeedClientAuth || getWantClientAuth -> isn't the default treated by some engines as "want"? for example iirc openssl flavors will have the server request the client's certificate but if they don't return one it will succeed. so I'm just trying to understand if client/server need to be consistent and if not, why.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Scottmitch I've added comments explaining the logic. Please take a look.

(tm = resumableTm.get()) != null) {
// Unwrap JdkSslEngines because they add their inner JDK SSLEngine objects to the set.
engine = unwrapEngine(engine);

if (!confirmedValidations.remove(engine)) {
Certificate[] peerCertificates;
try {
peerCertificates = session.getPeerCertificates();
} catch (SSLPeerUnverifiedException e) {
if (engine.getUseClientMode() || engine.getNeedClientAuth()) {
throw e;
}
return false;
}

// This is a resumed session.
if (engine.getUseClientMode()) {
// We are the client, resuming a session trusting the server
Expand Down
74 changes: 74 additions & 0 deletions handler/src/test/java/io/netty/handler/ssl/SSLEngineTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4040,6 +4040,80 @@ public void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Excep
}
}

@Timeout(value = 60, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
@MethodSource("newTestParams")
@ParameterizedTest
public void mustNotCallResumeWhenClientAuthIsOptionalAndNoClientCertIsProvided(SSLEngineTestParam param)
throws Exception {
SelfSignedCertificate ssc = CachedSelfSignedCertificate.getCachedCertificate();
SessionValueSettingTrustManager clientTm = new SessionValueSettingTrustManager("key", "client");
SessionValueSettingTrustManager serverTm = new SessionValueSettingTrustManager("key", "server");
clientSslCtx = wrapContext(param, SslContextBuilder.forClient()
.trustManager(clientTm)
.sslProvider(sslClientProvider())
.sslContextProvider(clientSslContextProvider())
.protocols(param.protocols())
.ciphers(param.ciphers())
.build()); // Client provides no certificate!
serverSslCtx = wrapContext(param, SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.trustManager(serverTm)
.sslProvider(sslServerProvider())
.sslContextProvider(serverSslContextProvider())
.protocols(param.protocols())
.ciphers(param.ciphers())
.clientAuth(ClientAuth.OPTIONAL) // Client auth is OPTIONAL!
.build());
final BlockingQueue<String> clientSessionValues = new LinkedBlockingQueue<String>();
final BlockingQueue<String> serverSessionValues = new LinkedBlockingQueue<String>();
OnNextMessage checkClient = new OnNextMessage() {
@Override
public void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
msg.release();
SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
SSLEngine engine = sslHandler.engine();
logger.debug("Client message received: {} ({}) {} {}",
engine.getSession(), engine.getHandshakeStatus(), isSessionReused(engine),
Arrays.toString(engine.getSession().getValueNames()));
Object value = engine.getSession().getValue("key");
clientSessionValues.put((String) value);
}
};
OnNextMessage checkServer = new OnNextMessage() {
@Override
public void messageReceived(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
SslHandler sslHandler = ctx.pipeline().get(SslHandler.class);
SSLEngine engine = sslHandler.engine();
logger.debug("Server message received: {} ({}) {} {}",
engine.getSession(), engine.getHandshakeStatus(), isSessionReused(engine),
Arrays.toString(engine.getSession().getValueNames()));
Object value = engine.getSession().getValue("key");
serverSessionValues.put(value == null ? "NULL" : String.valueOf(value));
ctx.writeAndFlush(msg).addListener(ChannelFutureListener.CLOSE);
}
};

setupServer(param.type(), param.delegate());
InetSocketAddress addr = (InetSocketAddress) serverChannel.localAddress();
setupClient(param.type(), param.delegate(), "a.netty.io", addr.getPort());
for (int i = 0; i < 10; i++) {
clientReceiver.onNextMessages.offer(checkClient);
serverReceiver.onNextMessages.offer(checkServer);

ChannelFuture ccf = cb.connect(addr);
assertTrue(ccf.syncUninterruptibly().isSuccess());
clientChannel = ccf.channel();

clientChannel.writeAndFlush(clientChannel.alloc().buffer().writeInt(42)).sync();
assertEquals("client", clientSessionValues.take());
assertEquals("NULL", serverSessionValues.take());
clientChannel.closeFuture().sync();
}
assertTrue(clientReceiver.onNextMessages.isEmpty());
assertTrue(serverReceiver.onNextMessages.isEmpty());
assertTrue(clientSessionValues.isEmpty());
assertTrue(serverSessionValues.isEmpty());
}

private void buildClientSslContextForMTLS(
SSLEngineTestParam param, SelfSignedCertificate ssc, TrustManager clientTm) throws SSLException {
clientSslCtx = wrapContext(param, SslContextBuilder.forClient()
Expand Down