diff --git a/runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/internal/stream/TlsProxyFactory.java b/runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/internal/stream/TlsProxyFactory.java index 81a89251ac..0efbbbadea 100644 --- a/runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/internal/stream/TlsProxyFactory.java +++ b/runtime/binding-tls/src/main/java/io/aklivity/zilla/runtime/binding/tls/internal/stream/TlsProxyFactory.java @@ -18,10 +18,7 @@ import static io.aklivity.zilla.runtime.engine.buffer.BufferPool.NO_SLOT; import static io.aklivity.zilla.runtime.engine.concurrent.Signaler.NO_CANCEL_ID; import static java.lang.System.currentTimeMillis; -import static java.nio.ByteOrder.BIG_ENDIAN; import static java.util.concurrent.TimeUnit.SECONDS; -import static org.agrona.BitUtil.SIZE_OF_BYTE; -import static org.agrona.BitUtil.SIZE_OF_SHORT; import java.util.Optional; import java.util.function.Consumer; @@ -36,7 +33,14 @@ import io.aklivity.zilla.runtime.binding.tls.internal.config.TlsBindingConfig; import io.aklivity.zilla.runtime.binding.tls.internal.config.TlsRouteConfig; import io.aklivity.zilla.runtime.binding.tls.internal.types.OctetsFW; +import io.aklivity.zilla.runtime.binding.tls.internal.types.codec.TlsExtensionFW; +import io.aklivity.zilla.runtime.binding.tls.internal.types.codec.TlsExtensionsFW; +import io.aklivity.zilla.runtime.binding.tls.internal.types.codec.TlsHandshakeClientHelloFW; +import io.aklivity.zilla.runtime.binding.tls.internal.types.codec.TlsHandshakeMessageFW; import io.aklivity.zilla.runtime.binding.tls.internal.types.codec.TlsRecordFW; +import io.aklivity.zilla.runtime.binding.tls.internal.types.codec.TlsServerNameExtensionFW; +import io.aklivity.zilla.runtime.binding.tls.internal.types.codec.TlsServerNameTypeFW; +import io.aklivity.zilla.runtime.binding.tls.internal.types.codec.TlsSniHostNameFW; import io.aklivity.zilla.runtime.binding.tls.internal.types.stream.AbortFW; import io.aklivity.zilla.runtime.binding.tls.internal.types.stream.BeginFW; import io.aklivity.zilla.runtime.binding.tls.internal.types.stream.DataFW; @@ -92,6 +96,13 @@ public final class TlsProxyFactory implements TlsStreamFactory private final ResetFW.Builder resetRW = new ResetFW.Builder(); private final TlsRecordFW tlsRecordRO = new TlsRecordFW(); + private final TlsHandshakeMessageFW tlsHandshakeRO = new TlsHandshakeMessageFW(); + private final TlsHandshakeClientHelloFW tlsClientHelloRO = new TlsHandshakeClientHelloFW(); + private final TlsExtensionsFW tlsExtensionsRO = new TlsExtensionsFW(); + private final TlsExtensionFW tlsExtensionRO = new TlsExtensionFW(); + private final TlsServerNameExtensionFW tlsServerNameExRO = new TlsServerNameExtensionFW(); + private final TlsServerNameTypeFW tlsServerNameTypeRO = new TlsServerNameTypeFW(); + private final TlsSniHostNameFW tlsHostnameRO = new TlsSniHostNameFW(); private final TlsProxyDecoder decodeRecord = this::decodeRecord; private final TlsProxyDecoder decodeRecordBytes = this::decodeRecordBytes; @@ -446,41 +457,20 @@ private int decodeRecord( break decode; } - DirectBuffer message = tlsRecord.payload().value(); - int messageProgress = 0; - byte messageType = message.getByte(messageProgress++); - int messageLength = - (message.getByte(messageProgress++) & 0xff) << 16 | - (message.getByte(messageProgress++) & 0xff) << 8 | - (message.getByte(messageProgress++) & 0xff) << 0; - - if (messageType != MESSAGE_TYPE_CLIENT_HELLO || - messageProgress + messageLength != message.capacity()) + TlsHandshakeMessageFW tlsHandshake = tlsRecord.payload().get(tlsHandshakeRO::tryWrap); + if (tlsHandshake == null || + tlsHandshake.typeAndLength() >> 24 != MESSAGE_TYPE_CLIENT_HELLO || + (tlsHandshake.typeAndLength() & 0x00ff_ffff) + tlsHandshake.limit() > tlsRecord.limit()) { proxy.doNetReset(traceId); proxy.decoder = decodeIgnoreAll; break decode; } - // skip version - messageProgress += 2; - - // skip random - messageProgress += 32; - - // skip session id - messageProgress += SIZE_OF_BYTE + (message.getByte(messageProgress) & 0xff); - - // cipher suites - messageProgress += SIZE_OF_SHORT + (message.getShort(messageProgress, BIG_ENDIAN) & 0xffff); - - // compress methods - messageProgress += SIZE_OF_BYTE + (message.getByte(messageProgress) & 0xff); - - int extensionsLength = message.getShort(messageProgress, BIG_ENDIAN) & 0xffff; - messageProgress += SIZE_OF_SHORT; - - if (messageProgress + extensionsLength != message.capacity()) + TlsHandshakeClientHelloFW tlsClientHello = + tlsClientHelloRO.tryWrap(tlsRecord.buffer(), tlsHandshake.limit(), tlsRecord.limit()); + if (tlsClientHello == null || + tlsClientHello.limit() > tlsRecord.limit()) { proxy.doNetReset(traceId); proxy.decoder = decodeIgnoreAll; @@ -488,52 +478,72 @@ private int decodeRecord( } String serverName = null; - while (messageProgress < message.capacity()) - { - int extensionType = message.getShort(messageProgress, BIG_ENDIAN) & 0xffff; - messageProgress += SIZE_OF_SHORT; - int extensionLength = message.getShort(messageProgress, BIG_ENDIAN) & 0xffff; - messageProgress += SIZE_OF_SHORT; - if (messageProgress + extensionLength > message.capacity()) + if (tlsClientHello.limit() < tlsRecord.limit()) + { + TlsExtensionsFW tlsExtensions = + tlsExtensionsRO.tryWrap(tlsRecord.buffer(), tlsClientHello.limit(), tlsRecord.limit()); + if (tlsExtensions == null || + tlsExtensions.limit() != tlsRecord.limit()) { proxy.doNetReset(traceId); proxy.decoder = decodeIgnoreAll; break decode; } - if (extensionType == EXTENSION_TYPE_SNI) - { - int sniLength = message.getShort(messageProgress, BIG_ENDIAN) & 0xffff; - messageProgress += SIZE_OF_SHORT; + DirectBuffer tlsExtensionsBuf = tlsExtensions.value().value(); + int tlsExtensionsLimit = tlsExtensions.length(); + int tlsExtensionsProgress = 0; - if (messageProgress + sniLength > message.capacity()) + while (tlsExtensionsProgress < tlsExtensionsLimit) + { + TlsExtensionFW tlsExtension = + tlsExtensionRO.tryWrap(tlsExtensionsBuf, tlsExtensionsProgress, tlsExtensionsLimit); + if (tlsExtension == null || + tlsExtension.limit() > tlsRecord.limit()) { proxy.doNetReset(traceId); proxy.decoder = decodeIgnoreAll; break decode; } - int sniType = message.getByte(messageProgress++); - if (sniType == SNI_TYPE_HOSTNAME) + if (tlsExtension.type() == EXTENSION_TYPE_SNI) { - int hostnameLength = message.getShort(messageProgress, BIG_ENDIAN) & 0xffff; - messageProgress += SIZE_OF_SHORT; + TlsServerNameExtensionFW tlsServerNameEx = tlsExtension.data().get(tlsServerNameExRO::tryWrap); + if (tlsServerNameEx == null || + tlsServerNameEx.limit() > tlsRecord.limit()) + { + proxy.doNetReset(traceId); + proxy.decoder = decodeIgnoreAll; + break decode; + } - if (messageProgress + hostnameLength > message.capacity()) + int tlsServerNameTypeOffset = tlsServerNameEx.value().offset(); + TlsServerNameTypeFW tlsServerNameType = + tlsServerNameTypeRO.tryWrap(tlsExtensionsBuf, tlsServerNameTypeOffset, tlsExtensionsLimit); + if (tlsServerNameType == null) { proxy.doNetReset(traceId); proxy.decoder = decodeIgnoreAll; break decode; } - serverName = message.getStringWithoutLengthUtf8(messageProgress, hostnameLength); - messageProgress += hostnameLength; + if (tlsServerNameType.value() == SNI_TYPE_HOSTNAME) + { + TlsSniHostNameFW tlsHostname = + tlsHostnameRO.tryWrap(tlsExtensionsBuf, tlsServerNameType.limit(), tlsExtensionsLimit); + if (tlsHostname == null) + { + proxy.doNetReset(traceId); + proxy.decoder = decodeIgnoreAll; + break decode; + } + + serverName = tlsHostname.value().asString(); + } } - } - else - { - messageProgress += extensionLength; + + tlsExtensionsProgress = tlsExtension.limit(); } } diff --git a/runtime/binding-tls/src/main/zilla/protocol.idl b/runtime/binding-tls/src/main/zilla/protocol.idl index 42e9378337..2a3f66670a 100644 --- a/runtime/binding-tls/src/main/zilla/protocol.idl +++ b/runtime/binding-tls/src/main/zilla/protocol.idl @@ -51,5 +51,66 @@ scope protocol uint16 length; octets[length] payload; } + + struct TlsHandshakeMessage + { + int32 typeAndLength; + } + + struct TlsSessionId + { + uint8 length; + octets[length] value; + } + + struct TlsCipherSuites + { + uint16 length; + octets[length] value; + } + + struct TlsCompressionMethods + { + uint8 length; + octets[length] value; + } + + struct TlsExtensions + { + uint16 length; + octets[length] value; + } + + struct TlsExtension + { + uint16 type; + uint16 length; + octets[length] data; + } + + struct TlsServerNameExtension + { + uint16 length; + octets[length] value; + } + + struct TlsServerNameType + { + uint8 value; + } + + struct TlsSniHostName + { + string16 value; + } + + struct TlsHandshakeClientHello + { + TlsProtocolVersion version; + octets[32] random; + TlsSessionId session; + TlsCipherSuites suites; + TlsCompressionMethods methods; + } } } diff --git a/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/streams/ProxyIT.java b/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/streams/ProxyIT.java index 57af3bc50d..d1abece52d 100644 --- a/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/streams/ProxyIT.java +++ b/runtime/binding-tls/src/test/java/io/aklivity/zilla/runtime/binding/tls/internal/streams/ProxyIT.java @@ -48,6 +48,16 @@ public class ProxyIT @Rule public final TestRule chain = outerRule(engine).around(k3po).around(timeout); + @Test + @Configuration("proxy.yaml") + @Specification({ + "${proxy}/client/client.hello.without.ext/client", + "${proxy}/server/client.hello.without.ext/server" }) + public void shouldProxyClientHelloWithoutExt() throws Exception + { + k3po.finish(); + } + @Test @Configuration("proxy.sni.yaml") @Specification({ diff --git a/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/config/proxy.yaml b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/config/proxy.yaml new file mode 100644 index 0000000000..b23da98861 --- /dev/null +++ b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/config/proxy.yaml @@ -0,0 +1,23 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity licenses this file to you 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. +# + +--- +name: test +bindings: + net0: + type: tls + kind: proxy + exit: net1 diff --git a/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/client/client.hello.without.ext/client.rpt b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/client/client.hello.without.ext/client.rpt new file mode 100644 index 0000000000..255cf6d578 --- /dev/null +++ b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/client/client.hello.without.ext/client.rpt @@ -0,0 +1,42 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity licenses this file to you 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. +# + +connect "zilla://streams/net0" + option zilla:window 65536 + option zilla:transmission "duplex" + option zilla:byteorder "network" + +connected + +write [0x16] # handshake record + [0x03] [0x03] # version 1.2 + 45s # length + [0x01] # client hello message + [0x00 0x00 0x29] # length + [0x03] [0x03] # version 1.2 + [0x00 0x00 0x00 0x00] # random + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00] # legacy session id + 2s # cipher suites + [0xc0 0x30] # TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + [0x01] # legacy compression methods + [0x00] # null diff --git a/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/client/client.hello.without.ext/server.rpt b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/client/client.hello.without.ext/server.rpt new file mode 100644 index 0000000000..47766634fc --- /dev/null +++ b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/client/client.hello.without.ext/server.rpt @@ -0,0 +1,44 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity licenses this file to you 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. +# + +accept "zilla://streams/net0" + option zilla:window 65536 + option zilla:transmission "duplex" + option zilla:byteorder "network" +accepted + +connected + +read [0x16] # handshake record + [0x03] [0x03] # version 1.2 + 45s # length + [0x01] # client hello message + [0x00 0x00 0x29] # length + [0x03] [0x03] # version 1.2 + [0x00 0x00 0x00 0x00] # random + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00] # legacy session id + 2s # cipher suites + [0xc0 0x30] # TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + [0x01] # legacy compression methods + [0x00] # null + diff --git a/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/server/client.hello.without.ext/client.rpt b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/server/client.hello.without.ext/client.rpt new file mode 100644 index 0000000000..8acd490b23 --- /dev/null +++ b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/server/client.hello.without.ext/client.rpt @@ -0,0 +1,42 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity licenses this file to you 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. +# + +connect "zilla://streams/net1" + option zilla:window 65536 + option zilla:transmission "duplex" + option zilla:byteorder "network" + +connected + +write [0x16] # handshake record + [0x03] [0x03] # version 1.2 + 45s # length + [0x01] # client hello message + [0x00 0x00 0x29] # length + [0x03] [0x03] # version 1.2 + [0x00 0x00 0x00 0x00] # random + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00] # legacy session id + 2s # cipher suites + [0xc0 0x30] # TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + [0x01] # legacy compression methods + [0x00] # null diff --git a/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/server/client.hello.without.ext/server.rpt b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/server/client.hello.without.ext/server.rpt new file mode 100644 index 0000000000..aeb2f971f1 --- /dev/null +++ b/specs/binding-tls.spec/src/main/scripts/io/aklivity/zilla/specs/binding/tls/streams/proxy/server/client.hello.without.ext/server.rpt @@ -0,0 +1,44 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity licenses this file to you 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. +# + +accept "zilla://streams/net1" + option zilla:window 65536 + option zilla:transmission "duplex" + option zilla:byteorder "network" +accepted + +connected + +read [0x16] # handshake record + [0x03] [0x03] # version 1.2 + 45s # length + [0x01] # client hello message + [0x00 0x00 0x29] # length + [0x03] [0x03] # version 1.2 + [0x00 0x00 0x00 0x00] # random + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00 0x00 0x00 0x00] + [0x00] # legacy session id + 2s # cipher suites + [0xc0 0x30] # TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + [0x01] # legacy compression methods + [0x00] # null + diff --git a/specs/binding-tls.spec/src/test/java/io/aklivity/zilla/specs/binding/tls/stream/ProxyIT.java b/specs/binding-tls.spec/src/test/java/io/aklivity/zilla/specs/binding/tls/stream/ProxyIT.java index 63406d3a3f..2c66a6f3e3 100644 --- a/specs/binding-tls.spec/src/test/java/io/aklivity/zilla/specs/binding/tls/stream/ProxyIT.java +++ b/specs/binding-tls.spec/src/test/java/io/aklivity/zilla/specs/binding/tls/stream/ProxyIT.java @@ -37,6 +37,15 @@ public class ProxyIT @Rule public final TestRule chain = outerRule(k3po).around(timeout); + @Test + @Specification({ + "${proxy}/client/client.hello.without.ext/client", + "${proxy}/client/client.hello.without.ext/server"}) + public void shouldSendClientHelloWithoutExt() throws Exception + { + k3po.finish(); + } + @Test @Specification({ "${proxy}/client/client.hello.with.sni/client", @@ -46,6 +55,15 @@ public void shouldSendClientHelloWithServerName() throws Exception k3po.finish(); } + @Test + @Specification({ + "${proxy}/server/client.hello.without.ext/client", + "${proxy}/server/client.hello.without.ext/server"}) + public void shouldReceiveClientHelloWithoutExt() throws Exception + { + k3po.finish(); + } + @Test @Specification({ "${proxy}/server/client.hello.with.sni/client",