Commit 468ebcc4ef

468ebcc4ef57de0fa0a14d342ae8013001bc8ae6

parent: bdab697b73

Unsigned

cmc <hello@cleberg.net> · 2026-08-09 01:32 UTC

Pin CI actions to SHA, refactor _cmd_verify, tidy CLI/tests/shell

Layout: unified · split

.github/workflows/release.yml +2 −2
@@ -11,7 +11,7 @@ jobs:
1111 steps:
1212 - uses: actions/checkout@v5
1313 - name: Install uv
14 uses: astral-sh/setup-uv@v6
14 uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
1515 - name: Build
1616 run: uv build
1717 - name: Check
@@ -33,4 +33,4 @@ jobs:
3333 name: dist
3434 path: dist/
3535 - name: Publish to PyPI
36 uses: pypa/gh-action-pypi-publish@release/v1
36 uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
evidence_seal/cli.py +34 −25
@@ -18,6 +18,8 @@ from .manifest import (
1818# Exit codes: 0 = intact/valid, 1 = tamper/verification failure, 2 = usage error.
1919OK, FAILED, USAGE = 0, 1, 2
2020
21_OUT_HELP = "write here instead of overwriting the manifest"
22
2123
2224def _default_manifest_path(directory: Path) -> Path:
2325 return directory.parent / f"{directory.name}.manifest.json"
@@ -79,6 +81,28 @@ def _cmd_seal(args) -> int:
7981 return OK
8082
8183
84def _print_drift(result) -> None:
85 for path in result.modified:
86 print(f" MODIFIED {path}")
87 for path in result.added:
88 print(f" ADDED {path}")
89 for path in result.removed:
90 print(f" REMOVED {path}")
91 if not result.id_ok:
92 print(" MANIFEST id does not re-derive — the manifest itself was altered")
93 if not result.root_ok:
94 print(" MANIFEST Merkle root does not match the file list")
95
96
97def _verify_timestamp_cli(manifest: dict, tsa_cert: str | None) -> bool:
98 from .timestamp import verify_timestamp
99
100 cert = Path(tsa_cert).read_bytes() if tsa_cert else None
101 ts_ok, ts_message = verify_timestamp(manifest, tsa_cert=cert)
102 print(f" timestamp {'OK' if ts_ok else 'FAIL'}: {ts_message}")
103 return ts_ok
104
105
82106def _cmd_verify(args) -> int:
83107 directory = Path(args.directory)
84108 manifest_path = Path(args.manifest) if args.manifest else _default_manifest_path(directory)
@@ -95,16 +119,7 @@ def _cmd_verify(args) -> int:
95119 exclude=_manifest_exclude(directory, manifest_path),
96120 )
97121
98 for path in result.modified:
99 print(f" MODIFIED {path}")
100 for path in result.added:
101 print(f" ADDED {path}")
102 for path in result.removed:
103 print(f" REMOVED {path}")
104 if not result.id_ok:
105 print(" MANIFEST id does not re-derive — the manifest itself was altered")
106 if not result.root_ok:
107 print(" MANIFEST Merkle root does not match the file list")
122 _print_drift(result)
108123
109124 status = OK
110125 if not result.intact:
@@ -115,14 +130,8 @@ def _cmd_verify(args) -> int:
115130 status = FAILED
116131
117132 # Verify an embedded timestamp when present.
118 if manifest.get("timestamp"):
119 from .timestamp import verify_timestamp
120
121 cert = Path(args.tsa_cert).read_bytes() if args.tsa_cert else None
122 ts_ok, ts_message = verify_timestamp(manifest, tsa_cert=cert)
123 print(f" timestamp {'OK' if ts_ok else 'FAIL'}: {ts_message}")
124 if not ts_ok:
125 status = FAILED
133 if manifest.get("timestamp") and not _verify_timestamp_cli(manifest, args.tsa_cert):
134 status = FAILED
126135
127136 if status == OK:
128137 print(f"intact — {result.checked} files match the seal")
@@ -175,7 +184,7 @@ def _cmd_sign(args) -> int:
175184 try:
176185 manifest = load_manifest(args.manifest)
177186 signed = sign_manifest(manifest, args.key)
178 except (RuntimeError, FileNotFoundError, ValueError, OSError) as exc:
187 except (RuntimeError, ValueError, OSError) as exc:
179188 print(f"error: {exc}", file=sys.stderr)
180189 return USAGE
181190 write_manifest(signed, args.out or args.manifest)
@@ -189,7 +198,7 @@ def _cmd_ts_request(args) -> int:
189198 try:
190199 manifest = load_manifest(args.manifest)
191200 request = build_request(manifest["id"])
192 except (RuntimeError, FileNotFoundError, ValueError, KeyError, OSError) as exc:
201 except (RuntimeError, ValueError, KeyError, OSError) as exc:
193202 print(f"error: {exc}", file=sys.stderr)
194203 return USAGE
195204 out = args.out or f"{args.manifest}.tsq"
@@ -210,7 +219,7 @@ def _cmd_ts_apply(args) -> int:
210219 try:
211220 manifest = load_manifest(args.manifest)
212221 stamped = apply_timestamp(manifest, load_der(args.token))
213 except (RuntimeError, FileNotFoundError, ValueError, OSError) as exc:
222 except (RuntimeError, ValueError, OSError) as exc:
214223 print(f"error: {exc}", file=sys.stderr)
215224 return USAGE
216225 write_manifest(stamped, args.out or args.manifest)
@@ -228,7 +237,7 @@ def _cmd_ts_submit(args) -> int:
228237 except ValueError as exc:
229238 print(f"error: {exc}", file=sys.stderr)
230239 return FAILED
231 except (RuntimeError, FileNotFoundError, KeyError, OSError) as exc:
240 except (RuntimeError, KeyError, OSError) as exc:
232241 print(f"error: {exc}", file=sys.stderr)
233242 return USAGE
234243 write_manifest(stamped, args.out or args.manifest)
@@ -288,7 +297,7 @@ def _build_parser() -> argparse.ArgumentParser:
288297 p_sign = sub.add_parser("sign", help="sign an existing manifest")
289298 p_sign.add_argument("manifest")
290299 p_sign.add_argument("--key", required=True, metavar="PRIVATE_KEY")
291 p_sign.add_argument("--out", help="write here instead of overwriting the manifest")
300 p_sign.add_argument("--out", help=_OUT_HELP)
292301 p_sign.set_defaults(func=_cmd_sign)
293302
294303 _add_timestamp_commands(sub)
@@ -307,14 +316,14 @@ def _add_timestamp_commands(sub) -> None:
307316 p_apply = ts.add_parser("apply", help="bind a TSA response/token into the manifest")
308317 p_apply.add_argument("manifest")
309318 p_apply.add_argument("--token", required=True, metavar="TSR", help="TSA response or token (DER)")
310 p_apply.add_argument("--out", help="write here instead of overwriting the manifest")
319 p_apply.add_argument("--out", help=_OUT_HELP)
311320 p_apply.set_defaults(func=_cmd_ts_apply)
312321
313322 p_submit = ts.add_parser("submit", help="request, POST to a TSA, and bind in one step")
314323 p_submit.add_argument("manifest")
315324 p_submit.add_argument("--tsa", required=True, metavar="URL", help="RFC 3161 TSA endpoint")
316325 p_submit.add_argument("--timeout", type=float, default=30.0, help="network timeout (seconds)")
317 p_submit.add_argument("--out", help="write here instead of overwriting the manifest")
326 p_submit.add_argument("--out", help=_OUT_HELP)
318327 p_submit.set_defaults(func=_cmd_ts_submit)
319328
320329 p_tsv = ts.add_parser("verify", help="verify the manifest's embedded timestamp")
scripts/e2e.sh +2 −2
@@ -17,7 +17,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
1717REPO="$(dirname "$SCRIPT_DIR")"
1818
1919# Prefer the project venv; fall back to whatever is on PATH.
20if [ -x "$REPO/.venv/bin/python" ]; then
20if [[ -x "$REPO/.venv/bin/python" ]]; then
2121 PY="$REPO/.venv/bin/python"
2222else
2323 PY="$(command -v python3 || command -v python)"
@@ -37,7 +37,7 @@ assert_exit() {
3737 printf '$ %s\n' "$*"
3838 eval "$*" >"$W/out" 2>&1; local rc=$?
3939 sed 's/^/ /' "$W/out"
40 if [ "$rc" = "$exp" ]; then
40 if [[ "$rc" == "$exp" ]]; then
4141 printf ' \033[32m✓ PASS\033[0m — %s (exit %s)\n' "$label" "$rc"; pass=$((pass+1))
4242 else
4343 printf ' \033[31m✗ FAIL\033[0m — %s (exit %s, expected %s)\n' "$label" "$rc" "$exp"; fail=$((fail+1))
tests/test_cli.py +2 −1
@@ -85,7 +85,8 @@ def test_timestamp_request_apply_verify(pkg, tmp_path):
8585
8686 req = tmp_path / "m.tsq"
8787 assert main(["timestamp", "request", str(manifest), "--out", str(req)]) == OK
88 assert req.exists() and req.stat().st_size > 0
88 assert req.exists()
89 assert req.stat().st_size > 0
8990
9091 manifest_id = json.loads(manifest.read_text())["id"]
9192 tsr = tmp_path / "resp.tsr"
tests/test_timestamp.py +4 −2
@@ -196,8 +196,9 @@ def test_apply_accepts_bare_token(manifest):
196196
197197def test_apply_refuses_token_for_other_id(manifest):
198198 wrong = "0" * 64
199 token = issue_token(wrong)
199200 with pytest.raises(ValueError, match="does not timestamp this manifest"):
200 apply_timestamp(manifest, issue_token(wrong))
201 apply_timestamp(manifest, token)
201202
202203
203204def test_timestamp_does_not_change_manifest_id(manifest):
@@ -232,7 +233,8 @@ def test_full_signature_verifies(manifest):
232233 token, cert_pem = issue_signed_token(manifest["id"])
233234 ok, message = verify_token_signature(token, cert_pem)
234235 assert ok
235 assert "valid" in message and "Test TSA" in message
236 assert "valid" in message
237 assert "Test TSA" in message
236238
237239
238240def test_full_signature_via_verify_timestamp(manifest):