-
Notifications
You must be signed in to change notification settings - Fork 0
Wave B Auditor — proof bundle integrity report #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| # Wave B Auditor Report — Proof Bundle Integrity | ||
|
|
||
| - **Date (UTC):** 2026-04-30 12:59:17Z | ||
| - **Auditor branch:** `feat/wave-b-auditor` | ||
| - **Audited bundle:** `05_truth_proof/bundles/ea909e8c5ae4-20260430T125728Z.zip` | ||
| - size: 5533 bytes | ||
| - sha256: `b25a5faa37e69b7f5dff09f9f35995d68b0efa56b3465398b0853fd27e23efc8` | ||
| - run_id: `ea909e8c5ae4-20260430T125728Z` | ||
| - HERMES3D_PROOF_KEY used for build + verify: `auditor-key` | ||
| - **Independent verifier:** `04_testing/wave-b-reports/auditor_verify.py` | ||
| (does NOT call `conformance_runner.py`; reimplements canonical-JSON | ||
| HMAC-SHA256 verification, file-hash walk, ledger cross-ref scan, | ||
| forbidden-extras diff, and `git cat-file -e` commit binding.) | ||
| - **Cross-check:** also run through `05_truth_proof/conformance_runner.py | ||
| --bundle …` for confirmation; both verifiers agree. | ||
|
|
||
| ## Verdict | ||
|
|
||
| **GREEN** — every positive check passed and every negative test was | ||
| rejected as expected. | ||
|
|
||
| ## Positive checks (a–e) | ||
|
|
||
| | # | Check | Result | Evidence | | ||
| |---|---|---|---| | ||
| | a | Signature (HMAC-SHA256 over canonical(manifest)) | **PASS** | algorithm=`HMAC-SHA256`; expected & actual share prefix `24e07e61c9e53f5c…`; `hmac.compare_digest` true | | ||
| | b | File-hash integrity (every `manifest.files[*]` entry exists in zip and sha256 matches) | **PASS** | files_total=4, missing=0, bad_hashes=0 | | ||
| | c | Cross-refs (`evidence_ledger.md` references files in zip; no empty proof rows) | **PASS** | `evidence_ledger.md` present; rows scanned=0 (no `\| proof_envelope ` rows in this minimal bundle); no missing refs; no empty proof paths | | ||
| | d | Forbidden extras (every zip entry is referenced in `manifest.files`, plus `manifest.json`/`manifest.sig`) | **PASS** | `extras=[]` | | ||
| | e | Commit binding (`manifest.git.sha` is a real commit) | **PASS** | sha=`ea909e8c5ae4fd058116013e95a981f058094738`; `git cat-file -e` returns 0 (matches `develop` HEAD) | | ||
|
|
||
| Confirmed independently by `conformance_runner.py --bundle`: | ||
| `OK — signature + file hashes + cross-refs verified`. | ||
|
|
||
| ## Negative tests | ||
|
|
||
| Each test produced a tampered copy of the bundle in a temp directory | ||
| (`%TEMP%/wave-b-audit-neg-v630tifl/`) and ran the independent | ||
| auditor against it. Tampered bundles were NOT pushed. | ||
|
|
||
| | Scenario | Expected | Auditor result | Errors observed | | ||
| |---|---|---|---| | ||
| | Flip `build.run_id` in `manifest.json`, replace `manifest.sig` with a forged 64-zero HMAC | reject | **PASS — failure detected** | `signature mismatch` | | ||
| | Remove `evidence_ledger.md` (a manifest-listed file) from the zip | reject | **PASS — failure detected** | `file integrity: 1 missing, 0 bad hashes`; `evidence_ledger.md missing` | | ||
| | Add an unlisted `smuggled.txt` to the zip | reject | **PASS — failure detected** | `forbidden extras: ['smuggled.txt']` | | ||
|
|
||
| All three adversarial mutations were rejected with the expected, | ||
| specific error. | ||
|
|
||
| ## Recommendations | ||
|
|
||
| 1. **No regressions found.** Bundle infrastructure correctly enforces | ||
| signature, file integrity, manifest exhaustiveness, and commit | ||
| binding. | ||
| 2. *(nice-to-have)* `conformance_runner.verify_bundle` does not currently | ||
| reject zip entries that are absent from `manifest.files` (the | ||
| "forbidden extras" check d). The independent auditor flags these; | ||
| consider porting that check upstream so smuggled files cannot pass | ||
| `conformance_runner.py --bundle` either. | ||
| 3. *(nice-to-have)* Bundles built outside an integration run contain | ||
| no `| proof_envelope ` rows in `evidence_ledger.md`, so check (c) | ||
| exercises only the structural path. A future audit should re-run | ||
| against a richer bundle (post-acceptance) to cover the data path. | ||
|
|
||
| ## Artifacts | ||
|
|
||
| - `04_testing/wave-b-reports/auditor_verify.py` — independent verifier | ||
| - `04_testing/wave-b-reports/negative_tests.py` — adversarial harness | ||
| - this report |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| #!/usr/bin/env python3 | ||
| """ | ||
| Independent Wave B Auditor verifier. | ||
|
|
||
| Implements its OWN verification logic for proof bundles — does NOT call | ||
| 05_truth_proof/conformance_runner.py. Used to cross-check the bundle | ||
| infrastructure for the Wave B audit. | ||
|
|
||
| Checks (a–e): | ||
| a. signature : HMAC-SHA256 over canonical(manifest) under HERMES3D_PROOF_KEY | ||
| matches manifest.sig.value | ||
| b. file hash : every file in manifest.files exists in the zip with sha256 match | ||
| c. cross-refs : evidence_ledger.md references files that exist in the zip; | ||
| every "| proof_envelope " row has a non-empty proof path | ||
| d. forbidden : every file in the zip is referenced from manifest.files or is | ||
| one of the structural files (manifest.json / manifest.sig) | ||
| e. commit : manifest.git.sha is a real commit (`git cat-file -e <sha>`) | ||
|
|
||
| Usage: | ||
| python auditor_verify.py --bundle <zip> [--key K] | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import hashlib | ||
| import hmac | ||
| import json | ||
| import os | ||
| import subprocess | ||
| import sys | ||
| import zipfile | ||
| from pathlib import Path | ||
|
|
||
|
|
||
| def canonical(doc: dict) -> bytes: | ||
| body = {k: v for k, v in doc.items() if k != "signature"} | ||
| return json.dumps(body, sort_keys=True, separators=(",", ":")).encode("utf-8") | ||
|
|
||
|
|
||
| def sha256_bytes(b: bytes) -> str: | ||
| return hashlib.sha256(b).hexdigest() | ||
|
|
||
|
|
||
| def audit(zip_path: Path, key: bytes) -> dict: | ||
| result = { | ||
| "bundle": str(zip_path), | ||
| "checks": {}, | ||
| "errors": [], | ||
| } | ||
| with zipfile.ZipFile(zip_path, "r") as zf: | ||
| names = set(zf.namelist()) | ||
| manifest = json.loads(zf.read("manifest.json").decode("utf-8")) | ||
| sig = json.loads(zf.read("manifest.sig").decode("utf-8")) | ||
|
|
||
| # (a) signature | ||
| expected = hmac.new(key, canonical(manifest), hashlib.sha256).hexdigest() | ||
| actual = sig.get("value", "") | ||
| sig_ok = hmac.compare_digest(expected, actual) | ||
| result["checks"]["a_signature"] = { | ||
| "ok": sig_ok, | ||
| "algorithm": sig.get("algorithm"), | ||
| "expected_prefix": expected[:16], | ||
| "actual_prefix": actual[:16], | ||
| } | ||
| if not sig_ok: | ||
| result["errors"].append("signature mismatch") | ||
|
|
||
| # (b) file hashes | ||
| files = manifest.get("files", []) | ||
| bad_hashes = [] | ||
| missing = [] | ||
| for entry in files: | ||
| rel = entry["path"] | ||
| want = entry["sha256"] | ||
| if rel in ("manifest.json", "manifest.sig"): | ||
| continue | ||
| if rel not in names: | ||
| missing.append(rel) | ||
| continue | ||
| got = sha256_bytes(zf.read(rel)) | ||
| if got != want: | ||
| bad_hashes.append( | ||
| {"path": rel, "want": want[:16], "got": got[:16]} | ||
| ) | ||
| result["checks"]["b_file_hashes"] = { | ||
| "ok": not bad_hashes and not missing, | ||
| "files_total": len(files), | ||
| "missing": missing, | ||
| "bad_hashes": bad_hashes, | ||
| } | ||
| if missing or bad_hashes: | ||
| result["errors"].append( | ||
| f"file integrity: {len(missing)} missing, {len(bad_hashes)} bad hashes" | ||
| ) | ||
|
|
||
| # (c) cross-refs in evidence_ledger.md | ||
| ledger_refs_missing = [] | ||
| ledger_rows = 0 | ||
| empty_proof_rows = [] | ||
| if "evidence_ledger.md" in names: | ||
| text = zf.read("evidence_ledger.md").decode("utf-8", "replace") | ||
| for line in text.splitlines(): | ||
| if not line.startswith("| proof_envelope "): | ||
| continue | ||
| ledger_rows += 1 | ||
| cols = [c.strip() for c in line.strip("|").split("|")] | ||
| if len(cols) >= 3: | ||
| ref = cols[2].replace("\\", "/") | ||
| if not ref: | ||
| empty_proof_rows.append(line[:80]) | ||
| elif ref not in names and not ref.startswith( | ||
| ("kit_manifest", "honesty_ledger") | ||
| ): | ||
| ledger_refs_missing.append(ref) | ||
| else: | ||
| result["errors"].append("evidence_ledger.md missing") | ||
| cross_ok = ( | ||
| "evidence_ledger.md" in names | ||
| and not ledger_refs_missing | ||
| and not empty_proof_rows | ||
| ) | ||
| result["checks"]["c_cross_refs"] = { | ||
| "ok": cross_ok, | ||
| "rows_seen": ledger_rows, | ||
| "missing_refs": ledger_refs_missing, | ||
| "empty_proof_rows": empty_proof_rows, | ||
| } | ||
| if ledger_refs_missing or empty_proof_rows: | ||
| result["errors"].append("evidence_ledger cross-refs invalid") | ||
|
|
||
| # (d) forbidden extras: every zip file should be referenced | ||
| manifest_paths = {e["path"] for e in files} | ||
| manifest_paths.update({"manifest.json", "manifest.sig"}) | ||
| extras = sorted(names - manifest_paths) | ||
| result["checks"]["d_forbidden_extras"] = { | ||
| "ok": not extras, | ||
| "extras": extras, | ||
| } | ||
| if extras: | ||
| result["errors"].append(f"forbidden extras: {extras}") | ||
|
|
||
| # (e) commit binding | ||
| sha = (manifest.get("git") or {}).get("sha", "") | ||
| commit_ok = False | ||
| if sha: | ||
| cp = subprocess.run( | ||
| ["git", "cat-file", "-e", sha], | ||
| cwd=Path(__file__).resolve().parents[2], | ||
| capture_output=True, | ||
| ) | ||
| commit_ok = cp.returncode == 0 | ||
| result["checks"]["e_commit_binding"] = { | ||
| "ok": commit_ok, | ||
| "sha": sha, | ||
| } | ||
| if not commit_ok: | ||
| result["errors"].append(f"commit binding failed: sha={sha!r}") | ||
|
|
||
| result["ok"] = not result["errors"] | ||
| return result | ||
|
|
||
|
|
||
| def main() -> int: | ||
| p = argparse.ArgumentParser() | ||
| p.add_argument("--bundle", required=True) | ||
| p.add_argument("--key", default=None) | ||
| args = p.parse_args() | ||
| key = (args.key or os.environ.get("HERMES3D_PROOF_KEY") or "").encode("utf-8") | ||
| if not key: | ||
| print("error: HERMES3D_PROOF_KEY (or --key) required", file=sys.stderr) | ||
| return 2 | ||
| out = audit(Path(args.bundle), key) | ||
| print(json.dumps(out, indent=2)) | ||
| return 0 if out["ok"] else 1 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main()) | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,129 @@ | ||||||||||||||||
| #!/usr/bin/env python3 | ||||||||||||||||
| """ | ||||||||||||||||
| Wave B Auditor — negative tests. | ||||||||||||||||
|
|
||||||||||||||||
| Three tampered copies of a bundle, each verified with auditor_verify.py. | ||||||||||||||||
| Each MUST fail. Prints PASS/FAIL for each scenario based on whether the | ||||||||||||||||
| expected failure was detected. | ||||||||||||||||
| """ | ||||||||||||||||
| from __future__ import annotations | ||||||||||||||||
|
|
||||||||||||||||
| import json | ||||||||||||||||
| import os | ||||||||||||||||
| import shutil | ||||||||||||||||
| import subprocess | ||||||||||||||||
| import sys | ||||||||||||||||
| import tempfile | ||||||||||||||||
| import zipfile | ||||||||||||||||
| from pathlib import Path | ||||||||||||||||
|
|
||||||||||||||||
| SCRIPT_DIR = Path(__file__).resolve().parent | ||||||||||||||||
| AUDITOR = SCRIPT_DIR / "auditor_verify.py" | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def run_auditor(bundle: Path, key: str) -> tuple[int, dict]: | ||||||||||||||||
| env = dict(os.environ) | ||||||||||||||||
| env["HERMES3D_PROOF_KEY"] = key | ||||||||||||||||
| cp = subprocess.run( | ||||||||||||||||
| [sys.executable, str(AUDITOR), "--bundle", str(bundle)], | ||||||||||||||||
| env=env, | ||||||||||||||||
| capture_output=True, | ||||||||||||||||
| text=True, | ||||||||||||||||
| ) | ||||||||||||||||
| try: | ||||||||||||||||
| return cp.returncode, json.loads(cp.stdout) | ||||||||||||||||
| except Exception: | ||||||||||||||||
| return cp.returncode, {"raw": cp.stdout, "stderr": cp.stderr} | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def copy_zip_with_changes(src: Path, dst: Path, | ||||||||||||||||
| mutate_file=None, | ||||||||||||||||
| remove_files=None, | ||||||||||||||||
| add_files=None) -> None: | ||||||||||||||||
| remove_files = set(remove_files or []) | ||||||||||||||||
| add_files = add_files or {} | ||||||||||||||||
| mutate_file = mutate_file or {} | ||||||||||||||||
| with zipfile.ZipFile(src, "r") as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout: | ||||||||||||||||
| for name in zin.namelist(): | ||||||||||||||||
| if name in remove_files: | ||||||||||||||||
| continue | ||||||||||||||||
| data = zin.read(name) | ||||||||||||||||
| if name in mutate_file: | ||||||||||||||||
| data = mutate_file[name](data) | ||||||||||||||||
| zout.writestr(name, data) | ||||||||||||||||
| for name, data in add_files.items(): | ||||||||||||||||
| zout.writestr(name, data) | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| def main() -> int: | ||||||||||||||||
| src = Path(sys.argv[1]) | ||||||||||||||||
| key = sys.argv[2] | ||||||||||||||||
|
Comment on lines
+59
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The script lacks validation for command-line arguments. Running it without the required arguments will result in an
Suggested change
|
||||||||||||||||
| tmp = Path(tempfile.mkdtemp(prefix="wave-b-audit-neg-")) | ||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||
| print(f"[neg] tmpdir: {tmp}") | ||||||||||||||||
|
|
||||||||||||||||
| results = [] | ||||||||||||||||
|
|
||||||||||||||||
| # 1. Tamper manifest + re-sign with WRONG key | ||||||||||||||||
| import hashlib, hmac | ||||||||||||||||
| def tamper_manifest(data: bytes) -> bytes: | ||||||||||||||||
| m = json.loads(data.decode("utf-8")) | ||||||||||||||||
| # flip a byte: change the build.run_id | ||||||||||||||||
| m.setdefault("build", {})["run_id"] = "TAMPERED-RUN-ID-XYZ" | ||||||||||||||||
| return json.dumps(m, sort_keys=True, separators=(",", ":")).encode("utf-8") | ||||||||||||||||
|
|
||||||||||||||||
| def resign_with_wrong_key(data: bytes) -> bytes: | ||||||||||||||||
| # Re-sign with a key that is NOT the verification key — this is the | ||||||||||||||||
| # adversary's attempt to forge a signature without knowing the secret. | ||||||||||||||||
| m = json.loads(data.decode("utf-8")) | ||||||||||||||||
| # The body actually being signed is the (tampered) manifest written above. | ||||||||||||||||
| # We don't have it here, so we just put a plausible-looking but wrong sig. | ||||||||||||||||
| wrong = "0" * 64 | ||||||||||||||||
| return json.dumps({"algorithm": "HMAC-SHA256", "value": wrong}, | ||||||||||||||||
| sort_keys=True, separators=(",", ":")).encode("utf-8") | ||||||||||||||||
|
|
||||||||||||||||
| t1 = tmp / "tampered_manifest.zip" | ||||||||||||||||
| copy_zip_with_changes( | ||||||||||||||||
| src, t1, | ||||||||||||||||
| mutate_file={ | ||||||||||||||||
| "manifest.json": tamper_manifest, | ||||||||||||||||
| "manifest.sig": resign_with_wrong_key, | ||||||||||||||||
| }, | ||||||||||||||||
| ) | ||||||||||||||||
| rc, out = run_auditor(t1, key) | ||||||||||||||||
| detected = rc != 0 and any("signature" in e for e in out.get("errors", [])) | ||||||||||||||||
| results.append(("tamper_manifest_wrong_key", detected, out.get("errors"))) | ||||||||||||||||
|
|
||||||||||||||||
| # 2. Remove a listed file from the zip | ||||||||||||||||
| with zipfile.ZipFile(src, "r") as zin: | ||||||||||||||||
| manifest = json.loads(zin.read("manifest.json").decode("utf-8")) | ||||||||||||||||
| victim = next(e["path"] for e in manifest["files"] | ||||||||||||||||
| if e["path"] not in ("manifest.json", "manifest.sig")) | ||||||||||||||||
| t2 = tmp / "removed_file.zip" | ||||||||||||||||
| copy_zip_with_changes(src, t2, remove_files={victim}) | ||||||||||||||||
| rc, out = run_auditor(t2, key) | ||||||||||||||||
| detected = rc != 0 and any("missing" in e or "integrity" in e for e in out.get("errors", [])) | ||||||||||||||||
| results.append((f"remove_file::{victim}", detected, out.get("errors"))) | ||||||||||||||||
|
|
||||||||||||||||
| # 3. Add an extra file not in manifest | ||||||||||||||||
| t3 = tmp / "extra_file.zip" | ||||||||||||||||
| copy_zip_with_changes(src, t3, add_files={"smuggled.txt": b"hello"}) | ||||||||||||||||
| rc, out = run_auditor(t3, key) | ||||||||||||||||
| detected = rc != 0 and any("forbidden" in e or "extras" in e for e in out.get("errors", [])) | ||||||||||||||||
| results.append(("add_extra_file::smuggled.txt", detected, out.get("errors"))) | ||||||||||||||||
|
|
||||||||||||||||
| summary = [] | ||||||||||||||||
| all_pass = True | ||||||||||||||||
| for name, detected, errors in results: | ||||||||||||||||
| status = "PASS (expected failure detected)" if detected else "FAIL (vulnerability)" | ||||||||||||||||
| if not detected: | ||||||||||||||||
| all_pass = False | ||||||||||||||||
| summary.append({"scenario": name, "status": status, "errors": errors}) | ||||||||||||||||
|
|
||||||||||||||||
| print(json.dumps(summary, indent=2)) | ||||||||||||||||
| # Cleanup | ||||||||||||||||
| shutil.rmtree(tmp, ignore_errors=True) | ||||||||||||||||
| return 0 if all_pass else 1 | ||||||||||||||||
|
|
||||||||||||||||
|
|
||||||||||||||||
| if __name__ == "__main__": | ||||||||||||||||
| sys.exit(main()) | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The script will crash with a
KeyErrorifmanifest.jsonormanifest.sigare missing from the zip file. It's safer to check for their existence innamesbefore attempting to read them, allowing the function to return a structured error report instead of an unhandled exception.