From 9e71c1b8b64998bd54919608ce2f6715342620e1 Mon Sep 17 00:00:00 2001 From: Nikos Mavrogiannopoulos Date: Sun, 19 Jul 2026 21:53:44 +0200 Subject: [PATCH] worker: use TLS 1.3 transparent rekey on TLS 1.3 CSTP sessions TLS 1.3 has no renegotiation and provides transparent rekeys but this was not used by the server. Instead rekey-method=ssl was silently downgraded to new-tunnel, causing a full tunnel/TUN-device rebuild (and a brief data-path interruption) on every rekey whenever a client negotiated TLS 1.3. This commit simplifies that handling by taking advantage of TLS 1.3's own rekey mechanism instead: the server now performs the "ssl" rekey on TLS 1.3 sessions via the standard TLS 1.3 KeyUpdate message. TLS <= 1.2 rekey behavior (client-driven rehandshake, gated on RFC 5746 safe renegotiation) is unchanged. Resolves: #745 Signed-off-by: Nikos Mavrogiannopoulos --- NEWS | 3 + doc/requirements/protocol/unified.md | 37 ++++++++ doc/sample.config | 5 +- src/worker-vpn.c | 122 ++++++++++++++++++++++++++- tests/data/rekey-tls12.config | 54 ++++++++++++ tests/data/rekey-tls13.config | 54 ++++++++++++ tests/meson.build | 1 + tests/test-rekey-tls12 | 103 ++++++++++++++++++++++ tests/test-rekey-tls13 | 104 +++++++++++++++++++++++ 9 files changed, 481 insertions(+), 2 deletions(-) create mode 100644 tests/data/rekey-tls12.config create mode 100644 tests/data/rekey-tls13.config create mode 100755 tests/test-rekey-tls12 create mode 100755 tests/test-rekey-tls13 diff --git a/NEWS b/NEWS index fb1a0788..ad99f26c 100644 --- a/NEWS +++ b/NEWS @@ -9,6 +9,9 @@ if the certificate provides a single group (#692) - Simplified RADIUS Acct-Session-Time calculation to fix inflated time reporting for sessions that reconnected with the same cookie. +- On TLS 1.3 CSTP sessions, rekey-method = ssl now performs a transparent + TLS 1.3 KeyUpdate server-side instead of silently falling back to a full + tunnel rebuild on every rekey (#745) * Version 1.5.0 (released 2026-06-07) diff --git a/doc/requirements/protocol/unified.md b/doc/requirements/protocol/unified.md index 46b9d513..a10e0d65 100644 --- a/doc/requirements/protocol/unified.md +++ b/doc/requirements/protocol/unified.md @@ -613,6 +613,43 @@ response advertises `X-CSTP-Rekey-Method: ssl` and `X-CSTP-Rekey-Time: `; a rekey is triggered at approximately `rekey_time` (with jitter — `FUZZ(WSRCONFIG(ws)->rekey_time, 30, rnd)` at `src/worker-vpn.c:2111`). + +*TLS ≤ 1.2*: rekey is an in-place rehandshake, gated on RFC 5746 safe +renegotiation (`gnutls_safe_renegotiation_status()`, +`src/worker-vpn.c:2415-2419`), falling back to `new-tunnel` only for peers +that lack it. + +*TLS 1.3+*: TLS 1.3 has no renegotiation, so the server performs the whole +rekey itself, with no client involvement: it advertises +`X-CSTP-Rekey-Method: none` (not `ssl`) and, from `periodic_check()` at +approximately `rekey_time`, unilaterally issues a TLS 1.3 `KeyUpdate` +(`gnutls_session_key_update(session, GNUTLS_KU_PEER)`, GnuTLS ≥ 3.6.3, in +`cstp_transparent_rekey_update()`, gated by +`cstp_transparent_rekey_capable()`). There is no tunnel/TUN-device rebuild +and no client-visible interruption — any TLS 1.3 peer accepts an +unsolicited KeyUpdate transparently at the record layer (RFC 8446 §4.6.3), +regardless of the CSTP header value. + +`none` is required rather than `ssl` on TLS 1.3 because both of +OpenConnect's TLS backends mishandle a client-driven rehandshake on an +already-established TLS 1.3 session (GnuTLS backend: session torn down with +an "illegal parameter" alert; OpenSSL backend: forced `new-tunnel` +reconnect) — the exact disruption this requirement exists to avoid. Full +analysis in the comment above `cstp_transparent_rekey_capable()` in +`src/worker-vpn.c`. + +The rekey is atomic: `gnutls_session_key_update()` is retried on +`GNUTLS_E_AGAIN`/`GNUTLS_E_INTERRUPTED` for up to 30 seconds; if it has not +succeeded by then, or fails for any other reason, the worker ends the +session (`exit_worker_reason(ws, REASON_ERROR)`) rather than leave a rekey +partially pending. + +Tested by `tests/test-rekey-tls13` (TLS 1.3: one tun-device assignment for +the session, i.e. no rebuild; `TLS 1.3 session keys refreshed` logged; +tunnel pings succeed before and after the rekey window) and +`tests/test-rekey-tls12` (TLS 1.2 negative case: legacy rehandshake +completes, no TLS 1.3 KeyUpdate logged). Both connect with `--no-dtls` so +the assertions exercise CSTP/TLS, not DTLS. **Divergence**: both `ssl` and `new-tunnel` are implemented (not just advertised), so this is MAJORITY rather than EXTENSION; classified MAJORITY (not UNIVERSAL) only because the *value* `rekey-time` and the *jitter* (`FUZZ`, diff --git a/doc/sample.config b/doc/sample.config index aa23c8ea..9ba7f7e8 100644 --- a/doc/sample.config +++ b/doc/sample.config @@ -490,7 +490,10 @@ rekey-time = 172800 # ReKey method # Valid options: ssl, new-tunnel # ssl: Will perform an efficient rehandshake on the channel allowing -# a seamless connection during rekey. +# a seamless connection during rekey. On TLS 1.2 and earlier this is +# an in-place rehandshake; on TLS 1.3 (which has no renegotiation) +# it is performed transparently via a standard TLS 1.3 KeyUpdate, +# with no tunnel rebuild or client-visible interruption either way. # new-tunnel: Will instruct the client to discard and re-establish the channel. # Use this option only if the connecting clients have issues with the ssl # option. diff --git a/src/worker-vpn.c b/src/worker-vpn.c index 026f0384..71b7a6bb 100644 --- a/src/worker-vpn.c +++ b/src/worker-vpn.c @@ -41,6 +41,7 @@ static inline void worker_exit(int status) #include #include #include +#include #include #include #include @@ -1351,6 +1352,112 @@ int get_pmtu_approx(worker_st *ws) #endif } +/* TLS 1.3 has no renegotiation. A "ssl" rekey on such a CSTP session is + * instead performed *only* by the server, unilaterally, via the standard + * TLS 1.3 KeyUpdate message (gnutls_session_key_update(), triggered from + * periodic_check() via cstp_transparent_rekey_update()) -- the client is + * told "none" (see connect_handler()) so it never tries to act on its own + * schedule. We cannot simply advertise "ssl" and let clients rekey + * themselves, because we do not control client implementations and they + * disagree on what a repeated rehandshake call means on an established + * TLS 1.3 session: + * - OpenConnect's GnuTLS backend calls gnutls_handshake() again, which + * on the client side is documented to be equivalent to + * gnutls_session_key_update(session, GNUTLS_KU_PEER) -- i.e. a real, + * peer-requesting KeyUpdate. Having both ends independently send one + * around the same time was observed (in testing) to produce a GnuTLS + * "illegal parameter" alert and tear down the session. + * - OpenConnect's OpenSSL backend (openssl.c, still a supported + * "--with-openssl" build) has cstp_handshake() unconditionally return + * -EOPNOTSUPP; per cstp.c that failure makes the client immediately + * fall back to a full reconnect ("new-tunnel") on every rekey -- + * exactly the disruption this whole feature exists to avoid. Trusting + * the client universally would silently keep that bug for any + * OpenSSL-backed client (which includes real-world AnyConnect- + * compatible builds). + * Telling the client "none" stops either backend from acting on its own + * for this session. Per RFC 8446 4.6.3 every TLS 1.3-conformant peer + * MUST still transparently accept an unsolicited KeyUpdate at any time + * at the record layer, regardless of what it was told at the CSTP layer, + * so a server-only, unilateral KeyUpdate correctly rekeys the channel + * for any client, independently of which TLS library or CSTP-level + * rekey support it has. */ +#if GNUTLS_VERSION_NUMBER >= 0x030603 +static bool cstp_transparent_rekey_capable(gnutls_session_t session) +{ + if (session == NULL) + return false; + + if (gnutls_protocol_get_version(session) >= GNUTLS_TLS1_3) + return true; + else + return false; +} + +static void cstp_transparent_rekey_update(worker_st *ws, time_t now) +{ + int counter = 300; /* allow 30 seconds for the KeyUpdate to be sent */ + int ret; + + do { + ret = gnutls_session_key_update(ws->session, GNUTLS_KU_PEER); + if (ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) + ms_sleep(100); + } while ((ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) && + --counter > 0); + + /* Atomic: the rekey either completes within the timeout above or the + * session is torn down. There is no partial state where a stalled + * KeyUpdate is left pending for a later retry. */ + if (ret < 0) { + if (gnutls_error_is_fatal(ret) != 0) + GNUTLS_ALERT_PRINT(ws, ws->session, ret); + oclog(ws, LOG_ERR, + "error updating TLS session keys: %s; closing session", + gnutls_strerror(ret)); + exit_worker_reason(ws, REASON_ERROR); + return; + } + + oclog(ws, LOG_DEBUG, "TLS 1.3 session keys refreshed (rekey)"); + ws->last_tls_rehandshake = now; +} +#else +static bool cstp_transparent_rekey_capable(gnutls_session_t session) +{ + return false; +} + +static void cstp_transparent_rekey_update(worker_st *ws, time_t now) +{ +} +#endif + +/* Whether ws is configured for a transparent, server-only rekey (see the + * comment above cstp_transparent_rekey_capable()). */ +static bool cstp_transparent_rekey_configured(worker_st *ws) +{ + if (WSRCONFIG(ws)->rekey_method != REKEY_METHOD_SSL) + return false; + + return cstp_transparent_rekey_capable(ws->session); +} + +/* Whether a transparent rekey is both configured and due right now. */ +static bool cstp_transparent_rekey_due(worker_st *ws, time_t now) +{ + if (WSRCONFIG(ws)->rekey_time <= 0) + return false; + + if (!cstp_transparent_rekey_configured(ws)) + return false; + + if (now - ws->last_tls_rehandshake >= WSRCONFIG(ws)->rekey_time) + return true; + else + return false; +} + static int periodic_check(worker_st *ws, struct timespec *tnow, unsigned int dpd) { @@ -1396,6 +1503,11 @@ static int periodic_check(worker_st *ws, struct timespec *tnow, send_stats_to_secmod(ws, now, 0); } + /* the client was told "none" for this case (connect_handler()) and + * never initiates its own rekey; only the server does, here. */ + if (cstp_transparent_rekey_due(ws, now)) + cstp_transparent_rekey_update(ws, now); + #if defined(CAPTURE_LATENCY_SUPPORT) if (now - ws->latency.last_stats_msg >= LATENCY_WORKER_AGGREGATION_TIME) { @@ -2376,7 +2488,15 @@ static int connect_handler(worker_st *ws) SEND_ERR(ret); } - if (WSRCONFIG(ws)->rekey_time > 0) { + /* TLS 1.3 has no renegotiation; instead of falling back to + * new-tunnel, the server performs the "ssl" rekey itself via a + * transparent TLS 1.3 KeyUpdate (periodic_check()). */ + if (WSRCONFIG(ws)->rekey_time > 0 && + cstp_transparent_rekey_configured(ws)) { + ret = cstp_puts(ws, "X-CSTP-Rekey-Method: none\r\n"); + SEND_ERR(ret); + ws->last_tls_rehandshake = now; + } else if (WSRCONFIG(ws)->rekey_time > 0) { unsigned int method; ret = cstp_printf(ws, "X-CSTP-Rekey-Time: %u\r\n", diff --git a/tests/data/rekey-tls12.config b/tests/data/rekey-tls12.config new file mode 100644 index 00000000..afdb24fc --- /dev/null +++ b/tests/data/rekey-tls12.config @@ -0,0 +1,54 @@ +auth = "plain[@SRCDIR@/data/test1.passwd]" + +# Forced off (not @ISOLATE_WORKERS@): this test asserts on worker-emitted +# log lines, which only reliably reach the captured server output when +# worker isolation is off. +isolate-workers = false + +occtl-socket-file = @OCCTL_SOCKET@ +use-occtl = true + +use-dbus = no + +max-clients = 16 +max-same-clients = 2 + +tcp-port = @PORT@ +udp-port = @PORT@ + +keepalive = 32400 +dpd = 440 +try-mtu-discovery = false + +server-cert = @SRCDIR@/certs/server-cert.pem +server-key = @SRCDIR@/certs/server-key.pem + +# Force TLS 1.2 on the control channel, so the "ssl" rekey-method must +# keep using the pre-existing in-place TLS <= 1.2 rehandshake path, not +# the transparent TLS 1.3 KeyUpdate path. +tls-priorities = "NORMAL:-VERS-ALL:+VERS-TLS1.2:%SERVER_PRECEDENCE:%COMPAT" + +auth-timeout = 40 +cookie-timeout = 30 + +use-utmp = true +pid-file = /var/run/ocserv.pid +socket-file = /var/run/ocserv-socket + +run-as-user = nobody +run-as-group = daemon + +device = vpns +default-domain = example.com + +ipv4-network = @VPNNET@ + +ping-leases = false + +sec-mod-scale = 6 + +idle-timeout = 0 +mobile-idle-timeout = 0 + +rekey-time = 20 +rekey-method = ssl diff --git a/tests/data/rekey-tls13.config b/tests/data/rekey-tls13.config new file mode 100644 index 00000000..56970e01 --- /dev/null +++ b/tests/data/rekey-tls13.config @@ -0,0 +1,54 @@ +auth = "plain[@SRCDIR@/data/test1.passwd]" + +# Forced off (not @ISOLATE_WORKERS@): this test asserts on worker-emitted +# log lines, which only reliably reach the captured server output when +# worker isolation is off. +isolate-workers = false + +occtl-socket-file = @OCCTL_SOCKET@ +use-occtl = true + +use-dbus = no + +max-clients = 16 +max-same-clients = 2 + +tcp-port = @PORT@ +udp-port = @PORT@ + +keepalive = 32400 +dpd = 440 +try-mtu-discovery = false + +server-cert = @SRCDIR@/certs/server-cert.pem +server-key = @SRCDIR@/certs/server-key.pem + +# Force TLS 1.3 on the control channel, so the "ssl" rekey-method must +# use the transparent TLS 1.3 KeyUpdate path rather than an in-place +# TLS <= 1.2 rehandshake. +tls-priorities = "NORMAL:-VERS-ALL:+VERS-TLS1.3:%SERVER_PRECEDENCE:%COMPAT" + +auth-timeout = 40 +cookie-timeout = 30 + +use-utmp = true +pid-file = /var/run/ocserv.pid +socket-file = /var/run/ocserv-socket + +run-as-user = nobody +run-as-group = daemon + +device = vpns +default-domain = example.com + +ipv4-network = @VPNNET@ + +ping-leases = false + +sec-mod-scale = 6 + +idle-timeout = 0 +mobile-idle-timeout = 0 + +rekey-time = 20 +rekey-method = ssl diff --git a/tests/meson.build b/tests/meson.build index b1f81f35..aefde82d 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -298,6 +298,7 @@ if get_option('root-tests') 'test-occtl', 'test-occtl-commands', 'no-ipv6-ocv3', 'test-config-per-group', 'test-config-per-group-url-pass', 'test-config-per-group-url-cert', 'test-multiple-client-ip', + 'test-rekey-tls13', 'test-rekey-tls12', ] foreach s : root_scripts diff --git a/tests/test-rekey-tls12 b/tests/test-rekey-tls12 new file mode 100755 index 00000000..28ee261b --- /dev/null +++ b/tests/test-rekey-tls12 @@ -0,0 +1,103 @@ +#!/bin/bash +# +# Copyright (C) 2026 Nikos Mavrogiannopoulos +# +# This file is part of ocserv. +# +# ocserv is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# ocserv is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GnuTLS; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Companion negative case for test-rekey-tls13: with rekey-method = ssl +# and a CSTP session negotiated at TLS 1.2, the transparent TLS 1.3 +# KeyUpdate path introduced for REQ-PROTO-CTRL-001 must not engage -- +# the legacy client-driven in-place rehandshake keeps working unchanged, +# and no TLS 1.3 key update is ever logged. + +OCCTL="${OCCTL:-../src/occtl/occtl}" +SERV="${SERV:-../src/ocserv}" +srcdir=${srcdir:-.} +OCCTL_SOCKET=./occtl-rekey-tls12-$$.socket +PIDFILE=ocserv-pid.$$.tmp +CPIDFILE=openpid.$$.tmp +OUTFILE=rekey-tls12.$$.tmp + +function finish { + echo " * Cleaning up..." + CLIPID=${CPIDFILE} cleanup_client_server + test -n "${OUTFILE}" && rm -f ${OUTFILE} >/dev/null 2>&1 +} +trap finish EXIT + +. `dirname $0`/random-net.sh +. `dirname $0`/common.sh +. `dirname $0`/ns.sh + +eval "${GETPORT}" + +update_config rekey-tls12.config +${CMDNS2} ${SERV} -p ${PIDFILE} -f -c ${CONFIG} -d 3 >${OUTFILE} 2>&1 & PID=$! + +sleep 5 + +echo "Testing TLS 1.2 rekey is unaffected (rekey-method = ssl, TLS 1.2 forced)... " +echo "Connecting to obtain cookie... " +eval `echo "test" | ${CMDNS1} ${OPENCONNECT} --passwd-on-stdin -q ${ADDRESS}:${PORT} -u test --authenticate --servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8=` + +if [ -z "$COOKIE" ]; then + cat ${OUTFILE} + fail $PID "Could not obtain cookie" +fi + +sleep 1 +echo "Connecting with cookie... " +${CMDNS1} ${OPENCONNECT} ${ADDRESS}:${PORT} -u test -C "$COOKIE" --servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8= --no-dtls --background --pid-file "${CPIDFILE}" + +sleep 4 + +if [ ! -f "${CPIDFILE}" ]; then + cat ${OUTFILE} + fail $PID "It was not possible to establish session!" +fi + +set -e +${CMDNS1} ping -c 3 ${VPNADDR} +set +e + +echo "Waiting across two rekey-time (20s) intervals... " +sleep 50 + +echo "Confirming the tunnel is still usable after the rekey window... " +set -e +${CMDNS1} ping -c 3 ${VPNADDR} +set +e + +tun_count=$(grep -c "assigning tun device" ${OUTFILE}) +if [ "${tun_count}" != "1" ]; then + cat ${OUTFILE} + fail $PID "FAIL: expected exactly 1 tun device assignment (no tunnel rebuild), got ${tun_count}" +fi + +if grep -q "TLS 1.3 session keys refreshed" ${OUTFILE}; then + cat ${OUTFILE} + fail $PID "FAIL: did not expect a TLS 1.3 key update on a TLS 1.2 session" +fi + +if ! grep -q "TLS rehandshake completed" ${OUTFILE}; then + cat ${OUTFILE} + fail $PID "FAIL: expected the legacy TLS rehandshake to complete, found none" +fi + +echo "ok" + +exit 0 diff --git a/tests/test-rekey-tls13 b/tests/test-rekey-tls13 new file mode 100755 index 00000000..7451fac8 --- /dev/null +++ b/tests/test-rekey-tls13 @@ -0,0 +1,104 @@ +#!/bin/bash +# +# Copyright (C) 2026 Nikos Mavrogiannopoulos +# +# This file is part of ocserv. +# +# ocserv is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 2 of the License, or (at +# your option) any later version. +# +# ocserv is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GnuTLS; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Verifies REQ-PROTO-CTRL-001: with rekey-method = ssl and a CSTP session +# negotiated at TLS 1.3, the periodic rekey is performed transparently via +# the standard TLS 1.3 KeyUpdate message (gnutls_session_key_update()), +# without any tunnel/TUN-device rebuild. See test-rekey-tls12 for the +# companion negative case (TLS 1.2, unaffected by this change). The two +# cases are kept in separate scripts, each with its own network namespace +# and single connect/disconnect cycle, rather than two VPN sessions in one +# script/namespace -- the latter was observed to occasionally leave the +# client namespace's routing in a state where the second session's TCP +# connect fails with ENETUNREACH, unrelated to the server-side behavior +# under test. + +OCCTL="${OCCTL:-../src/occtl/occtl}" +SERV="${SERV:-../src/ocserv}" +srcdir=${srcdir:-.} +OCCTL_SOCKET=./occtl-rekey-tls13-$$.socket +PIDFILE=ocserv-pid.$$.tmp +CPIDFILE=openpid.$$.tmp +OUTFILE=rekey-tls13.$$.tmp + +function finish { + echo " * Cleaning up..." + CLIPID=${CPIDFILE} cleanup_client_server + test -n "${OUTFILE}" && rm -f ${OUTFILE} >/dev/null 2>&1 +} +trap finish EXIT + +. `dirname $0`/random-net.sh +. `dirname $0`/common.sh +. `dirname $0`/ns.sh + +eval "${GETPORT}" + +update_config rekey-tls13.config +${CMDNS2} ${SERV} -p ${PIDFILE} -f -c ${CONFIG} -d 3 >${OUTFILE} 2>&1 & PID=$! + +sleep 5 + +echo "Testing transparent TLS 1.3 rekey (rekey-method = ssl, TLS 1.3 forced)... " +echo "Connecting to obtain cookie... " +eval `echo "test" | ${CMDNS1} ${OPENCONNECT} --passwd-on-stdin -q ${ADDRESS}:${PORT} -u test --authenticate --servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8=` + +if [ -z "$COOKIE" ]; then + cat ${OUTFILE} + fail $PID "Could not obtain cookie" +fi + +sleep 1 +echo "Connecting with cookie... " +${CMDNS1} ${OPENCONNECT} ${ADDRESS}:${PORT} -u test -C "$COOKIE" --servercert=pin-sha256:xp3scfzy3rOQsv9NcOve/8YVVv+pHr4qNCXEXrNl5s8= --no-dtls --background --pid-file "${CPIDFILE}" + +sleep 4 + +if [ ! -f "${CPIDFILE}" ]; then + cat ${OUTFILE} + fail $PID "It was not possible to establish session!" +fi + +set -e +${CMDNS1} ping -c 3 ${VPNADDR} +set +e + +echo "Waiting across two rekey-time (20s) intervals... " +sleep 50 + +echo "Confirming the tunnel is still usable after the rekey window... " +set -e +${CMDNS1} ping -c 3 ${VPNADDR} +set +e + +tun_count=$(grep -c "assigning tun device" ${OUTFILE}) +if [ "${tun_count}" != "1" ]; then + cat ${OUTFILE} + fail $PID "FAIL: expected exactly 1 tun device assignment (no tunnel rebuild), got ${tun_count}" +fi + +if ! grep -q "TLS 1.3 session keys refreshed" ${OUTFILE}; then + cat ${OUTFILE} + fail $PID "FAIL: expected a transparent TLS 1.3 key update to be logged, found none" +fi + +echo "ok" + +exit 0