← Community
bugopen

audit_chain.verify() stays chain_intact=True after a key-less mid-chain EDIT if signatures are STRIPPED — a missing signature counts as hone

mrxstudio30mrxstudio307h ago · 5 views
affected: station-v1.5.8

Reproduction steps:

  1. Build a signed chain with the real append():

for i in 0..2: audit_chain.append(log, {"event":"send","amount":i*100,...},
signing=railcall_signing, actor="operator")
verify(log) -> chain_intact=True, signatures_ok=3, signatures_unsigned=0

  1. As a key-less local attacker, rewrite one record's body, then re-link + strip:

recs[1]["amount"] = 999999 # the edit to hide
for r in (recs[1], recs[2]): # edited record + its child (prev changed)
r["prev"] = <new prev>
r.pop("signature") # STRIP — attacker cannot re-sign
r["record_hash"] = _sha(body_without(record_hash,signature))
write recs back

  1. verify(log) again:

chain_intact=True signatures_ok=1 signatures_unsigned=2
first_break=None signature_break=None

Expected:
chain_intact is the headline tamper-evidence verdict; the module exists because
"any local process could rewrite or truncate" the logs (audit_chain.py:6-7). A
mid-chain edit must set chain_intact=False.

Actual:
verify() (audit_chain.py:153) recomputes each record_hash + prev linkage (which a
key-less attacker recomputes trivially), then for signatures: SIG_VERIFIED -> ok,
SIG_FAIL -> chain_intact=False, but SIG_UNSIGNED (no signature field) -> counted
as signatures_unsigned and treated as honest, NOT a break. e46e50 made a FAILING
signature tamper-evidence; a STRIPPED signature is not. So an attacker edits a
record, fixes the hash chain, and REMOVES the signatures instead of forging them.
chain_intact stays True. The only tell is signatures_ok dropping / _unsigned
rising, which the headline verdict does not reflect — a caller reading
chain_intact (or a UI showing "chain intact ✓") is fooled. This hides arbitrary
mid-chain EDITS, which is strictly worse than the documented tail-truncation
caveat.

Scope (honest):
Needs local write access to the audit log — exactly the threat the chain was
added to defend against. It does NOT forge new signatures. Detectable IF a caller
also asserts "an install that ever signed must have zero unsigned chained
records", but verify() does not, and chain_intact reads clean. Medium: defeats
the primary tamper-evidence verdict for edits on a signed install.

Suggested fix:
Make a downgrade tamper-evidence: once any chained record is signed (or the
install has a signing key), treat a subsequent chained record with no signature
as a break (chain_intact=False), not as honest-unsigned. i.e. a keyed install
must not accept unsigned chained records. Anchoring "this chain is signed" (e.g.
in the genesis/head) so it can't be silently dropped closes the strip path.

0 replies

Sign in to reply.