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 11 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ subprojects {
mockito: 'org.mockito:mockito-core:5.+',
slf4j: "org.slf4j:slf4j-api:1.7.36",
truth: 'com.google.truth:truth:1.1.5',
awaitility: 'org.awaitility:awaitility:4.2.0'
awaitility: 'org.awaitility:awaitility:4.2.0',
lombok: 'org.projectlombok:lombok:1.18.30'
]
}

Expand Down
6 changes: 6 additions & 0 deletions zuul-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@ apply plugin: "java-library"

dependencies {

compileOnly libraries.lombok
annotationProcessor(libraries.lombok)

implementation libraries.guava
// TODO(carl-mastrangelo): this can be implementation; remove Logger from public api points.
api libraries.slf4j

implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'
implementation 'org.bouncycastle:bcpkix-jdk18on:1.78.1'
implementation 'org.bouncycastle:bctls-jdk18on:1.78.1'

implementation 'com.fasterxml.jackson.core:jackson-core:2.16.1'
api 'com.fasterxml.jackson.core:jackson-databind:2.16.1'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.netflix.netty.common.ssl;

import com.netflix.zuul.netty.server.psk.ClientPSKIdentityInfo;
import io.netty.handler.ssl.ClientAuth;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
Expand All @@ -31,20 +32,26 @@ public class SslHandshakeInfo {
private final Certificate serverCertificate;
private final X509Certificate clientCertificate;
private final boolean isOfIntermediary;
private final boolean usingExternalPSK;
private final ClientPSKIdentityInfo clientPSKIdentityInfo;

public SslHandshakeInfo(
boolean isOfIntermediary,
String protocol,
String cipherSuite,
ClientAuth clientAuthRequirement,
Certificate serverCertificate,
X509Certificate clientCertificate) {
X509Certificate clientCertificate,
boolean usingExternalPSK,
ClientPSKIdentityInfo clientPSKIdentityInfo) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

consider keeping the original constructor for backwards compatibility, and adding a new constructor with these two new parameters

this.protocol = protocol;
this.cipherSuite = cipherSuite;
this.clientAuthRequirement = clientAuthRequirement;
this.serverCertificate = serverCertificate;
this.clientCertificate = clientCertificate;
this.isOfIntermediary = isOfIntermediary;
this.usingExternalPSK = usingExternalPSK;
this.clientPSKIdentityInfo = clientPSKIdentityInfo;
}

public boolean isOfIntermediary() {
Expand All @@ -71,6 +78,14 @@ public X509Certificate getClientCertificate() {
return clientCertificate;
}

public boolean usingExternalPSK() {
return usingExternalPSK;
}

public ClientPSKIdentityInfo geClientPSKIdentityInfo() {
return clientPSKIdentityInfo;
}

@Override
public String toString() {
return "SslHandshakeInfo{" + "protocol='"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.netflix.netty.common.channel.config.CommonChannelConfigKeys;
import com.netflix.netty.common.http2.DynamicHttp2FrameLogger;
import com.netflix.zuul.netty.server.BaseZuulChannelInitializer;
import com.netflix.zuul.netty.server.psk.TlsPskHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
Expand All @@ -33,12 +34,13 @@
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import io.netty.util.AttributeKey;
import java.util.function.Consumer;

/**
* Http2 Or Http Handler
*
* <p>
* Author: Arthur Gonigberg
* Date: December 15, 2017
*/
Expand All @@ -47,6 +49,8 @@ public class Http2OrHttpHandler extends ApplicationProtocolNegotiationHandler {
public static final String PROTOCOL_HTTP_1_1 = "HTTP/1.1";
public static final String PROTOCOL_HTTP_2 = "HTTP/2";

private static final String FALLBACK_APPLICATION_PROTOCOL = ApplicationProtocolNames.HTTP_1_1;

private static final DynamicHttp2FrameLogger FRAME_LOGGER =
new DynamicHttp2FrameLogger(LogLevel.DEBUG, Http2FrameCodec.class);

Expand All @@ -62,7 +66,7 @@ public Http2OrHttpHandler(
ChannelHandler http2StreamHandler,
ChannelConfig channelConfig,
Consumer<ChannelPipeline> addHttpHandlerFn) {
super(ApplicationProtocolNames.HTTP_1_1);
super(FALLBACK_APPLICATION_PROTOCOL);
this.http2StreamHandler = http2StreamHandler;
this.maxConcurrentStreams = channelConfig.get(CommonChannelConfigKeys.maxConcurrentStreams);
this.initialWindowSize = channelConfig.get(CommonChannelConfigKeys.initialWindowSize);
Expand All @@ -72,6 +76,45 @@ public Http2OrHttpHandler(
this.addHttpHandlerFn = addHttpHandlerFn;
}

/**
* this method is inspired by ApplicationProtocolNegotiationHandler.userEventTriggered
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
Copy link
Collaborator

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

if (evt instanceof SslHandshakeCompletionEvent handshakeEvent) {
if (handshakeEvent.isSuccess()) {
TlsPskHandler tlsPskHandler = ctx.channel().pipeline().get(TlsPskHandler.class);
if (tlsPskHandler != null) {
// PSK mode
try {
String tlsPskApplicationProtocol = tlsPskHandler.getApplicationProtocol();
configurePipeline(
ctx,
tlsPskApplicationProtocol != null
? tlsPskApplicationProtocol
: FALLBACK_APPLICATION_PROTOCOL);
} catch (Throwable cause) {
exceptionCaught(ctx, cause);
} finally {
// Handshake failures are handled in exceptionCaught(...).
if (handshakeEvent.isSuccess()) {
removeSelfIfPresent(ctx);
}
}
} else {
// non PSK mode
super.userEventTriggered(ctx, evt);
}
} else {
// handshake failures
// TODO sunnys - handle PSK handshake failures
super.userEventTriggered(ctx, evt);
}
} else {
super.userEventTriggered(ctx, evt);
}
}

@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
Expand Down Expand Up @@ -120,4 +163,11 @@ private void configureHttp2(ChannelPipeline pipeline) {
private void configureHttp1(ChannelPipeline pipeline) {
addHttpHandlerFn.accept(pipeline);
}

private void removeSelfIfPresent(ChannelHandlerContext ctx) {
ChannelPipeline pipeline = ctx.pipeline();
if (!ctx.isRemoved()) {
pipeline.remove(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Copyright 2024 Netflix, Inc.
*
* 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 com.netflix.zuul.netty.server.psk;

public record ClientPSKIdentityInfo(byte[] clientPSKIdentity) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright 2024 Netflix, Inc.
*
* 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 com.netflix.zuul.netty.server.psk;


public interface ExternalTlsPskProvider {
byte[] provide(byte[] clientPskIdentity, byte[] clientRandom) throws PskCreationFailureException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2024 Netflix, Inc.
*
* 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 com.netflix.zuul.netty.server.psk;

public class PskCreationFailureException extends Exception {

public enum TlsAlertMessage {
/**
* The server does not recognize the (client) PSK identity
*/
unknown_psk_identity,
/**
* The (client) PSK identity existed but the key was incorrect
*/
decrypt_error,
}

private final TlsAlertMessage tlsAlertMessage;

public PskCreationFailureException(TlsAlertMessage tlsAlertMessage, String message) {
super(message);
this.tlsAlertMessage = tlsAlertMessage;
}

public PskCreationFailureException(TlsAlertMessage tlsAlertMessage, String message, Throwable cause) {
super(message, cause);
this.tlsAlertMessage = tlsAlertMessage;
}

public TlsAlertMessage getTlsAlertMessage() {
return tlsAlertMessage;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2024 Netflix, Inc.
*
* 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 com.netflix.zuul.netty.server.psk;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.ssl.SslHandshakeCompletionEvent;
import org.bouncycastle.tls.TlsFatalAlert;

import java.util.List;

public class TlsPskDecoder extends ByteToMessageDecoder {

private final TlsPskServerProtocol tlsPskServerProtocol;

public TlsPskDecoder(TlsPskServerProtocol tlsPskServerProtocol) {
this.tlsPskServerProtocol = tlsPskServerProtocol;
}

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
final byte[] bytesRead = in.hasArray() ? in.array() : TlsPskUtils.readDirect(in);
try {
tlsPskServerProtocol.offerInput(bytesRead);
} catch (TlsFatalAlert tlsFatalAlert) {
writeOutputIfAvailable(ctx);
ctx.fireUserEventTriggered(new SslHandshakeCompletionEvent(tlsFatalAlert));
ctx.close();
return;
}
writeOutputIfAvailable(ctx);
final int appDataAvailable = tlsPskServerProtocol.getAvailableInputBytes();
if (appDataAvailable > 0) {
byte[] appData = new byte[appDataAvailable];
tlsPskServerProtocol.readInput(appData, 0, appDataAvailable);
out.add(Unpooled.wrappedBuffer(appData));
}
}

private void writeOutputIfAvailable(ChannelHandlerContext ctx) {
final int availableOutputBytes = tlsPskServerProtocol.getAvailableOutputBytes();
// output is available immediately (handshake not complete), pipe that back to the client right away
if (availableOutputBytes != 0) {
byte[] outputBytes = new byte[availableOutputBytes];
tlsPskServerProtocol.readOutput(outputBytes, 0, availableOutputBytes);
ctx.writeAndFlush(Unpooled.wrappedBuffer(outputBytes))
.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
}
}
}
Loading
Loading