Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
53 changes: 53 additions & 0 deletions src/iocore/net/SSLUtils.cc
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
#endif

#include <algorithm>
#include <cstdint>
#include <thread>
#include <utility>
#include <string>
Expand All @@ -95,6 +96,7 @@ static int ssl_vc_index = -1;
static ink_mutex *mutex_buf = nullptr;
static bool open_ssl_initialized = false;

static DbgCtl dbg_ctl_ssl{"ssl"};
static DbgCtl dbg_ctl_ssl_load{"ssl_load"};
static DbgCtl dbg_ctl_ssl_session_cache{"ssl.session_cache"};
static DbgCtl dbg_ctl_ssl_error{"ssl.error"};
Expand Down Expand Up @@ -369,6 +371,53 @@ ssl_next_protos_advertised_callback(SSL *ssl, const unsigned char **out, unsigne
return SSL_TLSEXT_ERR_NOACK;
}

static bool
is_http2_prohibited_cipher(const SSL_CIPHER *cipher)
{
struct CipherRange {
uint16_t first;
uint16_t last;
};

// RFC 9113 Appendix A lists 276 prohibited TLS 1.2 cipher suites. The IANA
// identifiers for those suites form these 24 contiguous ranges.
static constexpr CipherRange prohibited_ranges[] = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

{0x0000, 0x001b},
{0x001e, 0x0046},
{0x0067, 0x006d},
{0x0084, 0x009d},
{0x00a0, 0x00a1},
{0x00a4, 0x00a9},
{0x00ac, 0x00c5},
{0x00ff, 0x00ff},
{0xc001, 0xc02a},
{0xc02d, 0xc02e},
{0xc031, 0xc051},
{0xc054, 0xc055},
{0xc058, 0xc05b},
{0xc05e, 0xc05f},
{0xc062, 0xc06b},
{0xc06e, 0xc07b},
{0xc07e, 0xc07f},
{0xc082, 0xc085},
{0xc088, 0xc089},
{0xc08c, 0xc08f},
{0xc092, 0xc09d},
{0xc0a0, 0xc0a1},
{0xc0a4, 0xc0a5},
{0xc0a8, 0xc0a9},
};

if (cipher == nullptr) {
return false;
}

const uint16_t cipher_id = SSL_CIPHER_get_protocol_id(cipher);

return std::any_of(std::begin(prohibited_ranges), std::end(prohibited_ranges),
[cipher_id](const CipherRange &range) { return cipher_id >= range.first && cipher_id <= range.last; });
}

int
ssl_alpn_select_callback(SSL *ssl, const unsigned char **out, unsigned char *outlen, const unsigned char *in, unsigned inlen,
void *)
Expand All @@ -377,6 +426,10 @@ ssl_alpn_select_callback(SSL *ssl, const unsigned char **out, unsigned char *out

ink_assert(alpns);
if (alpns) {
if (const SSL_CIPHER *cipher = SSL_get_pending_cipher(ssl); is_http2_prohibited_cipher(cipher)) {
Dbg(dbg_ctl_ssl, "disabling HTTP/2 for prohibited cipher %s", SSL_CIPHER_get_name(cipher));
alpns->disableProtocol(TS_ALPN_PROTOCOL_INDEX_HTTP_2_0);
}
return alpns->select_next_protocol(out, outlen, in, inlen);
}

Expand Down
94 changes: 94 additions & 0 deletions tests/gold_tests/tls/tls_h2_cipher_suite.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Verify that HTTP/2 is not negotiated with prohibited TLS cipher suites."""

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF 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.

Test.Summary = __doc__

Test.SkipUnless(Condition.HasOpenSSLVersion("1.1.1"))


class TestH2CipherSuite:
"""Verify HTTP/2 cipher suite restrictions."""

def __init__(self) -> None:
"""Configure Traffic Server and the cipher suite test runs."""
self._ts = self._configure_traffic_server()
self._configure_allowed_cipher_test()

for description, cipher in (
("non-ephemeral AEAD", "AES128-GCM-SHA256"),
("ephemeral CBC", "ECDHE-RSA-AES128-SHA256"),
):
self._configure_prohibited_cipher_test(description, cipher)

def _configure_traffic_server(self) -> 'Process':
"""Configure Traffic Server with allowed and prohibited cipher suites."""
ts = Test.MakeATSProcess("ts", enable_tls=True)
ts.addSSLfile("ssl/server.pem")
ts.addSSLfile("ssl/server.key")

ts.Disk.ssl_multicert_yaml.AddLines(
"""
ssl_multicert:
- dest_ip: "*"
ssl_cert_name: server.pem
ssl_key_name: server.key
""".split("\n"))

ts.Disk.records_config.update(
{
"proxy.config.ssl.server.cert.path": ts.Variables.SSLDir,
"proxy.config.ssl.server.private_key.path": ts.Variables.SSLDir,
"proxy.config.ssl.server.version.min": 2,
"proxy.config.ssl.server.version.max": 2,
"proxy.config.ssl.server.cipher_suite":
"ECDHE-RSA-AES128-GCM-SHA256:"
"AES128-GCM-SHA256:"
"ECDHE-RSA-AES128-SHA256:"
"@SECLEVEL=0",
})
return ts

def _configure_allowed_cipher_test(self) -> None:
"""Verify that an ephemeral AEAD cipher can negotiate HTTP/2."""
tr = Test.AddTestRun("Allow HTTP/2 with an ephemeral AEAD cipher")
tr.Processes.Default.Command = (
"openssl s_client -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 "
f"-alpn h2,http/1.1 -connect 127.0.0.1:{self._ts.Variables.ssl_port} </dev/null")
tr.Processes.Default.StartBefore(self._ts)
tr.Processes.Default.Streams.All += Testers.IncludesExpression("ALPN protocol: h2", "HTTP/2 should be negotiated")
tr.ReturnCode = 0
tr.StillRunningAfter = self._ts

def _configure_prohibited_cipher_test(self, description: str, cipher: str) -> None:
"""Verify that a prohibited cipher falls back to HTTP/1.1.

:param description: A human-readable description of the cipher.
:param cipher: The OpenSSL cipher suite name.
"""
tr = Test.AddTestRun(f"Fall back to HTTP/1.1 with a prohibited {description} cipher")
tr.Processes.Default.Command = (
"printf 'GET / HTTP/1.1\\r\\nHost: example.com\\r\\nConnection: close\\r\\n\\r\\n' | "
f"openssl s_client -ign_eof -tls1_2 -cipher {cipher} "
f"-alpn h2,http/1.1 -connect 127.0.0.1:{self._ts.Variables.ssl_port}")
tr.Processes.Default.Streams.All += Testers.IncludesExpression("ALPN protocol: http/1.1", "HTTP/1.1 should be negotiated")
tr.Processes.Default.Streams.All += Testers.IncludesExpression("HTTP/1.1 404", "The HTTP/1.1 request should be processed")
tr.ReturnCode = 0
tr.StillRunningAfter = self._ts


TestH2CipherSuite()