66156c619f
gates / gates (push) Successful in 16s
The drill: the loss reproduced on the shipped v0.229.0 before anything was built. 120 082 104 B -> 7 036 B in one Tier-2 run, recorded as a success. Phases 1a (before), 1b (the hollow primary, produced through the R-102 restore path exactly as the 2026-08-31 observation was), 1c (the loss), 1d (repair). scripts/read_credential.py is Part 4's rider, and it exists because a note did not work three times: 2026-07-20 a Failed login was diagnosed as a stale password and written into memory; 2026-08-31 the same misreading recurred and was caught; 2026-08-31, hours later, it recurred AGAIN and rewrote a live box's password hash. Between them the project already had a memory file stating the rule, a worked recipe in it, and a session report describing the mistake. The rule now lives in the code path: one matching quote pair is unwrapped, the result is REFUSED if it still carries a quote, and --expect-length gives the caller a second opinion. The value goes file->file at 0600 and stdout gets only its length. test_read_credential.py asserts each refusal by its reason, with a positive control before believing the not-in-stdout result. Red-proof E1: remove the final quote assertion -> three cases fail by name.
135 lines
5.5 KiB
Python
135 lines
5.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Fixture tests for read_credential.py.
|
|
|
|
Run: python3 scripts/test_read_credential.py
|
|
|
|
Every test asserts the EFFECT — the refusal happens, and the message names the reason — not merely
|
|
that the function ran. Fixtures are temp files; nothing here reads the real credentials file, and no
|
|
test contains a real secret.
|
|
"""
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
import read_credential as rc # noqa: E402
|
|
|
|
FAILURES = []
|
|
|
|
|
|
def check(name, cond, detail=""):
|
|
if cond:
|
|
print(" OK %s" % name)
|
|
else:
|
|
print(" FAIL %s %s" % (name, detail))
|
|
FAILURES.append(name)
|
|
|
|
|
|
def writefile(body):
|
|
fd, path = tempfile.mkstemp()
|
|
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
fh.write(body)
|
|
return path
|
|
|
|
|
|
# --- E1 — TestR404_CredentialLengthMismatchFailsLoudly ------------------------------------------
|
|
#
|
|
# THE REGRESSION THIS PINS, stated as the thing that actually happened: a session stripped only `"`
|
|
# from a single-quoted value, sent 15 characters where the password is 13, read the resulting
|
|
# 200-with-login-page as "the password drifted", and rewrote a live box's password hash.
|
|
#
|
|
# RED-PROOF (recorded in REPORT.md): delete the final quote-character assertion in `unwrap` and this
|
|
# test fails on `quote survives a one-sided strip`.
|
|
def test_r404_credential_length_mismatch_fails_loudly():
|
|
print("E1 TestR404_CredentialLengthMismatchFailsLoudly")
|
|
|
|
# The exact 2026-08-31 shape: single-quoted, and only `"` was stripped by the caller. The reader
|
|
# must never hand back a value carrying a quote.
|
|
p = writefile("PASSWORD='abcdefghijklm'\n")
|
|
check("single-quoted value unwraps to its 13 characters",
|
|
rc.read(p, "PASSWORD") == "abcdefghijklm")
|
|
os.unlink(p)
|
|
|
|
# A value that still carries a quote must be REFUSED, not returned.
|
|
try:
|
|
rc.unwrap("'abcdefghijklm")
|
|
check("quote survives a one-sided strip", False, "no refusal was raised")
|
|
except rc.CredentialError as exc:
|
|
check("quote survives a one-sided strip", "one side only" in str(exc), str(exc))
|
|
|
|
try:
|
|
rc.unwrap("abcdefghijklm'")
|
|
check("trailing-only quote is refused", False, "no refusal was raised")
|
|
except rc.CredentialError as exc:
|
|
check("trailing-only quote is refused", "one side only" in str(exc), str(exc))
|
|
|
|
# An unquoted value is legitimate and passes through untouched.
|
|
check("unquoted value passes through", rc.unwrap("abcdefghijklm") == "abcdefghijklm")
|
|
|
|
# Mismatched quote characters are not a pair.
|
|
try:
|
|
rc.unwrap("'abcdefghijklm\"")
|
|
check("mismatched quote pair is refused", False, "no refusal was raised")
|
|
except rc.CredentialError as exc:
|
|
check("mismatched quote pair is refused", "one side only" in str(exc), str(exc))
|
|
|
|
# Empty is refused — an empty password authenticates as nothing and reads as a wrong password.
|
|
try:
|
|
rc.unwrap("''")
|
|
check("empty value is refused", False, "no refusal was raised")
|
|
except rc.CredentialError as exc:
|
|
check("empty value is refused", "empty" in str(exc), str(exc))
|
|
|
|
# The caller's second opinion: --expect-length refuses a value of the wrong size BEFORE use.
|
|
p = writefile("PASSWORD='abcdefghijklm'\n")
|
|
out = tempfile.mkstemp()[1]
|
|
rcode = rc.main([ "PASSWORD", out, "--credentials", p, "--expect-length", "15" ])
|
|
check("--expect-length 15 is REFUSED for a 13-character value", rcode == 2, "rc=%s" % rcode)
|
|
rcode = rc.main([ "PASSWORD", out, "--credentials", p, "--expect-length", "13" ])
|
|
check("--expect-length 13 is accepted", rcode == 0, "rc=%s" % rcode)
|
|
with open(out, encoding="utf-8") as fh:
|
|
check("the value reached the file", fh.read() == "abcdefghijklm")
|
|
check("the file is 0600", oct(os.stat(out).st_mode & 0o777) == "0o600")
|
|
os.unlink(p)
|
|
os.unlink(out)
|
|
|
|
# A missing key is a refusal, not an empty string.
|
|
p = writefile("OTHER='x'\n")
|
|
try:
|
|
rc.read(p, "PASSWORD")
|
|
check("missing key is refused", False, "no refusal was raised")
|
|
except rc.CredentialError as exc:
|
|
check("missing key is refused", "not present" in str(exc), str(exc))
|
|
os.unlink(p)
|
|
|
|
|
|
# --- the value must never reach stdout ------------------------------------------------------------
|
|
def test_the_value_is_never_printed():
|
|
print("TestR404_TheValueIsNeverPrinted")
|
|
p = writefile("PASSWORD='swordfish1234'\n")
|
|
out = tempfile.mkstemp()[1]
|
|
res = subprocess.run(
|
|
[sys.executable, os.path.join(os.path.dirname(os.path.abspath(__file__)), "read_credential.py"),
|
|
"PASSWORD", out, "--credentials", p],
|
|
capture_output=True, text=True)
|
|
combined = res.stdout + res.stderr
|
|
check("exit 0", res.returncode == 0, combined)
|
|
# POSITIVE CONTROL first: the grep can find the secret when it IS there. A "not found" from a
|
|
# search that cannot find anything is not a measurement.
|
|
check("positive control — the search finds a planted copy",
|
|
"swordfish1234" in (combined + "swordfish1234"))
|
|
check("the secret is NOT in stdout/stderr", "swordfish1234" not in combined, combined)
|
|
check("the length IS reported", "13 characters" in res.stdout, res.stdout)
|
|
os.unlink(p)
|
|
os.unlink(out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
test_r404_credential_length_mismatch_fails_loudly()
|
|
test_the_value_is_never_printed()
|
|
if FAILURES:
|
|
print("\nFAILED: %d" % len(FAILURES))
|
|
sys.exit(1)
|
|
print("\nread_credential tests OK")
|