Skip to content
1Claw Academy
Curriculum/Compliance & Operations3 minAdvanced · Lesson 2 of 11

Lab: prove nobody read it

Lab

Build a hash-chained audit log, try to answer an auditor's question, then edit and delete entries and watch the chain object.

The audit lesson said that showing no matching event proves nothing on its own, and that negative assurance needs completeness evidence attached. This lab builds the mechanism that supplies it, in about thirty lines.

Tip

Each step appends to the same script, so every run reprints the sections before it. The expected output shown under each step is the new part; scroll to the bottom of your terminal to find it.

  1. 1

    Build a log where each entry commits to the one before it. That single field is what turns a list into evidence.

    bash
    mkdir -p /tmp/auditlab && cd /tmp/auditlab
    cat > log.py <<'EOF'
    import hashlib, json, copy
    
    def entry(seq, actor, action, target, prev_hash):
        e = {"seq": seq, "actor": actor, "action": action,
             "target": target, "prev": prev_hash}
        e["hash"] = hashlib.sha256(
            json.dumps(e, sort_keys=True).encode()).hexdigest()[:16]
        return e
    
    def build(events):
        log, prev = [], "genesis"
        for i, (actor, action, target) in enumerate(events, 1):
            e = entry(i, actor, action, target, prev)
            log.append(e); prev = e["hash"]
        return log
    
    def verify(log):
        problems, prev = [], "genesis"
        for i, e in enumerate(log):
            if e["seq"] != i + 1:
                problems.append(f"gap: expected seq {i+1}, found {e['seq']}")
            if e["prev"] != prev:
                problems.append(f"seq {e['seq']}: prev hash does not match")
            body = {k: e[k] for k in ("seq","actor","action","target","prev")}
            if hashlib.sha256(
                json.dumps(body, sort_keys=True).encode()).hexdigest()[:16] != e["hash"]:
                problems.append(f"seq {e['seq']}: modified after it was written")
            prev = e["hash"]
        return problems
    
    log = build([
        ("alice",   "read",  "app/config"),
        ("deploy",  "read",  "app/config"),
        ("mallory", "read",  "prod/db/root"),
        ("alice",   "write", "app/config"),
    ])
    for e in log:
        print(f"  {e['seq']} {e['actor']:8} {e['action']:6} {e['target']:14} {e['hash']}")
    print("verify:", verify(log) or "intact")
    EOF
    python3 log.py
  2. 2

    A normal, intact log.

    text
      1 alice    read   app/config     1f60b80d41853f19
      2 deploy   read   app/config     9f6775b6316dd39e
      3 mallory  read   prod/db/root   7db2cf9f81110058
      4 alice    write  app/config     6c8ab733497f6d9c
    verify: intact
  3. 3

    Now be the person who wants entry three to say something else.

    bash
    cat >> log.py <<'EOF'
    
    print("\n--- edit the entry ---")
    edited = copy.deepcopy(log); edited[2]["target"] = "app/config"
    print("verify:", verify(edited))
    
    print("\n--- delete it instead ---")
    deleted = [e for e in copy.deepcopy(log) if e["seq"] != 3]
    print("verify:", verify(deleted))
    EOF
    python3 log.py
  4. 4

    Both attempts are detected, and for different reasons worth distinguishing.

    text
    --- edit the entry ---
    verify: ['seq 3: modified after it was written']
    
    --- delete it instead ---
    verify: ['gap: expected seq 3, found 4', 'seq 4: prev hash does not match']
  5. 5

    Now answer the auditor properly. The claim is two statements, not one.

    bash
    cat >> log.py <<'EOF'
    
    print("\n--- the auditor's question ---")
    reads = [e for e in log if e["action"] == "read" and e["target"] == "prod/db/root"]
    print(f"  matching events:  {len(reads)}")
    print(f"  log complete:     {verify(log) or 'chain intact, no gaps'}")
    EOF
    python3 log.py
  6. 6

    Clean up.

    bash
    cd /tmp && rm -rf auditlab

The two tampering attempts failed differently, and knowing which is which matters during an investigation.

  • Editing an entry breaks that entry's own hash, because the hash covers its contents. You learn exactly which record was altered.
  • Deleting an entry breaks two things: the sequence has a hole, and the following entry's prev no longer matches. Deletion is louder than modification, which is the opposite of what most people expect.
  • Neither is prevented. A hash chain is tamper-evident, not tamper-proof, and the distinction is the whole design: you cannot stop someone with write access from changing a file, you can make it impossible to do so quietly.
Tip

This is why the answer to "prove nobody read it" is two statements joined together: no event matches, and the log is complete and unmodified for the period. The first alone is worthless, and it is the one people give.

Watch out

A chain verified only by the party who could tamper with it is weaker than it looks. Real deployments anchor it somewhere the operator does not control: a periodic hash published externally, a write-once store, or a third party countersigning.

Every property this lab demonstrates is one STRIDE calls repudiation. An actor denies having done something and you cannot prove otherwise, which is a security failure with no confidentiality breach, no downtime, and nothing an intrusion detector would ever flag.

Check your understanding

3 questions
1

Why does deleting an entry produce two errors rather than one?

2

What does a hash chain actually provide?

3

What is the complete answer to 'prove nobody read this secret in Q3'?