diff --git a/.github/workflows/os-check.yml b/.github/workflows/os-check.yml index 39b4c65585..1738340cf0 100644 --- a/.github/workflows/os-check.yml +++ b/.github/workflows/os-check.yml @@ -137,6 +137,13 @@ jobs: # This should be a safe limit for the tests to run. timeout-minutes: 14 steps: + # tlslite-ng is consumed by scripts/multi-msg-record.test (run from + # `make check`); without it that test is SKIPped. + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install tlslite-ng + - name: Build and test wolfSSL uses: wolfSSL/actions-build-autotools-project@v1 with: @@ -181,6 +188,13 @@ jobs: # This should be a safe limit for the tests to run. timeout-minutes: 14 steps: + # tlslite-ng is consumed by scripts/multi-msg-record.test (run from + # `make check`); without it that test is SKIPped. + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install tlslite-ng + - name: Build and test wolfSSL uses: wolfSSL/actions-build-autotools-project@v1 with: @@ -207,6 +221,13 @@ jobs: # This should be a safe limit for the tests to run. timeout-minutes: 14 steps: + # tlslite-ng is consumed by scripts/multi-msg-record.test (run from + # `make check`); without it that test is SKIPped. + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install tlslite-ng + - name: Build and test wolfSSL uses: wolfSSL/actions-build-autotools-project@v1 with: @@ -269,6 +290,12 @@ jobs: timeout-minutes: 14 steps: - uses: actions/checkout@v4 + # tlslite-ng is consumed by scripts/multi-msg-record.test (run from + # `make check`); without it that test is SKIPped. + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - run: pip install tlslite-ng - run: ./autogen.sh - name: user_settings_all.h with compatibility layer run: | diff --git a/.gitignore b/.gitignore index 7ea896eb2c..93388d03bf 100644 --- a/.gitignore +++ b/.gitignore @@ -200,6 +200,7 @@ tracefile.txt *.bak *.dummy *.xcworkspace +workspace.pid*/ xcuserdata compile NTRU_algorithm/ diff --git a/scripts/include.am b/scripts/include.am index f7a0bb37c8..38dc071466 100644 --- a/scripts/include.am +++ b/scripts/include.am @@ -106,9 +106,22 @@ endif dist_noinst_SCRIPTS+= scripts/unit.test noinst_SCRIPTS+= scripts/unit.test.in +# multi-msg-record.test drives the wolfSSL example client against a +# Python (tlslite-ng) peer to verify parsing of TLS records that carry +# multiple handshake messages. The script probes the client binary +# at runtime for TLS 1.2, TLS 1.3 and secure-renegotiation support +# and skips phases that are not compiled in. The whole test exits 77 +# (SKIP) if python3 or tlslite-ng is missing or if nothing is +# runnable. +dist_noinst_SCRIPTS+= scripts/multi-msg-record.test + endif endif +# The Python half of multi-msg-record.test always ships in tarballs so +# the wrapper can find it on the installed side. +EXTRA_DIST+= scripts/multi-msg-record.py + dist_noinst_SCRIPTS+= scripts/pem.test EXTRA_DIST += scripts/sniffer-static-rsa.pcap \ diff --git a/scripts/multi-msg-record.py b/scripts/multi-msg-record.py new file mode 100755 index 0000000000..ae035d5605 --- /dev/null +++ b/scripts/multi-msg-record.py @@ -0,0 +1,655 @@ +#!/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__)) +WOLFSSL_DIR = os.path.dirname(SCRIPT_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 _get_free_port(): + """Get an available TCP port.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return s.getsockname()[1] + + +def _listen_socket(): + """Bind a listening TCP socket on localhost with the standard test timeout.""" + port = _get_free_port() + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind(("127.0.0.1", port)) + srv.listen(1) + srv.settimeout(15) + return srv, port + + +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) + + +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): + 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 = (result["error"] + or proc.stderr.decode("utf-8", errors="replace")[:400]) + 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 = result["error"] or proc.stderr.decode("utf-8", errors="replace")[:200] + 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 + + +# --------------------------------------------------------------------------- +# 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)") + 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) diff --git a/scripts/multi-msg-record.test b/scripts/multi-msg-record.test new file mode 100755 index 0000000000..3bb70b9ff6 --- /dev/null +++ b/scripts/multi-msg-record.test @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# +# multi-msg-record.test +# +# Wrapper around scripts/multi-msg-record.py, invoked by automake via +# `make check`. Verifies that the wolfSSL client correctly processes +# TLS records that carry multiple handshake messages in a single record +# (TLS 1.2 plaintext + renegotiation, and TLS 1.3 encrypted flight). +# +# The test is SKIPped (exit 77) when python3 or tlslite-ng is not +# installed. +# + +# if we can, isolate the network namespace to eliminate port collisions. +if [[ -n "$NETWORK_UNSHARE_HELPER" ]]; then + if [[ -z "$NETWORK_UNSHARE_HELPER_CALLED" ]]; then + export NETWORK_UNSHARE_HELPER_CALLED=yes + exec "$NETWORK_UNSHARE_HELPER" "$0" "$@" || exit $? + fi +elif [ "${AM_BWRAPPED-}" != "yes" ]; then + bwrap_path="$(command -v bwrap)" + if [ -n "$bwrap_path" ]; then + export AM_BWRAPPED=yes + exec "$bwrap_path" --unshare-net --dev-bind / / "$0" "$@" + fi + unset AM_BWRAPPED +fi + +# Locate python3 - skip (exit 77) if unavailable. +PYTHON="$(command -v python3)" +if [ -z "$PYTHON" ]; then + echo "python3 not found, skipping multi-msg-record test" + exit 77 +fi + +SCRIPT_DIR="$(cd -- "$(dirname -- "$0")" && pwd)" +exec "$PYTHON" "$SCRIPT_DIR/multi-msg-record.py" "$@" diff --git a/scripts/ocsp-responder-openssl-interop.test b/scripts/ocsp-responder-openssl-interop.test index ed62c19951..8e0bd3ac25 100755 --- a/scripts/ocsp-responder-openssl-interop.test +++ b/scripts/ocsp-responder-openssl-interop.test @@ -219,9 +219,9 @@ port4=$(get_first_free_port $((port3 + 1))) # OCSP responder: root-ca port5=$(get_first_free_port $((port4 + 1))) # TLS server # Responder 1: intermediate1-ca (server1=valid, server2=revoked) -log1=$(mktemp /tmp/ocsp_resp1.XXXXXX) +log1=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp1.XXXXXX") resp_logs="$resp_logs $log1" -ready1=$(mktemp /tmp/ocsp_ready1.XXXXXX) +ready1=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready1.XXXXXX") ready_files="$ready_files $ready1" $OCSP_RESPONDER -p $port1 -v -R "$ready1" \ -c $OCSP_DIR/intermediate1-ca-cert.pem \ @@ -232,9 +232,9 @@ pid1=$! resp_pids="$resp_pids $pid1" # Responder 2: intermediate2-ca (server3=valid, server4=revoked) -log2=$(mktemp /tmp/ocsp_resp2.XXXXXX) +log2=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp2.XXXXXX") resp_logs="$resp_logs $log2" -ready2=$(mktemp /tmp/ocsp_ready2.XXXXXX) +ready2=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready2.XXXXXX") ready_files="$ready_files $ready2" $OCSP_RESPONDER -p $port2 -v -R "$ready2" \ -c $OCSP_DIR/intermediate2-ca-cert.pem \ @@ -245,9 +245,9 @@ pid2=$! resp_pids="$resp_pids $pid2" # Responder 3: intermediate3-ca (server5=valid) -log3=$(mktemp /tmp/ocsp_resp3.XXXXXX) +log3=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp3.XXXXXX") resp_logs="$resp_logs $log3" -ready3=$(mktemp /tmp/ocsp_ready3.XXXXXX) +ready3=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready3.XXXXXX") ready_files="$ready_files $ready3" $OCSP_RESPONDER -p $port3 -v -R "$ready3" \ -c $OCSP_DIR/intermediate3-ca-cert.pem \ @@ -258,9 +258,9 @@ pid3=$! resp_pids="$resp_pids $pid3" # Responder 4: root-ca (intermediate CAs: 1=valid, 2=valid, 3=revoked) -log4=$(mktemp /tmp/ocsp_resp4.XXXXXX) +log4=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp4.XXXXXX") resp_logs="$resp_logs $log4" -ready4=$(mktemp /tmp/ocsp_ready4.XXXXXX) +ready4=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready4.XXXXXX") ready_files="$ready_files $ready4" $OCSP_RESPONDER -p $port4 -v -R "$ready4" \ -c $OCSP_DIR/root-ca-cert.pem \ @@ -271,9 +271,9 @@ pid4=$! resp_pids="$resp_pids $pid4" # Responder 5: authorized responder (delegated OCSP signer with id-kp-OCSPSigning) -log5=$(mktemp /tmp/ocsp_resp5.XXXXXX) +log5=$(mktemp "${TMPDIR:-/tmp}/ocsp_resp5.XXXXXX") resp_logs="$resp_logs $log5" -ready5=$(mktemp /tmp/ocsp_ready5.XXXXXX) +ready5=$(mktemp "${TMPDIR:-/tmp}/ocsp_ready5.XXXXXX") ready_files="$ready_files $ready5" $OCSP_RESPONDER -p $port5 -v -R "$ready5" \ -c $OCSP_DIR/root-ca-cert.pem \ diff --git a/scripts/ocsp-stapling.test b/scripts/ocsp-stapling.test index 8f6ed717cf..55ead3e27f 100755 --- a/scripts/ocsp-stapling.test +++ b/scripts/ocsp-stapling.test @@ -341,7 +341,9 @@ server=login.live.com #ca=certs/external/DigiCertGlobalRootCA.pem ca=./certs/external/ca_collection.pem -if [[ "$V4V6" == "4" ]]; then +if [[ -z "${WOLFSSL_EXTERNAL_TEST-}" || "$WOLFSSL_EXTERNAL_TEST" == "0" ]]; then + echo "Skipping OCSP test on $server (set WOLFSSL_EXTERNAL_TEST=1 to run)" +elif [[ "$V4V6" == "4" ]]; then retry_with_backoff 3 ./examples/client/client -C -h "$server" -p 443 -A "$ca" -g -W 1 RESULT=$? [ $RESULT -ne 0 ] && echo -e "\n\nClient connection failed" && exit 1 diff --git a/src/dtls13.c b/src/dtls13.c index baec84569e..893f6813e7 100644 --- a/src/dtls13.c +++ b/src/dtls13.c @@ -1904,7 +1904,7 @@ static int _Dtls13HandshakeRecv(WOLFSSL* ssl, byte* input, word32 size, #endif /* WOLFSSL_DEBUG_TLS */ /* ignore the message */ - *processedSize = idx + fragLength + ssl->keys.padSz; + *processedSize = idx + fragLength; return 0; } @@ -1938,7 +1938,7 @@ static int _Dtls13HandshakeRecv(WOLFSSL* ssl, byte* input, word32 size, WOLFSSL_MSG("DTLS1.3 not accepting fragmented plaintext message"); #endif /* WOLFSSL_DEBUG_TLS */ /* ignore the message */ - *processedSize = idx + fragLength + ssl->keys.padSz; + *processedSize = idx + fragLength; return 0; } } @@ -1966,7 +1966,7 @@ static int _Dtls13HandshakeRecv(WOLFSSL* ssl, byte* input, word32 size, return DTLS_TOO_MANY_FRAGMENTS_E; } - *processedSize = idx + fragLength + ssl->keys.padSz; + *processedSize = idx + fragLength; if (Dtls13NextMessageComplete(ssl)) return Dtls13ProcessBufferedMessages(ssl); diff --git a/src/internal.c b/src/internal.c index f19d8fa10d..d772797424 100644 --- a/src/internal.c +++ b/src/internal.c @@ -11905,10 +11905,7 @@ int MsgCheckEncryption(WOLFSSL* ssl, byte type, byte encrypted) static WC_INLINE int isLastMsg(const WOLFSSL* ssl, word32 msgSz) { - word32 extra = 0; - if (IsEncryptionOn(ssl, 0)) - extra = ssl->keys.padSz; - return (ssl->buffers.inputBuffer.idx - ssl->curStartIdx) + msgSz + extra + return (ssl->buffers.inputBuffer.idx - ssl->curStartIdx) + msgSz == ssl->curSize; } @@ -17698,9 +17695,6 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, ssl->options.serverState = SERVER_CERT_COMPLETE; } - if (IsEncryptionOn(ssl, 0)) - args->idx += ssl->keys.padSz; - /* Advance state and proceed */ ssl->options.asyncState = TLS_ASYNC_END; } /* case TLS_ASYNC_FINALIZE */ @@ -17964,12 +17958,6 @@ static int DoCertificateStatus(WOLFSSL* ssl, byte* input, word32* inOutIdx, SendAlert(ssl, alert_fatal, bad_certificate_status_response); } - if (IsEncryptionOn(ssl, 0)) { - if (*inOutIdx + ssl->keys.padSz > size) - return BUFFER_E; - *inOutIdx += ssl->keys.padSz; - } - WOLFSSL_LEAVE("DoCertificateStatus", ret); WOLFSSL_END(WC_FUNC_CERTIFICATE_STATUS_DO); @@ -17984,10 +17972,9 @@ static int DoCertificateStatus(WOLFSSL* ssl, byte* input, word32* inOutIdx, #if !defined(NO_TLS) && !defined(WOLFSSL_NO_TLS12) -static int DoHelloRequest(WOLFSSL* ssl, const byte* input, word32* inOutIdx, - word32 size, word32 totalSz) +static int DoHelloRequest(WOLFSSL* ssl, word32 size) { - (void)input; + (void)size; WOLFSSL_START(WC_FUNC_HELLO_REQUEST_DO); WOLFSSL_ENTER("DoHelloRequest"); @@ -17995,17 +17982,6 @@ static int DoHelloRequest(WOLFSSL* ssl, const byte* input, word32* inOutIdx, if (size) /* must be 0 */ return BUFFER_ERROR; - if (IsEncryptionOn(ssl, 0)) { - /* If size == totalSz then we are in DtlsMsgDrain so no need to worry - * about padding */ - /* access beyond input + size should be checked against totalSz */ - if (size != totalSz && - *inOutIdx + ssl->keys.padSz > totalSz) - return BUFFER_E; - - *inOutIdx += ssl->keys.padSz; - } - if (ssl->options.side == WOLFSSL_SERVER_END) { SendAlert(ssl, alert_fatal, unexpected_message); /* try */ WOLFSSL_ERROR_VERBOSE(FATAL_ERROR); @@ -18040,7 +18016,7 @@ int DoFinished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 size, * If size == totalSz then we are in DtlsMsgDrain so no need to worry about * padding */ if (size != totalSz) { - if (*inOutIdx + size + ssl->keys.padSz > totalSz) + if (*inOutIdx + size > totalSz) return BUFFER_E; } @@ -18083,8 +18059,7 @@ int DoFinished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, word32 size, } #endif - /* force input exhaustion at ProcessReply consuming padSz */ - *inOutIdx += size + ssl->keys.padSz; + *inOutIdx += size; if (ssl->options.side == WOLFSSL_CLIENT_END) { ssl->options.serverState = SERVER_FINISHED_COMPLETE; @@ -18737,8 +18712,7 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, return INCOMPLETE_DATA; } - expectedIdx = *inOutIdx + size + - (ssl->keys.encryptionOn ? ssl->keys.padSz : 0); + expectedIdx = *inOutIdx + size; #if !defined(NO_WOLFSSL_SERVER) && \ defined(HAVE_SECURE_RENEGOTIATION) && \ @@ -18903,21 +18877,13 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, case hello_request: WOLFSSL_MSG("processing hello request"); - ret = DoHelloRequest(ssl, input, inOutIdx, size, totalSz); + ret = DoHelloRequest(ssl, size); break; #ifndef NO_WOLFSSL_CLIENT case hello_verify_request: WOLFSSL_MSG("processing hello verify request"); ret = DoHelloVerifyRequest(ssl, input,inOutIdx, size); - if (IsEncryptionOn(ssl, 0)) { - /* access beyond input + size should be checked against totalSz - */ - if (*inOutIdx + ssl->keys.padSz > totalSz) - return BUFFER_E; - - *inOutIdx += ssl->keys.padSz; - } break; case server_hello: @@ -18989,8 +18955,6 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, AddLateName("ServerHelloDone", &ssl->timeoutInfo); #endif ssl->options.serverState = SERVER_HELLODONE_COMPLETE; - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; break; case finished: @@ -19022,16 +18986,6 @@ int DoHandShakeMsgType(WOLFSSL* ssl, byte* input, word32* inOutIdx, } } #endif - /* If size == totalSz then we are in DtlsMsgDrain so no need to worry - * about padding */ - if (IsEncryptionOn(ssl, 0)) { - /* access beyond input + size should be checked against totalSz - */ - if (size != totalSz && - *inOutIdx + ssl->keys.padSz > totalSz) - return BUFFER_E; - *inOutIdx += ssl->keys.padSz; - } break; case client_key_exchange: @@ -19132,7 +19086,11 @@ static int DoHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, return DoHandShakeMsgType(ssl, input, inOutIdx, type, size, totalSz); } - inputLength = ssl->buffers.inputBuffer.length - *inOutIdx; + /* totalSz is now curStartIdx + curSize (content-only, padSz already + * subtracted in ProcessReply). */ + if (*inOutIdx > totalSz) + return BUFFER_ERROR; + inputLength = totalSz - *inOutIdx; /* If there is a pending fragmented handshake message, * pending message size will be non-zero. */ @@ -19893,11 +19851,6 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, input + *inOutIdx, size, type, fragOffset, fragSz, ssl->heap); *inOutIdx += fragSz; - if (*inOutIdx + ssl->keys.padSz > totalSz) { - WOLFSSL_ERROR(BUFFER_E); - return BUFFER_E; - } - *inOutIdx += ssl->keys.padSz; ret = 0; #ifndef WOLFSSL_DTLS_RESEND_ONLY_TIMEOUT /* If we receive an out of order last flight msg then retransmit */ @@ -19936,10 +19889,6 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, /* Already saw this message and processed it. It can be ignored. */ WOLFSSL_MSG("Already saw this message and processed it"); *inOutIdx += fragSz; - if (*inOutIdx + ssl->keys.padSz > totalSz) { - WOLFSSL_ERROR(BUFFER_E); - return BUFFER_E; - } #ifndef WOLFSSL_DTLS_RESEND_ONLY_TIMEOUT if (IsDtlsNotSctpMode(ssl) && VerifyForDtlsMsgPoolSend(ssl, type, fragOffset)) { @@ -19947,7 +19896,6 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, ret = DtlsMsgPoolSend(ssl, 0); } #endif - *inOutIdx += ssl->keys.padSz; } else if (fragSz < size) { /* Since this branch is in order, but fragmented, dtls_rx_msg_list will @@ -19971,11 +19919,6 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, input + *inOutIdx, size, type, fragOffset, fragSz, ssl->heap); *inOutIdx += fragSz; - if (*inOutIdx + ssl->keys.padSz > totalSz) { - WOLFSSL_ERROR(BUFFER_E); - return BUFFER_E; - } - *inOutIdx += ssl->keys.padSz; ret = 0; if (ssl->dtls_rx_msg_list != NULL && ssl->dtls_rx_msg_list->ready) ret = DtlsMsgDrain(ssl); @@ -19992,9 +19935,9 @@ static int DoDtlsHandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, WOLFSSL_ERROR(BUFFER_ERROR); return BUFFER_ERROR; } - if (idx + fragSz + ssl->keys.padSz > totalSz) + if (idx + fragSz > totalSz) return BUFFER_E; - *inOutIdx = idx + fragSz + ssl->keys.padSz; + *inOutIdx = idx + fragSz; /* In async mode always store the message and process it with * DtlsMsgDrain because in case of a WC_PENDING_E it will be * easier this way. */ @@ -22147,7 +22090,7 @@ int DoApplicationData(WOLFSSL* ssl, byte* input, word32* inOutIdx, int sniff) } #endif - dataSz = (int)(msgSz - ssl->keys.padSz); + dataSz = (int)msgSz; if (dataSz < 0) { WOLFSSL_MSG("App data buffer error, malicious input?"); if (sniff == NO_SNIFF) { @@ -22202,8 +22145,6 @@ int DoApplicationData(WOLFSSL* ssl, byte* input, word32* inOutIdx, int sniff) ssl->buffers.clearOutputBuffer.length = (unsigned int)dataSz; } - idx += ssl->keys.padSz; - #ifdef HAVE_LIBZ /* decompress could be bigger, overwrite after verify */ if (ssl->options.usingCompression) @@ -22494,9 +22435,6 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type) } #endif - if (IsEncryptionOn(ssl, 0)) - dataSz -= ssl->keys.padSz; - /* make sure can read the message */ if (dataSz != ALERT_SIZE) { #ifdef WOLFSSL_EXTRA_ALERTS @@ -22565,9 +22503,6 @@ static int DoAlert(WOLFSSL* ssl, byte* input, word32* inOutIdx, int* type) WOLFSSL_ERROR(*type); } } - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; - return level; } @@ -23575,6 +23510,14 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) } + /* Reduce curSize to content only, excluding padding/MAC overhead. + * padSz is accounted for once at the end of record processing. */ + if (IsEncryptionOn(ssl, 0)) { + if (ssl->keys.padSz > ssl->curSize) + return BUFFER_E; + ssl->curSize -= (word16)ssl->keys.padSz; + } + /* in case > 1 msg per record */ ssl->curStartIdx = ssl->buffers.inputBuffer.idx; @@ -23592,7 +23535,8 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) WOLFSSL_MSG("Dropping DTLS record outside receiving " "window"); ssl->options.processReply = doProcessInit; - ssl->buffers.inputBuffer.idx += ssl->curSize; + ssl->buffers.inputBuffer.idx += ssl->curSize + + ssl->keys.padSz; if (ssl->buffers.inputBuffer.idx > ssl->buffers.inputBuffer.length) return BUFFER_E; @@ -23632,7 +23576,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) } #if defined(HAVE_ENCRYPT_THEN_MAC) && !defined(WOLFSSL_AEAD_ONLY) if (IsEncryptionOn(ssl, 0) && ssl->options.startedETMRead) { - if ((ssl->curSize - ssl->keys.padSz > MAX_PLAINTEXT_SZ) + if ((ssl->curSize > MAX_PLAINTEXT_SZ) #ifdef WOLFSSL_ASYNC_CRYPT && ssl->buffers.inputBuffer.length != ssl->buffers.inputBuffer.idx @@ -23650,7 +23594,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) #endif /* TLS13 plaintext limit is checked earlier before decryption */ if (!IsAtLeastTLSv1_3(ssl->version) - && ssl->curSize - ssl->keys.padSz > MAX_PLAINTEXT_SZ + && ssl->curSize > MAX_PLAINTEXT_SZ #ifdef WOLFSSL_ASYNC_CRYPT && ssl->buffers.inputBuffer.length != ssl->buffers.inputBuffer.idx @@ -23676,7 +23620,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ret = DoDtlsHandShakeMsg(ssl, ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length); + ssl->curStartIdx + ssl->curSize); if (ret == 0 || ret == WC_NO_ERR_TRACE(WC_PENDING_E)) { /* Reset timeout as we have received a valid @@ -23696,7 +23640,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ret = Dtls13HandshakeRecv(ssl, ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length); + ssl->curStartIdx + ssl->curSize); #ifdef WOLFSSL_EARLY_DATA if (ret == 0 && ssl->options.side == WOLFSSL_SERVER_END && @@ -23707,8 +23651,12 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) exit */ ssl->earlyData = no_early_data; ssl->options.processReply = doProcessInit; - if (ssl->options.clientInEarlyData) + if (ssl->options.clientInEarlyData) { + if (IsEncryptionOn(ssl, 0)) + ssl->buffers.inputBuffer.idx += + ssl->keys.padSz; return APP_DATA_READY; + } } #endif /* WOLFSSL_EARLY_DATA */ if (ret == 0 || @@ -23729,7 +23677,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ret = DoHandShakeMsg(ssl, ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length); + ssl->curStartIdx + ssl->curSize); if (ret != 0) { if (SendFatalAlertOnly(ssl, ret) == WC_NO_ERR_TRACE(SOCKET_ERROR_E)) @@ -23745,7 +23693,7 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ret = DoTls13HandShakeMsg(ssl, ssl->buffers.inputBuffer.buffer, &ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length); + ssl->curStartIdx + ssl->curSize); #ifdef WOLFSSL_EARLY_DATA if (ret != 0) return ret; @@ -23754,8 +23702,12 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ssl->options.handShakeState == HANDSHAKE_DONE) { ssl->earlyData = no_early_data; ssl->options.processReply = doProcessInit; - if (ssl->options.clientInEarlyData) + if (ssl->options.clientInEarlyData) { + if (IsEncryptionOn(ssl, 0)) + ssl->buffers.inputBuffer.idx += + ssl->keys.padSz; return APP_DATA_READY; + } } #endif #else @@ -23855,11 +23807,6 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) return LENGTH_ERROR; } - if (IsEncryptionOn(ssl, 0) && ssl->options.handShakeDone) { - ssl->buffers.inputBuffer.idx += ssl->keys.padSz; - ssl->curSize -= (word16)ssl->keys.padSz; - } - if (ssl->curSize != 1) { WOLFSSL_MSG("Malicious or corrupted ChangeCipher msg"); WOLFSSL_ERROR_VERBOSE(LENGTH_ERROR); @@ -24023,11 +23970,10 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) word32 processedSize = 0; ret = DoDtls13Ack(ssl, ssl->buffers.inputBuffer.buffer + ssl->buffers.inputBuffer.idx, - ssl->buffers.inputBuffer.length - - ssl->buffers.inputBuffer.idx - - ssl->keys.padSz, &processedSize); + ssl->curStartIdx + ssl->curSize - + ssl->buffers.inputBuffer.idx, + &processedSize); ssl->buffers.inputBuffer.idx += processedSize; - ssl->buffers.inputBuffer.idx += ssl->keys.padSz; if (ret != 0) return ret; break; @@ -24041,53 +23987,44 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) ssl->options.processReply = doProcessInit; - /* input exhausted */ - if (ssl->buffers.inputBuffer.idx >= ssl->buffers.inputBuffer.length -#ifdef WOLFSSL_DTLS - || (ssl->options.dtls && - /* If app data was processed then return now to avoid - * dropping any app data. */ - (ssl->curRL.type == application_data || - /* client: if we processed a finished message, return to - * allow higher layers to establish the crypto - * parameters of the connection. The remaining data - * may be app data that we would drop without the - * crypto setup. */ - (ssl->options.side == WOLFSSL_CLIENT_END && - ssl->options.serverState == SERVER_FINISHED_COMPLETE && - ssl->options.handShakeState != HANDSHAKE_DONE))) -#endif - ) { - /* Shrink input buffer when we successfully finish record - * processing */ - if ((ret == 0) && ssl->buffers.inputBuffer.dynamicFlag) - ShrinkInputBuffer(ssl, NO_FORCED_FREE); - return ret; - } /* more messages per record */ - else if ((ssl->buffers.inputBuffer.idx - ssl->curStartIdx) + if ((ssl->buffers.inputBuffer.idx - ssl->curStartIdx) < ssl->curSize) { WOLFSSL_MSG("More messages in record"); ssl->options.processReply = runProcessingOneMessage; - - if (IsEncryptionOn(ssl, 0)) { - /* With encryption on, we advance the index by the value - * of ssl->keys.padSz. Since padding only appears once, we - * only can do this at the end of record parsing. We have to - * reset the index to the start of the next message here. */ - if (ssl->buffers.inputBuffer.idx >= ssl->keys.padSz) { - ssl->buffers.inputBuffer.idx -= ssl->keys.padSz; - } - else { - WOLFSSL_MSG("\tBuffer advanced not enough error"); - WOLFSSL_ERROR_VERBOSE(FATAL_ERROR); - return FATAL_ERROR; - } - } } - /* more records */ else { + /* Done with this record. Advance past padding/MAC. */ + if (IsEncryptionOn(ssl, 0)) + ssl->buffers.inputBuffer.idx += ssl->keys.padSz; + + /* input exhausted */ + if (ssl->buffers.inputBuffer.idx >= + ssl->buffers.inputBuffer.length +#ifdef WOLFSSL_DTLS + || (ssl->options.dtls && + /* If app data was processed then return now to avoid + * dropping any app data. */ + (ssl->curRL.type == application_data || + /* client: if we processed a finished message, return + * to allow higher layers to establish the + * crypto parameters of the connection. The + * remaining data may be app data that we would + * drop without the crypto setup. */ + (ssl->options.side == WOLFSSL_CLIENT_END && + ssl->options.serverState == + SERVER_FINISHED_COMPLETE && + ssl->options.handShakeState != HANDSHAKE_DONE))) +#endif + ) { + /* Shrink input buffer when we successfully finish record + * processing */ + if ((ret == 0) && ssl->buffers.inputBuffer.dynamicFlag) + ShrinkInputBuffer(ssl, NO_FORCED_FREE); + return ret; + } + /* more records */ WOLFSSL_MSG("More records in input"); } #ifdef WOLFSSL_ASYNC_CRYPT @@ -24102,10 +24039,14 @@ static int DoProcessReplyEx(WOLFSSL* ssl, int allowSocketErr) if (ret == WC_NO_ERR_TRACE(APP_DATA_READY)) return ret; #endif - /* It is safe to shrink the input buffer here now. local vars will - * be reset to the new starting value. */ - if (ret == 0 && ssl->buffers.inputBuffer.dynamicFlag) + /* It is safe to shrink the input buffer here now, but only + * when done with the current record. During multi-message + * record processing, shrinking would invalidate curStartIdx + * and curSize. */ + if (ssl->options.processReply != runProcessingOneMessage + && ret == 0 && ssl->buffers.inputBuffer.dynamicFlag) { ShrinkInputBuffer(ssl, NO_FORCED_FREE); + } continue; default: WOLFSSL_MSG("Bad process input state, programming error"); @@ -32163,9 +32104,6 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) ssl->options.serverState = SERVER_HELLO_COMPLETE; - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; - #ifdef HAVE_SECRET_CALLBACK if (ssl->sessionSecretCb != NULL #ifdef HAVE_SESSION_TICKET @@ -32497,9 +32435,6 @@ static void MakePSKPreMasterSecret(Arrays* arrays, byte use_psk_key) ssl->options.sendVerify = SEND_BLANK_CERT; } - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; - WOLFSSL_LEAVE("DoCertificateRequest", 0); WOLFSSL_END(WC_FUNC_CERTIFICATE_REQUEST_DO); @@ -33739,9 +33674,6 @@ static int DoServerKeyExchange(WOLFSSL* ssl, const byte* input, case TLS_ASYNC_FINALIZE: { - if (IsEncryptionOn(ssl, 0)) - args->idx += ssl->keys.padSz; - /* Advance state and proceed */ ssl->options.asyncState = TLS_ASYNC_END; } /* case TLS_ASYNC_FINALIZE */ @@ -35509,9 +35441,6 @@ static int DoSessionTicket(WOLFSSL* ssl, const byte* input, word32* inOutIdx, #endif } - if (IsEncryptionOn(ssl, 0)) - *inOutIdx += ssl->keys.padSz; - ssl->expect_session_ticket = 0; return 0; @@ -39340,9 +39269,6 @@ static int AddPSKtoPreMasterSecret(WOLFSSL* ssl) case TLS_ASYNC_FINALIZE: { - if (IsEncryptionOn(ssl, 0)) - args->idx += ssl->keys.padSz; - ssl->options.havePeerVerify = 1; /* Set final index */ @@ -42404,9 +42330,6 @@ static int DefTicketEncCb(WOLFSSL* ssl, byte key_name[WOLFSSL_TICKET_NAME_SZ], case TLS_ASYNC_FINALIZE: { - if (IsEncryptionOn(ssl, 0)) - args->idx += ssl->keys.padSz; - ret = MakeMasterSecret(ssl); /* Check for error */ diff --git a/src/tls13.c b/src/tls13.c index 1d7e0fd3e1..db0d352db8 100644 --- a/src/tls13.c +++ b/src/tls13.c @@ -6005,9 +6005,6 @@ static int DoTls13EncryptedExtensions(WOLFSSL* ssl, const byte* input, /* Move index to byte after message. */ *inOutIdx = i + totalExtSz; - /* Always encrypted. */ - *inOutIdx += ssl->keys.padSz; - #ifdef WOLFSSL_EARLY_DATA if (ssl->earlyData != no_early_data) { TLSX* ext = TLSX_Find(ssl->extensions, TLSX_EARLY_DATA); @@ -6148,9 +6145,6 @@ static int DoTls13CertificateRequest(WOLFSSL* ssl, const byte* input, #endif } - /* This message is always encrypted so add encryption padding. */ - *inOutIdx += ssl->keys.padSz; - #ifdef WOLFSSL_POST_HANDSHAKE_AUTH { /* CertReqCtx has one byte at end for context value. @@ -11619,9 +11613,6 @@ static int DoTls13CertificateVerify(WOLFSSL* ssl, byte* input, args->idx += args->sz; *inOutIdx = args->idx; - /* Encryption is always on: add padding */ - *inOutIdx += ssl->keys.padSz; - /* Advance state and proceed */ ssl->options.asyncState = TLS_ASYNC_END; @@ -11819,8 +11810,7 @@ int DoTls13Finished(WOLFSSL* ssl, const byte* input, word32* inOutIdx, } } - /* Force input exhaustion at ProcessReply by consuming padSz. */ - *inOutIdx += size + ssl->keys.padSz; + *inOutIdx += size; #ifndef NO_WOLFSSL_SERVER if (ssl->options.side == WOLFSSL_SERVER_END && @@ -12273,8 +12263,6 @@ static int DoTls13KeyUpdate(WOLFSSL* ssl, const byte* input, word32* inOutIdx, /* Move index to byte after message. */ *inOutIdx += totalSz; - /* Always encrypted. */ - *inOutIdx += ssl->keys.padSz; /* Future traffic uses new decryption keys. */ if ((ret = DeriveTls13Keys(ssl, update_traffic_key, DECRYPT_SIDE_ONLY, 1)) @@ -12425,9 +12413,6 @@ static int DoTls13EndOfEarlyData(WOLFSSL* ssl, const byte* input, ssl->earlyData = done_early_data; - /* Always encrypted. */ - *inOutIdx += ssl->keys.padSz; - ret = SetKeysSide(ssl, DECRYPT_SIDE_ONLY); WOLFSSL_LEAVE("DoTls13EndOfEarlyData", ret); @@ -12599,9 +12584,6 @@ static int DoTls13NewSessionTicket(WOLFSSL* ssl, const byte* input, AddSession(ssl); #endif - /* Always encrypted. */ - *inOutIdx += ssl->keys.padSz; - ssl->expect_session_ticket = 0; #else (void)ssl; @@ -12609,7 +12591,7 @@ static int DoTls13NewSessionTicket(WOLFSSL* ssl, const byte* input, WOLFSSL_ENTER("DoTls13NewSessionTicket"); - *inOutIdx += size + ssl->keys.padSz; + *inOutIdx += size; #endif /* HAVE_SESSION_TICKET */ WOLFSSL_LEAVE("DoTls13NewSessionTicket", 0); @@ -13952,7 +13934,11 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, totalSz); } - inputLength = ssl->buffers.inputBuffer.length - *inOutIdx - ssl->keys.padSz; + /* totalSz is now curStartIdx + curSize (content-only, padSz already + * subtracted in ProcessReply). */ + if (*inOutIdx > totalSz) + return BUFFER_ERROR; + inputLength = totalSz - *inOutIdx; /* If there is a pending fragmented handshake message, * pending message size will be non-zero. */ @@ -13994,7 +13980,7 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, input + *inOutIdx - HANDSHAKE_HEADER_SZ, inputLength); ssl->arrays->pendingMsgOffset = inputLength; - *inOutIdx += inputLength + ssl->keys.padSz - HANDSHAKE_HEADER_SZ; + *inOutIdx += inputLength - HANDSHAKE_HEADER_SZ; return 0; } @@ -14018,7 +14004,7 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, XMEMCPY(ssl->arrays->pendingMsg + ssl->arrays->pendingMsgOffset, input + *inOutIdx, inputLength); ssl->arrays->pendingMsgOffset += inputLength; - *inOutIdx += inputLength + ssl->keys.padSz; + *inOutIdx += inputLength; if (ssl->arrays->pendingMsgOffset == ssl->arrays->pendingMsgSz) { @@ -14033,7 +14019,7 @@ int DoTls13HandShakeMsg(WOLFSSL* ssl, byte* input, word32* inOutIdx, ret == WC_NO_ERR_TRACE(OCSP_WANT_READ)) { /* setup to process fragment again */ ssl->arrays->pendingMsgOffset -= inputLength; - *inOutIdx -= inputLength + ssl->keys.padSz; + *inOutIdx -= inputLength; } else #endif