R-403 drill evidence + the credential reader that ends a three-time mistake
gates / gates (push) Successful in 16s
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.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Read ONE value out of ~/.config/credentials, correctly, and refuse loudly when it is not.
|
||||
|
||||
Run: python3 scripts/read_credential.py <KEY> <OUTFILE> [--credentials PATH]
|
||||
|
||||
WHY THIS FILE EXISTS — it is the third occurrence that earned it.
|
||||
|
||||
Values in that file are SINGLE-quoted (`PASSWORD='...'`). Naive extraction keeps the quotes and sends
|
||||
two extra characters, and an authentication failure then reads exactly like a stale credential:
|
||||
|
||||
2026-07-20 a `Failed login` against guest 9201 was diagnosed as "the stored password is stale, the
|
||||
customer-claim flow changed it" — repeated three times and written into memory. The
|
||||
credential was correct the whole time.
|
||||
2026-08-31 the same misreading recurred and was caught in-session.
|
||||
2026-08-31 it recurred AGAIN, hours later, and this time it CHANGED A LIVE BOX: a session read a
|
||||
200-with-login-page as drift and rewrote guest 9201's `password_hash`. Repaired, but the
|
||||
original hash bytes are gone.
|
||||
|
||||
Three occurrences, and between them the project already had: a memory file stating the rule, a worked
|
||||
recipe in that memory, and a session report describing the mistake. **None of that stopped it.** A note
|
||||
is read by whoever thinks to look; a check runs whether or not anyone remembers. So the rule now lives
|
||||
in the code path instead of beside it.
|
||||
|
||||
THE VALUE IS NEVER PRINTED. It goes file → file at mode 0600 and stdout gets only its LENGTH, so a
|
||||
transcript can prove the read succeeded without carrying the secret (the standing
|
||||
operator-present-one-time-secrets rule).
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
QUOTES = ("'", '"')
|
||||
|
||||
|
||||
class CredentialError(Exception):
|
||||
"""Raised for any shape this reader will not vouch for. Always fatal, never a warning."""
|
||||
|
||||
|
||||
def unwrap(raw):
|
||||
"""Return the value inside ONE matching quote pair, asserting the result is quote-free.
|
||||
|
||||
THE ASSERTION IS THE POINT OF THIS FUNCTION. Stripping is easy and has been got wrong three
|
||||
times; what was missing every time was a check that the stripping actually worked. A returned
|
||||
value that still begins or ends with a quote character is refused here rather than sent to an
|
||||
authentication endpoint, where the failure is indistinguishable from a wrong password.
|
||||
"""
|
||||
raw = raw.rstrip("\n")
|
||||
if len(raw) >= 2 and raw[0] in QUOTES and raw[-1] == raw[0]:
|
||||
value = raw[1:-1]
|
||||
# The declared length relationship: exactly the quote pair was removed, nothing else.
|
||||
if len(value) != len(raw) - 2:
|
||||
raise CredentialError(
|
||||
"length mismatch after unwrapping: raw=%d stripped=%d (expected %d)"
|
||||
% (len(raw), len(value), len(raw) - 2))
|
||||
elif raw[:1] in QUOTES or raw[-1:] in QUOTES:
|
||||
# One quote and not the other: a truncated or hand-edited line. Refuse — guessing which end
|
||||
# is real is how a wrong secret gets sent confidently.
|
||||
raise CredentialError(
|
||||
"value is quoted on one side only (starts %r, ends %r) — refusing to guess"
|
||||
% (raw[:1], raw[-1:]))
|
||||
else:
|
||||
value = raw
|
||||
if value[:1] in QUOTES or value[-1:] in QUOTES:
|
||||
raise CredentialError(
|
||||
"value still carries a quote character after unwrapping (starts %r, ends %r) — "
|
||||
"this is the 2026-07-20 / 2026-08-31 defect and it is refused here, not sent"
|
||||
% (value[:1], value[-1:]))
|
||||
if value == "":
|
||||
raise CredentialError("value is empty")
|
||||
return value
|
||||
|
||||
|
||||
def read(path, key):
|
||||
"""Return the unwrapped value for `key`, or raise. The first matching line wins."""
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
if line.startswith(key + "="):
|
||||
return unwrap(line[len(key) + 1:])
|
||||
raise CredentialError("key %r not present in %s" % (key, path))
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||
ap.add_argument("key")
|
||||
ap.add_argument("outfile")
|
||||
ap.add_argument("--credentials",
|
||||
default=os.path.expanduser("~/.config/credentials"))
|
||||
ap.add_argument("--expect-length", type=int, default=None,
|
||||
help="refuse unless the value is exactly this long (a caller-side second opinion)")
|
||||
args = ap.parse_args(argv)
|
||||
try:
|
||||
value = read(args.credentials, args.key)
|
||||
except (CredentialError, OSError) as exc:
|
||||
print("CREDENTIAL READ REFUSED [%s]: %s" % (args.key, exc), file=sys.stderr)
|
||||
return 2
|
||||
if args.expect_length is not None and len(value) != args.expect_length:
|
||||
print("CREDENTIAL READ REFUSED [%s]: length %d, caller expected %d"
|
||||
% (args.key, len(value), args.expect_length), file=sys.stderr)
|
||||
return 2
|
||||
fd = os.open(args.outfile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
||||
fh.write(value)
|
||||
print("%s: %d characters written to %s (value not printed)"
|
||||
% (args.key, len(value), args.outfile))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user