forgectrl.tls-records: a window another client connected in is measured again

The test judges each connection by the kernel's TLS counters and the
crypto engine's interrupt, and both are the machine's, not the
connection's. On image 20260928175056's campaign the test before it
restarted forgectrl, the operator's panel (six HTTPS connections from
the bench PC) reconnected in the same second as aes-1.3#1, and the
kernel counted 7 sessions each way where the test wanted exactly 1; the
connection itself chose AES-128-GCM, carried the page byte for byte, and
counted no decrypt error. _fetch now also reads the machine's accepted
TCP connections (PassiveOpens, shared by both families); a case whose
window saw any connection but its own is measured again, up to 30 times
after a pause drawn from 0.1 to 0.9 s (so no steady poller, such as a
page polling this suite once a second, stays in step with it), and
fails if every window was contested. The judgments are unchanged: exactly one session each way for a cipher the kernel seals,
none for CBC, the engine's interrupt counting the AES records and not
the ChaCha20 ones.

Proof: pyflakes clean. On the bench reference, image 20260928175056,
with an openssl client connecting to :443 every 0.25 s for the first
6 s of the run and the drill polling this suite once a second: the
image's test fails (desktop-1.2 counted 2 sessions each way), the fixed
test passes with contested windows measured again (at most 4 attempts a
case), and passes again without the extra clients (at most 4).

Acceptance: the change is forgectrl.tls-records itself; tlsrec.py holds
no other test, so no other fingerprint moves.
This commit is contained in:
ScottW514
2026-09-28 14:23:04 -04:00
parent f47ddad839
commit 83686fc712
+29 -7
View File
@@ -23,6 +23,7 @@ of every test there.
"""
import os
import random
import shutil
import subprocess
import tempfile
@@ -51,6 +52,14 @@ def _tls_stat():
return out
def _passive_opens():
"""The TCP connections the machine has accepted, on every port and both
families (the TCP counters are shared)."""
with open("/proc/net/snmp") as f:
rows = [line.split() for line in f if line.startswith("Tcp:")]
return int(rows[1][rows[0].index("PassiveOpens")])
def _engine_irqs():
with open("/proc/interrupts") as f:
for line in f:
@@ -80,16 +89,17 @@ def _driver(name):
def _fetch(version, offer, work):
"""GET the page over HTTPS on loopback with the given offer. Returns
the suite, the status line, the body, the kernel counters' change,
and the crypto engine's interrupts during the connection."""
the crypto engine's interrupts during the connection, and how many
other connections the machine accepted meanwhile."""
req = os.path.join(work, "req")
with open(req, "wb") as f:
f.write(b"GET " + PAGE.encode() + b" HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
opt = ["-tls1_3", "-ciphersuites", offer] if version == "1.3" else ["-tls1_2", "-cipher", offer]
s0, i0 = _tls_stat(), _engine_irqs()
s0, i0, p0 = _tls_stat(), _engine_irqs(), _passive_opens()
with open(req, "rb") as stdin:
r = subprocess.run([OPENSSL, "s_client", "-connect", "127.0.0.1:443", "-brief", "-ign_eof"] + opt,
stdin=stdin, capture_output=True, timeout=60)
s1, i1 = _tls_stat(), _engine_irqs()
s1, i1, p1 = _tls_stat(), _engine_irqs(), _passive_opens()
info = {}
for line in r.stderr.decode("utf-8", "replace").splitlines():
k, _, v = line.partition(":")
@@ -97,7 +107,7 @@ def _fetch(version, offer, work):
head, _, body = r.stdout.partition(b"\r\n\r\n")
delta = {k: s1.get(k, 0) - s0.get(k, 0) for k in ("TlsTxSw", "TlsRxSw", "TlsDecryptError")}
return (info.get("Ciphersuite"), head.split(b"\r\n")[0].decode("latin-1"), body, delta,
(i1 or 0) - (i0 or 0))
(i1 or 0) - (i0 or 0), p1 - p0 - 1)
@test("forgectrl.tls-records", title="HTTPS: ChaCha20 chosen, records sealed in the kernel",
@@ -116,7 +126,9 @@ def _fetch(version, offer, work):
"offering a CBC suite alone still connects and stays in GnuTLS, which is the control "
"for the kernel's counters; the ChaCha20 runs are the control for the engine's "
"interrupt. Every copy of the panel page must be the plain-HTTP page byte for byte, "
"and the kernel must count no decrypt error. Nothing on the machine changes.")
"and the kernel must count no decrypt error. The counters and the interrupt are the "
"machine's, so a connection during which another client connected (a panel "
"reconnecting after a restart) is measured again. Nothing on the machine changes.")
def tls_records(ctx):
ev = ctx.evidence
@@ -152,9 +164,19 @@ def tls_records(ctx):
True)]
cases.append(("cbc-1.2", "1.2", "ECDHE-ECDSA-AES128-SHA", "ECDHE-ECDSA-AES128-SHA", False))
for label, version, offer, want, kernel in cases:
suite, status, body, delta, irqs = _fetch(version, offer, work)
# The kernel's counters and the engine's interrupt are the machine's, not the
# connection's: a window in which another client connected (a panel reconnecting
# after a restart, a page polling this suite once a second) says nothing about this
# one, and is measured again, after a pause that no steady poller stays in step with.
for attempt in range(1, 31):
suite, status, body, delta, irqs, others = _fetch(version, offer, work)
if others <= 0:
break
ctx.log("%s: %d other connection(s) during the window; measured again", label, others)
ctx.sleep(random.uniform(0.1, 0.9))
ctx.check(others <= 0, "%s: another client connected during each of %d windows", label, attempt)
run = {"offer": offer, "suite": suite, "status": status, "bytes": len(body),
"same_as_http": body == plain, "tls_stat": delta, "engine_irqs": irqs}
"same_as_http": body == plain, "tls_stat": delta, "engine_irqs": irqs, "attempts": attempt}
runs[label] = run
ctx.log("%s: offer %s -> %s, %s, %d bytes, same %s, tls_stat %s, %s +%d",
label, offer, suite, status, len(body), body == plain, delta, ENGINE_IRQ, irqs)