bryancall commented on code in PR #13736:
URL: https://github.com/apache/trafficserver/pull/13736#discussion_r4124574159
##########
doc/admin-guide/files/records.yaml.en.rst:
##########
@@ -4642,6 +4642,19 @@ SSL Termination
note that OpenSSL session tickets are sensitive to the version of the
ca-certificates. Once the
file is changed with new tickets, use :option:`traffic_ctl config reload`
to begin using them.
+ For a connection to an address with its own ``dest_ip`` entry in
:file:`ssl_multicert.yaml`,
+ the ticket keys are derived from these keys and that entry's certificate.
This applies to every
+ client, including one whose SNI selects a different certificate, so a
ticket resumes only on
+ addresses whose ``dest_ip`` entries serve the same certificate. Servers
sharing this file resume
+ each other's tickets for such an address only if they serve the same
certificate. A ``dest_ip``
+ entry with ``action: tunnel`` has no certificate, so connections to it use
these keys as they
+ are. Two consequences follow:
Review Comment:
Nit: "Two consequences follow:" now comes straight after the tunnel
sentence, so it reads as if the two bullets are consequences of the tunnel
exception. Moving the `action: tunnel` sentence below the bullets would fix
that.
##########
src/iocore/net/TLSSessionResumptionSupport.cc:
##########
@@ -103,18 +186,30 @@ TLSSessionResumptionSupport::processSessionTicket(SSL
*ssl, unsigned char *keyna
SSLCertificateConfig::scoped_config lookup;
SSLTicketKeyConfig::scoped_config params;
- // Get the IP address to look up the keyblock
- const IpEndpoint &ip = this->_getLocalEndpoint();
- SSLCertContext *cc = lookup->find(ip);
- ssl_ticket_key_block *keyblock = nullptr;
- if (cc == nullptr || cc->keyblock == nullptr) {
- // Try the default
- keyblock = params->default_global_keyblock;
- } else {
- keyblock = cc->keyblock.get();
- }
+ ssl_ticket_key_block *keyblock = params->default_global_keyblock;
ink_release_assert(keyblock != nullptr && keyblock->num_keys > 0);
+ // The context is looked up by destination address for every connection. A
client that sends no
+ // SNI has nothing else to keep its tickets apart from another address's, so
bind the global keys
+ // to that context's certificate. Connections to an address with no context
of its own use the
+ // global keys as they are, and so do connections to a tunnel entry, which
deliberately has no
+ // certificate and terminates only the clients another entry serves by SNI.
+ const IpEndpoint &ip = this->_getCertLookupEndpoint();
+ SSLCertContext *cc = lookup->find(ip);
+ unique_ssl_ticket_key_block cert_keyblock{nullptr, ticket_block_free};
+ if (cc != nullptr && cc->opt != SSLCertContextOption::OPT_TUNNEL) {
Review Comment:
Optional tightening. Tunnel addresses now share the global keys with
addresses that have no `dest_ip` entry, which is what master does. As a result,
a client that sends no SNI can resume on a tunnel address where its own full
handshake always fails (`no suitable signature algorithm`).
Two examples I ran: no-SNI at an address with no entry, then no-SNI at a
tunnel address, resumes. So does SNI `foo.com` at a tunnel address followed by
no-SNI at an address with no entry.
The per-certificate keys stay separate: a session from a tunnel address does
not resume on the `dest_ip` VIPs. So this is master's existing behaviour for
addresses without an entry, not something this PR adds.
If you want to close it anyway: on an `OPT_TUNNEL` context, use the global
keys only when `SSL_get_servername()` returns a name, and return 0 otherwise.
No-SNI clients can't complete a handshake there, so nothing that works today
would lose resumption. Fine as a follow-up.
##########
tests/gold_tests/tls/tls_resume_cert_partition.test.py:
##########
@@ -0,0 +1,254 @@
+'''
+Test that TLS session tickets are partitioned by the certificate selected by
destination address.
+'''
+# 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.
+
+import os
+import re
+import sys
+
+Test.Summary = '''
+Test that a session ticket issued on one dest_ip-selected certificate is not
+resumed on a different one, while servers sharing the ticket keys and the
+certificate still resume each other's tickets.
+'''
+
+Test.SkipUnless(Condition.HasOpenSSLVersion('1.1.1'))
+
+
+class TlsResumeCertPartition:
+ '''
+ Test that ticket resumption is partitioned by the certificate selected by
destination address.
+
+ A client that sends no SNI has its certificate chosen by destination
+ address, so the server name cannot tell two such connections apart.
+ Session tickets for every certificate are protected by the same globally
+ configured ticket keys, so a ticket carries nothing tying it to the
+ certificate that issued it unless the keys used for it depend on that
+ certificate. Without that, a ticket issued on one address resumes on
another
+ address serving a different certificate, and the client skips the
+ certificate it would otherwise have been shown.
+
+ Each run checks what the client observes, which certificate each leg is
+ served and whether it resumed, and the log checks that ATS records the
+ resumption the same way. The second address is the IPv6 loopback because it
+ needs no interface alias on any platform, unlike 127.0.0.2. For the PROXY
+ protocol runs the destination comes from the PROXY header, so documentation
+ addresses stand in for load balancer VIPs, one IPv4 pair and one IPv6 pair.
+ No request is routed to an origin: ATS answers each one itself, and only
the
+ TLS handshake and the resumption it logs matter here.
+ '''
+
+ _first_ip = '127.0.0.1'
+ _second_ip = '::1'
+ _first_vip = '192.0.2.1'
+ _second_vip = '192.0.2.2'
+ _first_vip6 = '2001:db8::1'
+ _second_vip6 = '2001:db8::2'
+ _first_cn = 'random.server.com'
+ _second_cn = 'foo.com'
+ _client = 'tls_resume_cert_partition_client.py'
+
+ def __init__(self) -> None:
+ '''Configure the ATS processes and test runs.'''
+ Test.Setup.Copy('file.ticket')
+ Test.Setup.Copy(self._client)
+ self.ticket_file = os.path.join(Test.RunDirectory, 'file.ticket')
+ self.ts = self._configure_ts('ts')
+ # A second instance sharing the ticket key file stands in for another
server in a fleet.
+ self.ts2 = self._configure_ts('ts2')
+ self._started = False
+ self._second_legs: list[tuple['Process', str, bool]] = []
+
+ for tls in ('1.3', '1.2'):
+ self._add_run(
+ f'A no-SNI TLS {tls} session resumes on the certificate that
issued it',
+ tls,
+ self._leg(self.ts, self._first_ip, f'/same-{tls}-first'),
+ self._leg(self.ts, self._first_ip, f'/same-{tls}-second'),
+ resumed=True,
+ cns=(self._first_cn, self._first_cn))
+ self._add_run(
+ f'A no-SNI TLS {tls} session does not resume on a different
certificate',
+ tls,
+ self._leg(self.ts, self._first_ip, f'/cross-{tls}-first'),
+ self._leg(self.ts, self._second_ip, f'/cross-{tls}-second'),
+ resumed=False,
+ cns=(self._first_cn, self._second_cn))
+ self._add_run(
+ 'A PROXY protocol session resumes on the VIP whose certificate
issued it',
+ '1.3',
+ self._leg(self.ts, self._first_ip, '/proxy-same-first',
self._first_vip),
+ self._leg(self.ts, self._first_ip, '/proxy-same-second',
self._first_vip),
+ resumed=True,
+ cns=(self._first_cn, self._first_cn))
+ self._add_run(
+ 'A PROXY protocol session does not resume on a VIP with a
different certificate',
+ '1.3',
+ self._leg(self.ts, self._first_ip, '/proxy-cross-first',
self._first_vip),
+ self._leg(self.ts, self._first_ip, '/proxy-cross-second',
self._second_vip),
+ resumed=False,
+ cns=(self._first_cn, self._second_cn))
+ self._add_run(
+ 'An IPv6 PROXY protocol session does not resume on a VIP with a
different certificate',
Review Comment:
Heads-up: on macOS this file now fails often, mostly in this run. In 13
runs, 8 failed with `SSLEOFError: UNEXPECTED_EOF_WHILE_READING`, and 7 of those
failures were in this IPv6 run. Linux CI is green.
Every failure has the same ATS-side pattern: `proxy protocol was enabled,
but Proxy Protocol header was not present`, followed by `SSL accept ... wrong
version number`. The cause looks older than this PR. In
`SSLNetVConnection::read_raw_data()`, `haveCheckedProxyProtocol = true` is set
on the first read even when it returns no bytes (EAGAIN). A PROXY header that
arrives after that first read is then handed to OpenSSL as TLS.
Having the client wait 200 ms between connect and sending the header makes
it fail 10 of 10 times. With the `ssl` debug tag on, the extra logging changes
the timing and it passes 100 of 100.
The line comes from #12290 and isn't in this diff, so this doesn't block the
PR. Setting the flag only once `r > 0` would probably fix it, as a separate
change. Alternatively, the client could send the PROXY header and ClientHello
in a single write so the test doesn't depend on the timing.
##########
tests/gold_tests/tls/tls_resume_cert_partition.test.py:
##########
@@ -0,0 +1,252 @@
+'''
+Test that a TLS session is not resumed against a different server certificate
when no SNI is sent.
+'''
+# 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
Review Comment:
Verified, resolved.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]