audit-labs/evidence-seal
Tamper-evident seals and chain of custody for audit evidence.
clone: git clone https://gitbay.org/audit-labs/evidence-seal.git
468ebcc4ef57de0fa0a14d342ae8013001bc8ae6
unsigned
author: Christian Cleberg <hello@cleberg.net> · 2026-08-09T01:32:54Z
.github/workflows/release.yml | 4 +-- evidence_seal/cli.py | 59 +++++++++++++++++++++++++------------------ scripts/e2e.sh | 4 +-- tests/test_cli.py | 3 ++- tests/test_timestamp.py | 6 +++-- 5 files changed, 44 insertions(+), 32 deletions(-) @@ -11,7 +11,7 @@ jobs: steps: - uses: actions/checkout@v5 - name: Install uv - uses: astral-sh/setup-uv@v6 + uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6 - name: Build run: uv build - name: Check @@ -33,4 +33,4 @@ jobs: name: dist path: dist/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 + uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1 @@ -18,6 +18,8 @@ from .manifest import ( # Exit codes: 0 = intact/valid, 1 = tamper/verification failure, 2 = usage error. OK, FAILED, USAGE = 0, 1, 2 +_OUT_HELP = "write here instead of overwriting the manifest" + def _default_manifest_path(directory: Path) -> Path: return directory.parent / f"{directory.name}.manifest.json" @@ -79,6 +81,28 @@ def _cmd_seal(args) -> int: return OK +def _print_drift(result) -> None: + for path in result.modified: + print(f" MODIFIED {path}") + for path in result.added: + print(f" ADDED {path}") + for path in result.removed: + print(f" REMOVED {path}") + if not result.id_ok: + print(" MANIFEST id does not re-derive — the manifest itself was altered") + if not result.root_ok: + print(" MANIFEST Merkle root does not match the file list") + + +def _verify_timestamp_cli(manifest: dict, tsa_cert: str | None) -> bool: + from .timestamp import verify_timestamp + + cert = Path(tsa_cert).read_bytes() if tsa_cert else None + ts_ok, ts_message = verify_timestamp(manifest, tsa_cert=cert) + print(f" timestamp {'OK' if ts_ok else 'FAIL'}: {ts_message}") + return ts_ok + + def _cmd_verify(args) -> int: directory = Path(args.directory) manifest_path = Path(args.manifest) if args.manifest else _default_manifest_path(directory) @@ -95,16 +119,7 @@ def _cmd_verify(args) -> int: exclude=_manifest_exclude(directory, manifest_path), ) - for path in result.modified: - print(f" MODIFIED {path}") - for path in result.added: - print(f" ADDED {path}") - for path in result.removed: - print(f" REMOVED {path}") - if not result.id_ok: - print(" MANIFEST id does not re-derive — the manifest itself was altered") - if not result.root_ok: - print(" MANIFEST Merkle root does not match the file list") + _print_drift(result) status = OK if not result.intact: @@ -115,14 +130,8 @@ def _cmd_verify(args) -> int: status = FAILED # Verify an embedded timestamp when present. - if manifest.get("timestamp"): - from .timestamp import verify_timestamp - - cert = Path(args.tsa_cert).read_bytes() if args.tsa_cert else None - ts_ok, ts_message = verify_timestamp(manifest, tsa_cert=cert) - print(f" timestamp {'OK' if ts_ok else 'FAIL'}: {ts_message}") - if not ts_ok: - status = FAILED + if manifest.get("timestamp") and not _verify_timestamp_cli(manifest, args.tsa_cert): + status = FAILED if status == OK: print(f"intact — {result.checked} files match the seal") @@ -175,7 +184,7 @@ def _cmd_sign(args) -> int: try: manifest = load_manifest(args.manifest) signed = sign_manifest(manifest, args.key) - except (RuntimeError, FileNotFoundError, ValueError, OSError) as exc: + except (RuntimeError, ValueError, OSError) as exc: print(f"error: {exc}", file=sys.stderr) return USAGE write_manifest(signed, args.out or args.manifest) @@ -189,7 +198,7 @@ def _cmd_ts_request(args) -> int: try: manifest = load_manifest(args.manifest) request = build_request(manifest["id"]) - except (RuntimeError, FileNotFoundError, ValueError, KeyError, OSError) as exc: + except (RuntimeError, ValueError, KeyError, OSError) as exc: print(f"error: {exc}", file=sys.stderr) return USAGE out = args.out or f"{args.manifest}.tsq" @@ -210,7 +219,7 @@ def _cmd_ts_apply(args) -> int: try: manifest = load_manifest(args.manifest) stamped = apply_timestamp(manifest, load_der(args.token)) - except (RuntimeError, FileNotFoundError, ValueError, OSError) as exc: + except (RuntimeError, ValueError, OSError) as exc: print(f"error: {exc}", file=sys.stderr) return USAGE write_manifest(stamped, args.out or args.manifest) @@ -228,7 +237,7 @@ def _cmd_ts_submit(args) -> int: except ValueError as exc: print(f"error: {exc}", file=sys.stderr) return FAILED - except (RuntimeError, FileNotFoundError, KeyError, OSError) as exc: + except (RuntimeError, KeyError, OSError) as exc: print(f"error: {exc}", file=sys.stderr) return USAGE write_manifest(stamped, args.out or args.manifest) @@ -288,7 +297,7 @@ def _build_parser() -> argparse.ArgumentParser: p_sign = sub.add_parser("sign", help="sign an existing manifest") p_sign.add_argument("manifest") p_sign.add_argument("--key", required=True, metavar="PRIVATE_KEY") - p_sign.add_argument("--out", help="write here instead of overwriting the manifest") + p_sign.add_argument("--out", help=_OUT_HELP) p_sign.set_defaults(func=_cmd_sign) _add_timestamp_commands(sub) @@ -307,14 +316,14 @@ def _add_timestamp_commands(sub) -> None: p_apply = ts.add_parser("apply", help="bind a TSA response/token into the manifest") p_apply.add_argument("manifest") p_apply.add_argument("--token", required=True, metavar="TSR", help="TSA response or token (DER)") - p_apply.add_argument("--out", help="write here instead of overwriting the manifest") + p_apply.add_argument("--out", help=_OUT_HELP) p_apply.set_defaults(func=_cmd_ts_apply) p_submit = ts.add_parser("submit", help="request, POST to a TSA, and bind in one step") p_submit.add_argument("manifest") p_submit.add_argument("--tsa", required=True, metavar="URL", help="RFC 3161 TSA endpoint") p_submit.add_argument("--timeout", type=float, default=30.0, help="network timeout (seconds)") - p_submit.add_argument("--out", help="write here instead of overwriting the manifest") + p_submit.add_argument("--out", help=_OUT_HELP) p_submit.set_defaults(func=_cmd_ts_submit) p_tsv = ts.add_parser("verify", help="verify the manifest's embedded timestamp") @@ -17,7 +17,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO="$(dirname "$SCRIPT_DIR")" # Prefer the project venv; fall back to whatever is on PATH. -if [ -x "$REPO/.venv/bin/python" ]; then +if [[ -x "$REPO/.venv/bin/python" ]]; then PY="$REPO/.venv/bin/python" else PY="$(command -v python3 || command -v python)" @@ -37,7 +37,7 @@ assert_exit() { printf '$ %s\n' "$*" eval "$*" >"$W/out" 2>&1; local rc=$? sed 's/^/ /' "$W/out" - if [ "$rc" = "$exp" ]; then + if [[ "$rc" == "$exp" ]]; then printf ' \033[32m✓ PASS\033[0m — %s (exit %s)\n' "$label" "$rc"; pass=$((pass+1)) else printf ' \033[31m✗ FAIL\033[0m — %s (exit %s, expected %s)\n' "$label" "$rc" "$exp"; fail=$((fail+1)) @@ -85,7 +85,8 @@ def test_timestamp_request_apply_verify(pkg, tmp_path): req = tmp_path / "m.tsq" assert main(["timestamp", "request", str(manifest), "--out", str(req)]) == OK - assert req.exists() and req.stat().st_size > 0 + assert req.exists() + assert req.stat().st_size > 0 manifest_id = json.loads(manifest.read_text())["id"] tsr = tmp_path / "resp.tsr" @@ -196,8 +196,9 @@ def test_apply_accepts_bare_token(manifest): def test_apply_refuses_token_for_other_id(manifest): wrong = "0" * 64 + token = issue_token(wrong) with pytest.raises(ValueError, match="does not timestamp this manifest"): - apply_timestamp(manifest, issue_token(wrong)) + apply_timestamp(manifest, token) def test_timestamp_does_not_change_manifest_id(manifest): @@ -232,7 +233,8 @@ def test_full_signature_verifies(manifest): token, cert_pem = issue_signed_token(manifest["id"]) ok, message = verify_token_signature(token, cert_pem) assert ok - assert "valid" in message and "Test TSA" in message + assert "valid" in message + assert "Test TSA" in message def test_full_signature_via_verify_timestamp(manifest):