1
0
mirror of https://github.com/wolfSSL/wolfssl.git synced 2026-10-24 04:09:07 +03:00
Files
wolfssl/scripts/multi-msg-record.py
Tobias Frauenschläger cc49fc0ec9 Make multi-msg-record independent of the peer's session ticket support (#11551)
* Stop multi-msg-record depending on the peer's session ticket support

scripts/multi-msg-record.test fails all eleven of its TLS 1.2 cases on any
machine running tlslite-ng 0.8.0-beta1, whenever wolfSSL is built with
session tickets. The wolfSSL client aborts the handshake with
SESSION_TICKET_EXPECT_E (-390).

That beta echoes an empty session_ticket extension in its ServerHello and
sets send_session_ticket unconditionally, then sends no NewSessionTicket
because its default HandshakeSettings carry no ticket keys. Rejecting that
is correct behaviour on our side, per RFC 5077. Upstream fixed the server
in 0.8.0-beta2 by gating the echo on ticket_count and ticketKeys, so only
that one release is affected.

HAVE_SESSION_TICKET alone decides it. A PRB-dtls run on the affected node
makes that plain: three configurations, one commit, one box, and only the
third, --enable-dtls --enable-dtls13 --enable-dtls-mtu
--enable-session-ticket, failed. It is not specific to --enable-all, which
merely happens to turn the feature on.

Hide the extension from the tlslite server for the duration of the TLS 1.2
case, using the monkey patch the test already installs. Session tickets are
not what this test covers, and releases from 0.8.0-beta2 on never echo the
extension here anyway, so this changes nothing on a current tlslite-ng.

Also report the wolfSSL client's return code and stderr on failure. Both
runners preferred the server side traceback and dropped the client's output
entirely, so a failing CI log showed a bare TLSAbruptCloseError from the
socket layer and no sign of the -390 that caused it.

Verified on master and on the failing branch, x86_64 Linux and macOS arm64,
against tlslite-ng 0.8.0-beta1 and 0.8.2: 15 passed, 0 failed, 1 skipped in
every combination. Before the change, 0.8.0-beta1 gives 4 passed, 11 failed.

* Test that a promised but absent session ticket is rejected

RFC 5077 requires a server that puts the session_ticket extension in its
ServerHello to follow with a NewSessionTicket. wolfSSL answers a server
that skips it with SESSION_TICKET_EXPECT_E, but nothing tested that. The
behaviour only came to light because a tlslite-ng beta got it wrong, and a
refactor could relax it into accepting the ticketless handshake without any
test noticing.

Drive the case deliberately: configure the tlslite server with ticket keys
so it promises a ticket on every release, replace _serverSendTickets with
an empty generator so it sends none, and require the client to fail with
-390.

The case skips itself when the client offers no session_ticket extension,
which is how a build without HAVE_SESSION_TICKET reports.

Checked both ways round. Leaving _serverSendTickets intact makes the
handshake succeed and the case fail, so the assertion is specific to the
missing ticket and not to any handshake error. Hiding the extension from
the server, which is how a build without HAVE_SESSION_TICKET looks, makes
the case skip rather than fail.

* Pin tlslite-ng in the os-check workflow

scripts/multi-msg-record.test speaks TLS to a tlslite-ng server, so that
package decides what the 24 --enable-all entries in os-check-linux.json
and the macOS entry are tested against, and whether the test runs at all
rather than reporting SKIP. Installing it unpinned means any upstream
release lands across the whole matrix at once, on whichever unrelated PR
happens to run next.

Pin it to 0.8.2, matching how advisory.yml and sbom.yml pin their Python
dependencies. No behaviour change today: 0.8.2 is what an unpinned install
already resolves to.
2026-09-28 14:03:15 -07:00

751 lines
27 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
#
# multi-msg-record.py
#
# Python half of scripts/multi-msg-record.test (the bash wrapper handles
# NETWORK_UNSHARE_HELPER / AM_BWRAPPED and the python3 availability
# check, then execs this script).
#
# Tests that wolfSSL correctly processes TLS records containing multiple
# handshake messages packed into a single record.
#
# Uses tlslite-ng as the TLS peer to craft multi-message records:
#
# TLS 1.2 – Each connection tests TWO code paths back-to-back:
# 1. Initial handshake: RecordMergingSocket rewrites separate
# plaintext ServerHello + Certificate + ServerKeyExchange +
# ServerHelloDone records into one multi-message TLS
# record before forwarding to the wolfSSL client.
# 2. Renegotiation on the same connection: tlslite-ng is
# monkey-patched to coalesce SH+Cert+SKE+SHD into ONE
# encrypted handshake record (exercises the
# curSize -= padSz CBC-padding path and the AEAD path).
#
# TLS 1.3 – tlslite-ng's _queue_message / _queue_flush mechanism already
# coalesces EncryptedExtensions + Certificate + CertificateVerify
# + Finished into a single encrypted record. The test verifies
# that wolfSSL parses this correctly.
#
# Multiple cipher suites are tested for both protocol versions.
#
# Requirements: python3, tlslite-ng (pip install tlslite-ng)
import socket
import struct
import subprocess
import os
import sys
import threading
import time
import types
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
def _find_wolfssl_dir():
# Under `make check` the working directory is the build tree, which is
# where the client binary lives and which differs from the tree
# containing this script in an out-of-tree (VPATH) build. Fall back to
# the script's parent for direct invocation from the source tree.
for root in (os.getcwd(), os.path.dirname(SCRIPT_DIR)):
if os.path.isfile(os.path.join(root, "examples", "client", "client")):
return root
return os.path.dirname(SCRIPT_DIR)
WOLFSSL_DIR = _find_wolfssl_dir()
WOLF_CLIENT = os.path.join(WOLFSSL_DIR, "examples", "client", "client")
CERT_DIR = os.path.join(WOLFSSL_DIR, "certs")
# CA cert path passed to the wolfSSL client via -A. Set in main() after
# detect_wolf_features() determines whether the build accepts PEM or DER.
WOLF_CA_CERT = os.path.join(CERT_DIR, "ca-cert.pem")
# ---------------------------------------------------------------------------
# Bypass a strict tlslite-ng validation that rejects wolfSSL's ClientHello
# when the client advertises FFDHE groups in a TLS-1.3-only hello.
# This must happen before importing TLSConnection.
#
# If tlslite-ng isn't installed we exit 77 so automake marks the test
# SKIPped instead of FAILed.
# ---------------------------------------------------------------------------
try:
import tlslite.tlsconnection # noqa: E402
import tlslite.recordlayer # noqa: E402
tlslite.tlsconnection.TLS_1_3_FORBIDDEN_GROUPS = frozenset()
from tlslite import ( # noqa: E402
TLSConnection, HandshakeSettings, X509CertChain, parsePEMKey,
)
from tlslite.constants import ContentType # noqa: E402
from tlslite.extensions import RenegotiationInfoExtension # noqa: E402
from tlslite.constants import ExtensionType # noqa: E402
from tlslite.messages import HelloMessage, Message as TLSMessage # noqa: E402
except ImportError as e:
sys.stdout.write(
"tlslite-ng not installed ({}); skipping multi-msg-record test\n"
" (install with: pip install tlslite-ng)\n".format(e))
sys.exit(77)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
HS_NAMES = {
2: "SH", 4: "NST", 8: "EE", 11: "Cert", 12: "SKE",
13: "CR", 14: "SHD", 15: "CV", 16: "CKE", 20: "Fin",
}
PASS_COUNT = 0
FAIL_COUNT = 0
SKIP_COUNT = 0
def passed(label):
global PASS_COUNT
PASS_COUNT += 1
print(f" PASS: {label}")
def failed(label):
global FAIL_COUNT
FAIL_COUNT += 1
print(f" FAIL: {label}")
def skipped(label):
global SKIP_COUNT
SKIP_COUNT += 1
print(f" SKIP: {label}")
def detect_wolf_features():
"""Probe the wolfSSL client binary to find which features are
compiled in. Used to decide which test phases to run.
Returns dict with keys: tls12 (bool), tls13 (bool),
secure_reneg (bool), rsa (bool), ciphers (set[str]), ca_cert (str).
"""
feats = {"tls12": False, "tls13": False, "secure_reneg": False,
"rsa": True,
"ciphers": set(),
"ca_cert": os.path.join(CERT_DIR, "ca-cert.pem")}
# ./client -V -> e.g. "3:4:d(downgrade):e(either):"
try:
r = subprocess.run([WOLF_CLIENT, "-V"],
capture_output=True, timeout=5)
parts = r.stdout.decode("utf-8", errors="replace").strip().split(":")
feats["tls12"] = "3" in parts
feats["tls13"] = "4" in parts
except Exception:
pass
# ./client -? -> help text includes "-R" only when
# HAVE_SECURE_RENEGOTIATION is defined. The default -A path
# ("ca-cert.pem" vs "ca-cert.der") also tells us which CA file
# format the build can load. The RSA key-size line reports
# "RSA not supported" when NO_RSA is defined.
try:
r = subprocess.run([WOLF_CLIENT, "-?"],
capture_output=True, timeout=5)
htxt = r.stdout.decode("utf-8", errors="replace")
feats["secure_reneg"] = ("Allow Secure Renegotiation" in htxt)
if "ca-cert.der" in htxt and "ca-cert.pem" not in htxt:
feats["ca_cert"] = os.path.join(CERT_DIR, "ca-cert.der")
if "RSA not supported" in htxt:
feats["rsa"] = False
except Exception:
pass
# ./client -e -> colon-separated list of supported cipher suites.
try:
r = subprocess.run([WOLF_CLIENT, "-e"],
capture_output=True, timeout=5)
ctxt = r.stdout.decode("utf-8", errors="replace").strip()
feats["ciphers"] = {c for c in ctxt.split(":") if c}
except Exception:
pass
return feats
def _load_chain(cert_file):
with open(cert_file) as f:
chain = X509CertChain()
chain.parsePemList(f.read())
return chain
def _load_key(key_file):
with open(key_file) as f:
return parsePEMKey(f.read(), private=True)
def _parse_hs_types(data):
"""Parse handshake message types from raw handshake content."""
msgs = []
off = 0
while off + 4 <= len(data):
ht = data[off]
hl = struct.unpack("!I", b"\x00" + bytes(data[off + 1 : off + 4]))[0]
msgs.append(HS_NAMES.get(ht, f"T{ht}"))
off += 4 + hl
return msgs
def _listen_socket():
"""Bind a listening TCP socket on localhost with the standard test timeout.
The port is read back from the socket that keeps it, rather than from a
throwaway one that is closed first: closing it leaves a window in which
anything else on the machine can take the port before the real bind.
"""
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("127.0.0.1", 0))
srv.listen(1)
srv.settimeout(15)
return srv, srv.getsockname()[1]
def _run_wolf_client(port, version, cipher, extra=()):
"""Invoke the wolfSSL example client against 127.0.0.1:port.
WOLF_CA_CERT is PEM or DER depending on the build (NO_CODING /
OPENSSL_EXTRA builds don't both support PEM).
"""
cmd = [WOLF_CLIENT, "-h", "127.0.0.1", "-p", str(port),
"-v", version, "-A", WOLF_CA_CERT,
"-g", *extra]
if cipher:
cmd.extend(["-l", cipher])
return subprocess.run(cmd, capture_output=True, timeout=15)
def _describe_failure(proc, result):
"""Render the client's and the server's side of a failed connection."""
client = proc.stderr.decode("utf-8", errors="replace").strip()
server = result["error"].strip()
parts = ["client rc={}".format(proc.returncode)]
if client:
parts.append("client: " + client[-400:])
if server:
parts.append("server: " + server[-400:])
return "; ".join(parts)
class _SendRecordTrace:
"""Context manager that wraps RecordLayer.sendRecord to log every record."""
def __init__(self):
self.log = []
self._orig = None
def __enter__(self):
self._orig = tlslite.recordlayer.RecordLayer.sendRecord
log = self.log
orig = self._orig
def wrapper(self_rl, msg):
data = msg.write()
ct = msg.contentType
encrypted = bool(self_rl._writeState
and self_rl._writeState.encContext)
hs_msgs = []
if ct == ContentType.handshake:
hs_msgs = _parse_hs_types(data)
log.append((ct, encrypted, len(data), hs_msgs))
yield from orig(self_rl, msg)
tlslite.recordlayer.RecordLayer.sendRecord = wrapper
return self.log
def __exit__(self, *exc):
tlslite.recordlayer.RecordLayer.sendRecord = self._orig
# ---------------------------------------------------------------------------
# RecordMergingSocket (TLS 1.2 plaintext record merging)
# ---------------------------------------------------------------------------
class RecordMergingSocket:
"""Socket wrapper that rewrites consecutive TLS handshake records into
a single multi-message record. Only merges plaintext records that
precede ChangeCipherSpec."""
def __init__(self, sock):
self._sock = sock
self._pending = bytearray()
self._ver = 0x0303
self._after_ccs = False
self.merged_msgs = [] # [(n_msgs, [names], size)]
def _flush(self):
if not self._pending:
return
msgs = _parse_hs_types(self._pending)
n = len(msgs)
hdr = struct.pack("!BHH", 22, self._ver, len(self._pending))
self._sock.sendall(hdr + bytes(self._pending))
self.merged_msgs.append((n, msgs, len(self._pending)))
self._pending = bytearray()
# Called by BufferedSocket (one record per call, or multiple from flush)
def _process(self, data):
data = bytearray(data)
off = 0
while off + 5 <= len(data):
ct = data[off]
ver = struct.unpack("!H", data[off + 1 : off + 3])[0]
rlen = struct.unpack("!H", data[off + 3 : off + 5])[0]
if off + 5 + rlen > len(data):
break
payload = data[off + 5 : off + 5 + rlen]
if not self._after_ccs and ct == 22:
self._pending.extend(payload)
self._ver = ver
else:
if ct == 20:
self._after_ccs = True
self._flush()
self._sock.sendall(bytes(data[off : off + 5 + rlen]))
off += 5 + rlen
def send(self, data):
self._process(data)
return len(data)
def sendall(self, data):
self._process(data)
def recv(self, bufsize):
self._flush()
return self._sock.recv(bufsize)
def __getattr__(self, name):
return getattr(self._sock, name)
# ---------------------------------------------------------------------------
# Test runners
# ---------------------------------------------------------------------------
def run_tls12_test(cipher_wolf, cert_chain, priv_key, label,
do_reneg=True):
"""TLS 1.2 test – one connection optionally exercises two code paths:
Phase 1 (plaintext grouping, initial handshake):
RecordMergingSocket rewrites separate plaintext ServerHello,
Certificate, ServerKeyExchange and ServerHelloDone records into
one multi-message TLS record before delivery to wolfSSL.
Phase 2 (encrypted grouping, renegotiation on same connection):
tlslite-ng server is monkey-patched to coalesce SH+Cert+SKE+SHD
into a single encrypted handshake record inside the renegotiation
(exercises wolfSSL's encrypted multi-message parsing including
curSize -= padSz for CBC padding).
Phase 2 is skipped when do_reneg=False (e.g. the wolfSSL client was
built without HAVE_SECURE_RENEGOTIATION).
"""
srv, port = _listen_socket()
result = {"ok": False, "error": ""}
msock_ref = [None]
trace_log = []
reneg_active = [False]
verify_data = {'client': None, 'server': None}
# --- monkey-patches (used only during this connection) ----------------
orig_calc_key = tlslite.tlsconnection.calc_key
def capturing_calc_key(*args, **kwargs):
res = orig_calc_key(*args, **kwargs)
lbl = args[3] if len(args) > 3 else kwargs.get('label', b'')
if lbl == b"client finished" and verify_data['client'] is None:
verify_data['client'] = bytearray(res)
elif lbl == b"server finished" and verify_data['server'] is None:
verify_data['server'] = bytearray(res)
return res
orig_getExt = HelloMessage.getExtension
def patched_getExt(self, ext_type):
# tlslite-ng 0.8.0b1 promises a session ticket it never sends (-390).
if ext_type == ExtensionType.session_ticket:
return None
ext = orig_getExt(self, ext_type)
if (ext_type == ExtensionType.renegotiation_info
and ext is not None and reneg_active[0]):
ext._internal_value = bytearray(0)
return ext
orig_rie_create = RenegotiationInfoExtension.create
def patched_rie_create(self, data):
if reneg_active[0] and data == bytearray(0):
combined = (bytearray(verify_data['client'])
+ bytearray(verify_data['server']))
return orig_rie_create(self, combined)
return orig_rie_create(self, data)
# ----------------------------------------------------------------------
def server():
try:
tlslite.tlsconnection.calc_key = capturing_calc_key
HelloMessage.getExtension = patched_getExt
RenegotiationInfoExtension.create = patched_rie_create
conn, _ = srv.accept()
conn.settimeout(15)
msock = RecordMergingSocket(conn)
msock_ref[0] = msock
tls = TLSConnection(msock)
settings = HandshakeSettings()
settings.minVersion = (3, 3)
settings.maxVersion = (3, 3)
# ---------- Phase 1: initial handshake (plaintext grouping) ----
tls.handshakeServer(certChain=cert_chain, privateKey=priv_key,
settings=settings)
tlslite.tlsconnection.calc_key = orig_calc_key
data = tls.recv(4096)
if do_reneg:
# ---------- Phase 2: trigger + run renegotiation ----------
hr = TLSMessage(ContentType.handshake,
bytearray([0, 0, 0, 0]))
for _ in tls._sendMsg(hr, randomizeFirstBlock=False,
update_hashes=False):
pass
# Bypass tlslite-ng renegotiation guards
tls.closed = True
tls.session = None
reneg_active[0] = True
# Coalesce handshake messages into ONE encrypted TLS record
def coalescing_sendMsgs(self, msgs):
for msg in msgs:
self._queue_message(msg)
yield from self._queue_flush()
tls._sendMsgs = types.MethodType(coalescing_sendMsgs, tls)
with _SendRecordTrace() as log:
tls.handshakeServer(certChain=cert_chain,
privateKey=priv_key,
settings=settings)
reneg_active[0] = False
trace_log.extend(log)
if data:
tls.send(data)
tls.close()
result["ok"] = True
except Exception as e:
import traceback
result["error"] = traceback.format_exc()
finally:
tlslite.tlsconnection.calc_key = orig_calc_key
HelloMessage.getExtension = orig_getExt
RenegotiationInfoExtension.create = orig_rie_create
reneg_active[0] = False
srv.close()
st = threading.Thread(target=server, daemon=True)
st.start()
time.sleep(0.1)
proc = _run_wolf_client(port, "3", cipher_wolf,
extra=("-R",) if do_reneg else ())
st.join(timeout=5)
if proc.returncode != 0 or not result["ok"]:
err = _describe_failure(proc, result)
failed(f"{label}: connection failed ({err})")
return False
ok = True
# Phase 1 verification: plaintext multi-message record
msock = msock_ref[0]
has_pt_grouped = False
for n, msgs, sz in (msock.merged_msgs if msock else []):
if n > 1:
has_pt_grouped = True
passed(f"{label} [plaintext]: {n} msgs "
f"[{'+'.join(msgs)}] in one record ({sz} bytes)")
if not has_pt_grouped:
failed(f"{label} [plaintext]: no multi-message record detected")
ok = False
# Phase 2 verification: encrypted multi-message record (renego)
if do_reneg:
has_enc_grouped = False
for ct, enc, sz, msgs in trace_log:
if ct == ContentType.handshake and enc and len(msgs) > 1:
has_enc_grouped = True
passed(f"{label} [encrypted]: {len(msgs)} msgs "
f"[{'+'.join(msgs)}] in one record ({sz} bytes)")
if not has_enc_grouped:
failed(f"{label} [encrypted]: no multi-message "
f"encrypted record")
ok = False
return ok
def run_tls13_test(cipher_wolf, cert_chain, priv_key, label):
"""TLS 1.3: verify tlslite-ng sends multi-msg encrypted record and
wolfSSL client processes it."""
srv, port = _listen_socket()
result = {"ok": False, "error": ""}
def server():
try:
conn, _ = srv.accept()
conn.settimeout(15)
tls = TLSConnection(conn)
settings = HandshakeSettings()
settings.minVersion = (3, 4)
settings.maxVersion = (3, 4)
tls.handshakeServer(certChain=cert_chain, privateKey=priv_key,
settings=settings)
data = tls.recv(4096)
if data:
tls.send(data)
tls.close()
result["ok"] = True
except Exception as e:
result["error"] = str(e)
finally:
srv.close()
with _SendRecordTrace() as log:
st = threading.Thread(target=server, daemon=True)
st.start()
time.sleep(0.1)
proc = _run_wolf_client(port, "4", cipher_wolf)
st.join(timeout=5)
if proc.returncode != 0 or not result["ok"]:
err = _describe_failure(proc, result)
failed(f"{label}: handshake failed ({err})")
return False
# Check that at least one encrypted handshake record has multiple messages
has_multi = False
for ct, enc, sz, msgs in log:
if ct == ContentType.handshake and enc and len(msgs) > 1:
has_multi = True
passed(f"{label}: {len(msgs)} encrypted msgs "
f"[{'+'.join(msgs)}] in one record ({sz} bytes)")
if not has_multi:
failed(f"{label}: no multi-message encrypted records")
return False
return True
def run_tls12_ticket_expect_test(cert_chain, priv_key, label):
"""Expect SESSION_TICKET_EXPECT_E when a promised ticket never arrives."""
srv, port = _listen_socket()
result = {"ok": False, "error": ""}
offered = [None]
orig_getExt = HelloMessage.getExtension
orig_send_tickets = TLSConnection._serverSendTickets
def patched_getExt(self, ext_type):
ext = orig_getExt(self, ext_type)
if ext_type == ExtensionType.session_ticket and offered[0] is None:
offered[0] = ext is not None
return ext
def no_tickets(self, settings):
# The ServerHello still promises a ticket; this sends none.
return iter(())
def server():
try:
HelloMessage.getExtension = patched_getExt
TLSConnection._serverSendTickets = no_tickets
conn, _ = srv.accept()
conn.settimeout(15)
tls = TLSConnection(conn)
settings = HandshakeSettings()
settings.minVersion = (3, 3)
settings.maxVersion = (3, 3)
# Releases from 0.8.0-beta2 on only echo the extension when
# ticket keys are configured.
settings.ticketKeys = [bytearray(32)]
settings.ticket_count = 1
tls.handshakeServer(certChain=cert_chain, privateKey=priv_key,
settings=settings)
tls.close()
result["ok"] = True
except Exception:
import traceback
result["error"] = traceback.format_exc()
finally:
HelloMessage.getExtension = orig_getExt
TLSConnection._serverSendTickets = orig_send_tickets
srv.close()
st = threading.Thread(target=server, daemon=True)
st.start()
time.sleep(0.1)
proc = _run_wolf_client(port, "3", None)
st.join(timeout=5)
if offered[0] is False:
skipped(f"{label} (build has no HAVE_SESSION_TICKET)")
return True
err = proc.stderr.decode("utf-8", errors="replace")
if offered[0] and proc.returncode != 0 and "-390" in err:
passed(f"{label}: client rejected the missing NewSessionTicket")
return True
failed(f"{label}: expected -390, got {_describe_failure(proc, result)}")
return False
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
if not os.path.isfile(WOLF_CLIENT):
print(f"ERROR: wolfSSL client not found: {WOLF_CLIENT}")
print(" Build wolfSSL first (./configure && make)")
sys.exit(1)
# Probe the client to see which features are compiled in so each
# phase of the test is only run when it can succeed.
feats = detect_wolf_features()
global WOLF_CA_CERT
WOLF_CA_CERT = feats["ca_cert"]
print("=" * 60)
print(" Multi-Message TLS Record Test")
print("=" * 60)
print(f" wolfSSL features: TLS1.2={feats['tls12']} "
f"TLS1.3={feats['tls13']} "
f"secure_reneg={feats['secure_reneg']} "
f"rsa={feats['rsa']}")
# The test certs are RSA; skip the whole test when the wolfSSL build
# has no RSA support (the client can't load or verify them).
if not feats["rsa"]:
print("\n wolfSSL built without RSA; skipping multi-msg-record "
"test (RSA test certs cannot be verified).")
sys.exit(77)
# Load certificate / key pairs
rsa_chain = _load_chain(os.path.join(CERT_DIR, "server-cert.pem"))
rsa_key = _load_key(os.path.join(CERT_DIR, "server-key.pem"))
# ------------------------------------------------------------------
# TLS 1.2 – plaintext (initial HS) + optional encrypted (renegotiation)
# multi-message records, same connection per cipher suite.
# ------------------------------------------------------------------
tls12_suites = [
# (wolfSSL cipher name, description)
(None, "default negotiated"),
# AEAD (GCM)
("ECDHE-RSA-AES128-GCM-SHA256", "ECDHE-RSA AES128-GCM"),
("ECDHE-RSA-AES256-GCM-SHA384", "ECDHE-RSA AES256-GCM"),
("DHE-RSA-AES128-GCM-SHA256", "DHE-RSA AES128-GCM"),
("DHE-RSA-AES256-GCM-SHA384", "DHE-RSA AES256-GCM"),
# CBC + HMAC (exercises padding path)
("ECDHE-RSA-AES128-SHA256", "ECDHE-RSA AES128-CBC-SHA256"),
("ECDHE-RSA-AES256-SHA384", "ECDHE-RSA AES256-CBC-SHA384"),
("DHE-RSA-AES128-SHA256", "DHE-RSA AES128-CBC-SHA256"),
("DHE-RSA-AES256-SHA256", "DHE-RSA AES256-CBC-SHA256"),
# AEAD (ChaCha20-Poly1305)
("ECDHE-RSA-CHACHA20-POLY1305", "ECDHE-RSA CHACHA20-POLY1305"),
("DHE-RSA-CHACHA20-POLY1305", "DHE-RSA CHACHA20-POLY1305"),
]
if feats["tls12"]:
if feats["secure_reneg"]:
print("\n--- TLS 1.2: plaintext + encrypted multi-message "
"records ---")
print(" Each connection verifies BOTH code paths:")
print(" * initial handshake -> plaintext SH+Cert+SKE+SHD")
print(" * renegotiation -> encrypted SH+Cert+SKE+SHD")
else:
print("\n--- TLS 1.2: plaintext multi-message records ---")
print(" wolfSSL built without HAVE_SECURE_RENEGOTIATION;")
print(" skipping the encrypted (renegotiation) half.")
print(" Covers multiple key-exchanges, ciphers and MAC "
"families.\n")
for cipher, desc in tls12_suites:
if cipher and cipher not in feats["ciphers"]:
skipped(f"TLS1.2 {desc} - cipher not in wolfSSL build")
continue
run_tls12_test(cipher, rsa_chain, rsa_key,
f"TLS1.2 {desc}",
do_reneg=feats["secure_reneg"])
if not feats["secure_reneg"]:
skipped("TLS1.2 encrypted multi-msg record "
"(requires HAVE_SECURE_RENEGOTIATION)")
run_tls12_ticket_expect_test(
rsa_chain, rsa_key, "TLS1.2 promised session ticket never sent")
else:
skipped(f"TLS 1.2 tests ({len(tls12_suites)} suites) - "
"wolfSSL built without TLS 1.2")
# ------------------------------------------------------------------
# TLS 1.3 – encrypted multi-message records
# ------------------------------------------------------------------
tls13_suites = [
# (wolfSSL cipher name, description)
(None, "default negotiated"),
("TLS13-AES128-GCM-SHA256", "AES-128-GCM"),
("TLS13-AES256-GCM-SHA384", "AES-256-GCM"),
("TLS13-CHACHA20-POLY1305-SHA256", "CHACHA20-POLY1305"),
]
if feats["tls13"]:
print("\n--- TLS 1.3: encrypted multi-message records ---")
print(" Server sends EE+Cert+CV+Fin in a single encrypted "
"record;")
print(" wolfSSL client must decrypt and parse.\n")
for cipher, desc in tls13_suites:
if cipher and cipher not in feats["ciphers"]:
skipped(f"TLS1.3 {desc} - cipher not in wolfSSL build")
continue
run_tls13_test(cipher, rsa_chain, rsa_key,
f"TLS1.3 {desc}")
else:
skipped(f"TLS 1.3 tests ({len(tls13_suites)} suites) - "
"wolfSSL built without TLS 1.3")
# ------------------------------------------------------------------
# Summary
# ------------------------------------------------------------------
print()
print("=" * 60)
print(f" Results: {PASS_COUNT} passed, {FAIL_COUNT} failed, "
f"{SKIP_COUNT} skipped")
print("=" * 60)
# If nothing at all could run, signal SKIP (exit 77) so automake
# records the test as skipped rather than passed-with-nothing.
if PASS_COUNT == 0 and FAIL_COUNT == 0:
sys.exit(77)
return FAIL_COUNT == 0
if __name__ == "__main__":
sys.exit(0 if main() else 1)