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

Pin CI actions to SHA, refactor _cmd_verify, tidy CLI/tests/shell
 .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(-)

diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index dd9ff4c..b9f5577 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/evidence_seal/cli.py b/evidence_seal/cli.py
index de7c630..7988d20 100644
--- a/evidence_seal/cli.py
+++ b/evidence_seal/cli.py
@@ -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")
diff --git a/scripts/e2e.sh b/scripts/e2e.sh
index e4ee2e6..b67845a 100755
--- a/scripts/e2e.sh
+++ b/scripts/e2e.sh
@@ -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))
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 6d3ebb8..0ce99f7 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -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"
diff --git a/tests/test_timestamp.py b/tests/test_timestamp.py
index 5e66536..9c01ce6 100644
--- a/tests/test_timestamp.py
+++ b/tests/test_timestamp.py
@@ -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):