skills: the fingerprint advice was missing its precondition, and the skill had no section on proving absence
Docs only; no image behaviour changes. WHY THIS AND NOT A PRIVATE NOTE. pi@emb-7kj4vr4g reported itself for printing sha256[:8] fingerprints of GIT_USER_EMAIL, GIT_USER_NAME and HOST_SSH_USER, and wrote a private rule forbidding it. It had not broken a rule. It followed §2 of this skill as written, and §2 is incomplete: it says a fingerprint lets you compare a credential "without ever materialising the secret" with no condition attached. When two agents independently make the same mistake, the artifact that taught them both is the bug. §2 NOW CARRIES THE PRECONDITION. A fingerprint is 32 bits over its INPUT SPACE, so publishing fp8(x) hands anyone a MEMBERSHIP ORACLE: they can test x == v for every candidate v they can generate. Safe for a 40-char random token; a wordlist for a hostname, username, e-mail, port, path, commit SHA or weak password. "High entropy" is the usual sufficient condition, NOT the test — a commit SHA is 160-bit and still fully enumerable from the repo. Operationally: if you can imagine writing the wordlist, you cannot publish the fingerprint. Also added: candidate fingerprints are working memory and never output (an extractor hashes hostnames and paths too, so the tempting "print what the scanner saw" debug step leaks low-entropy fingerprints wholesale), and a plain statement that a fingerprint register is a CONFIRMATION ORACLE for anyone already holding a candidate corpus — which is exactly how a retired token is identified in old transcripts, and works identically for someone else holding those same files. NEW §6, "Proving absence: instrument strength, and four ways a scan lies clean", placed next to §5 on purpose: §5 optimises against false POSITIVES, and every failure in §6 is a false NEGATIVE. Triage optimises precision, a gate optimises recall, and conflating them is what produced three clean reports over secrets that were really there. Contents: instrument ranking (exact-byte value search > class/structure pass > fingerprint census) with the instruction to state which one produced your zero; census vs class passes as different questions, both failure modes measured on this fleet; the tokenisation trap where quoting alone decided detectability; scan the index or pushed tree, never the working tree; git filters never run on symlinks while check-attr claims they do; two-sided self-tests that abort, incl. the fixture-interaction artifact; row-gone is not bytes-gone. Attribution kept per finding: the census/class split and the instrument ranking's provenance are pi@emb-7kj4vr4g's; exact-byte search over index blobs is pi@tor-ms22's. The credential sense of "census" originated in this skill, not with either agent. TRAP FOR THE NEXT EDITOR, also in the CHANGELOG: the frontmatter description is now 1022 of 1024 characters. Trim before adding, or the skill silently fails to load. Verified by parsing the frontmatter (1022 chars, name intact, every prior trigger phrase retained). Deployment: baked skill -> needs an image rebuild AND a container recreate to reach a running container.
This commit is contained in:
@@ -13,6 +13,65 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
||||
|
||||
## Unreleased
|
||||
|
||||
**`credential-incident-response` gained the section its own guidance had been
|
||||
missing, and §2 gained a precondition it should always have carried.** Docs only;
|
||||
no image behaviour moves. Both changes came out of a session where three separate
|
||||
detectors reported *clean* over secrets that were really there — the skill was
|
||||
the artifact that had taught two agents the pattern, so the fix belongs here
|
||||
rather than in either operator's private notes.
|
||||
|
||||
**§2 previously said an 8-hex fingerprint lets you compare a credential "without
|
||||
ever materialising the secret", with no condition attached.** That is true only
|
||||
when the *input space* is unreachable. A fingerprint is 32 bits over whatever it
|
||||
was computed from, so publishing `fp8(x)` hands anyone a **membership oracle**:
|
||||
they can test `x == v` for every candidate `v` they can generate. For a 40-char
|
||||
random token, fine. For a hostname, username, e-mail, port, path, commit SHA or
|
||||
weak password, that candidate set is a wordlist — and note that "high entropy" is
|
||||
the usual sufficient condition, not the test: a commit SHA is 160-bit and still
|
||||
fully enumerable from the repo. Two agents on this fleet published fingerprints of
|
||||
`GIT_USER_EMAIL`-class values while following this section as written; harmless in
|
||||
that instance, because those values sit in every commit trailer already, but the
|
||||
guidance licensed it. §2 now states the precondition, adds that candidate
|
||||
fingerprints are working memory and never output (a scanner hashes hostnames and
|
||||
paths too, so "print what it saw" leaks wholesale), and names what a fingerprint
|
||||
register *is* — a confirmation oracle for anyone already holding a candidate
|
||||
corpus, which is exactly how a retired token gets identified in old transcripts,
|
||||
and works the same way for someone else holding those files.
|
||||
|
||||
**New §6, "Proving absence: instrument strength, and four ways a scan lies
|
||||
clean".** Deliberately placed next to §5, because §5 optimises against false
|
||||
*positives* (name-anchoring, provenance — what stops a triage sweep drowning in
|
||||
session UUIDs) and every failure in §6 is a false *negative*. Triage optimises
|
||||
precision; a gate optimises recall, and conflating the two is what produced the
|
||||
clean reports. It carries: an instrument-strength ranking (exact-byte value search
|
||||
> class/structure pass > fingerprint census) with the standing instruction to say
|
||||
which one produced your zero; census and class passes answering different
|
||||
questions, with both failure modes measured here — a class-only pre-commit hook
|
||||
passed plaintext UUID API credentials to a shared repo twice because a UUID has no
|
||||
key header, while a census-only gate reported 0 hits with freshly-synced SSH
|
||||
private keys in the tree because no key is in the census; the tokenisation trap,
|
||||
where maximal-run extraction swallows an unquoted `VAR=<uuid>` so the value is
|
||||
never hashed alone while a *quoted* one is found, meaning quoting alone decided
|
||||
detectability; scan the index or the pushed tree, never the working tree, plus why
|
||||
a repo-only fix on an rsync-published mirror is temporary rather than weaker; git
|
||||
filters never running on symlinks, where `check-attr` answers `git-crypt` for a
|
||||
path it can never encrypt, so a coverage audit must join the attribute against the
|
||||
file mode and verify the blob magic; two-sided self-tests that abort, including
|
||||
the fixture-interaction artifact where a quoted and unquoted probe share one
|
||||
buffer and make the weak extractor look as strong as the union; and row-gone is
|
||||
not bytes-gone, since a correct sqlite DELETE leaves the payload in freelist pages
|
||||
until VACUUM.
|
||||
|
||||
Findings contributed by `pi@emb-7kj4vr4g` (the census/class split, and the
|
||||
instrument ranking's provenance) and `pi@tor-ms22` (exact-byte value search over
|
||||
index blobs). The description's trigger list grew accordingly and is 1022/1024
|
||||
characters — **it has almost no headroom, so trim before adding to it**, or the
|
||||
skill silently fails to load.
|
||||
|
||||
**Deployment:** the skill is baked at
|
||||
`/usr/local/share/pi-devbox/skills/credential-incident-response/`, so this needs
|
||||
an image rebuild **and** a container recreate to reach any running container.
|
||||
|
||||
**Two vendored skills changed, and one of the changes is a correction rather than
|
||||
an addition.** Nothing about the image's behaviour moves; this is entirely about
|
||||
what the next agent reads before it acts.
|
||||
|
||||
Reference in New Issue
Block a user