Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS PSK implementation #1777

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Conversation

sunnysingh85
Copy link

This PR adds an implementation to use a pre-shared key (PSK) to complete a TLS handshake (TLS-PSK). This implementation is based on PSK APIs offered by BouncyCastle.

@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (!(msg instanceof ByteBuf byteBufMsg)) {
ctx.write(msg, promise);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This will continue propagating the message to the handlers. Is that intentional?

For this case, typically we want to do the following:

  • release the message using ReferenceCountUtil.safeRelease(msg) to avoid memory leaks
  • set failure on the promise with an appropriate exception.

if (availableOutputBytes != 0) {
byte[] outputBytes = new byte[availableOutputBytes];
tlsPskServerProtocol.readOutput(outputBytes, 0, availableOutputBytes);
ctx.write(Unpooled.wrappedBuffer(outputBytes), promise)
Copy link
Collaborator

Choose a reason for hiding this comment

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

This won't actually flush the buffer.
You might want to use .writeAndFlush instead in this case.

Copy link
Author

Choose a reason for hiding this comment

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

I was using .writeAndFlush previously but I ran into some issues during load testing with larger payloads and chunks. I can try to reproduce those and share more details.

try {
tlsPskServerProtocol.offerInput(bytesRead);
} catch (TlsFatalAlert tlsFatalAlert) {
writeOutputIfAvailable(ctx);
Copy link
Collaborator

@argha-c argha-c Jun 25, 2024

Choose a reason for hiding this comment

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

We should explicitly fail the handshake at this point? For ex, see this utility

It's unclear how/where this is handled?

Copy link
Author

Choose a reason for hiding this comment

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

Thanks for pointing this out. I'll update the code.

if (appDataAvailable > 0) {
byte[] appData = new byte[appDataAvailable];
tlsPskServerProtocol.readInput(appData, 0, appDataAvailable);
out.add(Unpooled.wrappedBuffer(appData));
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to pass this data down to other handlers?

Copy link
Author

Choose a reason for hiding this comment

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

Based on my understanding, the fact that TlsPskHandler is a ByteToMessageDecoder, adding the decoded output to List<Object> out does the job of passing down the data to other handlers. LMK if I'm missing the point that you are making.


@Override
public String getCipherSuite() {
return SUPPORTED_TLS_PSK_CIPHER_SUITE_MAP.get(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the suite map the accurate value here?
Or should it be derived from the underlying session, i.e. SessionParameters in this.getContext().getSession()?

Copy link
Author

Choose a reason for hiding this comment

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

I'm actually getting the negotiated cipher suite from context getContext().getSecurityParameters().getCipherSuite() which returns an integer but SSLSession requires me to return a name of the suite, the map is actually doing the conversion from int to String.

protected Vector getProtocolNames() {
Vector protocolNames = new Vector();
protocolNames.addElement(ProtocolName.HTTP_1_1);
protocolNames.addElement(ProtocolName.HTTP_2_TLS);
Copy link
Collaborator

Choose a reason for hiding this comment

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

This list should probably be injected, esp. for clients we don't want to advertise h2 for.

@Override
@SneakyThrows // TODO: Ask BC folks to see if getExternalPSK can throw a checked exception
public TlsPSKExternal getExternalPSK(Vector clientPskIdentities) {
byte[] clientPskIdentity = ((PskIdentity)clientPskIdentities.get(0)).getIdentity();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is the identity we want guaranteed to be first in an ordered list?

Copy link
Author

Choose a reason for hiding this comment

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

AFAIK we only plan to support one PSK identity in the hint, so it should be safe. And BC only invokes this method if clientPskIdentities is non empty.

psk = externalTlsPskProvider.provide(clientPskIdentity, this.context.getSecurityParametersHandshake().getClientRandom());
}catch (PskCreationFailureException e) {
throw switch (e.getTlsAlertMessage()) {
case unknown_psk_identity -> new TlsFatalAlert(AlertDescription.unknown_psk_identity, "Unknown or null client PSk identity");
Copy link
Collaborator

@argha-c argha-c Jun 25, 2024

Choose a reason for hiding this comment

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

It's unclear from the API if this fails the handshake. Can you clarify how this aborts the handshake?
We probably need to handle things like firing SslCloseCompletionEvent and cleanup manually anyway.

import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

public class TlsPskHandler extends ByteToMessageDecoder
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it might be nice to break this class into two pieces: a decoder and a handler. That way the TlsPskHandler can extend ChannelDuplexHandler and avoid all the boilerplate of needing to implement the full interfaces. A pattern I've seen before (that may work) is to implement handlerAdded in TlsPskHanlder and use it to add the decoder to the pipeline automatically

Copy link
Author

Choose a reason for hiding this comment

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

This made the code so much cleaner, thanks for this comment!

@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
tlsPskServer =
new ZuulPskServer(new JcaTlsCryptoProvider().create(new SecureRandom()), registry, externalTlsPskProvider, ctx);
Copy link
Contributor

Choose a reason for hiding this comment

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

can you make this SecureRandom a static constant instead? There isn't any concurrency benefits to create a new one every time

}
}

static class TlsPskServerProtocol extends TlsServerProtocol {
Copy link
Contributor

Choose a reason for hiding this comment

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

for improved readability it might be nice to extract these static inner classes

@@ -98,13 +105,23 @@ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exc
serverCert = session.getLocalCertificates()[0];
}

Boolean tlsHandshakeUsingExternalPSK = ctx.channel()
.attr(ZuulPskServer.TLS_HANDSHAKE_USING_EXTERNAL_PSK)
.get();
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks like a NPE risk since the SslHandshakeInfo ctor will unbox this to a boolean. Is TLS_HANDSHAKE_USING_EXTERNAL_PSK always going to be set, or should this be set to false in the case of the attribute having not been set on the channel?

}

@Override
@SneakyThrows
Copy link
Contributor

Choose a reason for hiding this comment

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

Are there other checked exceptions that you're not handling?

@@ -70,6 +75,42 @@ public Http2OrHttpHandler(
this.addHttpHandlerFn = addHttpHandlerFn;
}

@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: add a javadoc saying this method is inspired by ApplicationProtocolNegotiationHandler.userEventTriggered

static byte[] readDirect(ByteBuf byteBufMsg) {
int length = byteBufMsg.readableBytes();
byte[] dest = new byte[length];
byteBufMsg.readSlice(length).getBytes(0, dest);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I think byteBufMsg.readBytes(dest) essentially does the same thing, but is a little easier to parse

promise.setFailure(new IllegalStateException("Failed to write message on the channel. Message is not a ByteBuf"));
return;
}
byte[] appDataBytes = byteBufMsg.hasArray() ? byteBufMsg.array() : TlsPskUtils.readDirect(byteBufMsg);
Copy link
Contributor

Choose a reason for hiding this comment

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

could all of this logic (including the safeRelease) be put in the helper method?

}

@Override
public void notifyAlertRaised(short alertLevel, short alertDescription, String message, Throwable cause) {
Copy link
Contributor

Choose a reason for hiding this comment

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

There is one other event netty fires that we'd be interested in (if possible): https://github.com/netty/netty/blob/4.1/handler/src/main/java/io/netty/handler/ssl/SslCloseCompletionEvent.java netty will fire this event when the the ssl session is closed. Does bouncy castle trigger any similar alerts or anything when that happens?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants