| @@ -18,6 +18,8 @@ from .manifest import ( |
| 18 | # Exit codes: 0 = intact/valid, 1 = tamper/verification failure, 2 = usage error. |
18 | # Exit codes: 0 = intact/valid, 1 = tamper/verification failure, 2 = usage error. |
| 19 | OK, FAILED, USAGE = 0, 1, 2 |
19 | OK, FAILED, USAGE = 0, 1, 2 |
| 20 | |
20 | |
| |
21 | _OUT_HELP = "write here instead of overwriting the manifest" |
| |
22 | |
| 21 | |
23 | |
| 22 | def _default_manifest_path(directory: Path) -> Path: |
24 | def _default_manifest_path(directory: Path) -> Path: |
| 23 | return directory.parent / f"{directory.name}.manifest.json" |
25 | return directory.parent / f"{directory.name}.manifest.json" |
| @@ -79,6 +81,28 @@ def _cmd_seal(args) -> int: |
| 79 | return OK |
81 | return OK |
| 80 | |
82 | |
| 81 | |
83 | |
| |
84 | def _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 | |
| |
97 | def _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 | |
| 82 | def _cmd_verify(args) -> int: |
106 | def _cmd_verify(args) -> int: |
| 83 | directory = Path(args.directory) |
107 | directory = Path(args.directory) |
| 84 | manifest_path = Path(args.manifest) if args.manifest else _default_manifest_path(directory) |
108 | manifest_path = Path(args.manifest) if args.manifest else _default_manifest_path(directory) |
| @@ -95,16 +119,7 @@ def _cmd_verify(args) -> int: |
| 95 | exclude=_manifest_exclude(directory, manifest_path), |
119 | exclude=_manifest_exclude(directory, manifest_path), |
| 96 | ) |
120 | ) |
| 97 | |
121 | |
| 98 | for path in result.modified: |
122 | _print_drift(result) |
| 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") |
| |
| 108 | |
123 | |
| 109 | status = OK |
124 | status = OK |
| 110 | if not result.intact: |
125 | if not result.intact: |
| @@ -115,14 +130,8 @@ def _cmd_verify(args) -> int: |
| 115 | status = FAILED |
130 | status = FAILED |
| 116 | |
131 | |
| 117 | # Verify an embedded timestamp when present. |
132 | # Verify an embedded timestamp when present. |
| 118 | if manifest.get("timestamp"): |
133 | if manifest.get("timestamp") and not _verify_timestamp_cli(manifest, args.tsa_cert): |
| 119 | from .timestamp import verify_timestamp |
134 | status = FAILED |
| 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 |
| |
| 126 | |
135 | |
| 127 | if status == OK: |
136 | if status == OK: |
| 128 | print(f"intact — {result.checked} files match the seal") |
137 | print(f"intact — {result.checked} files match the seal") |
| @@ -175,7 +184,7 @@ def _cmd_sign(args) -> int: |
| 175 | try: |
184 | try: |
| 176 | manifest = load_manifest(args.manifest) |
185 | manifest = load_manifest(args.manifest) |
| 177 | signed = sign_manifest(manifest, args.key) |
186 | signed = sign_manifest(manifest, args.key) |
| 178 | except (RuntimeError, FileNotFoundError, ValueError, OSError) as exc: |
187 | except (RuntimeError, ValueError, OSError) as exc: |
| 179 | print(f"error: {exc}", file=sys.stderr) |
188 | print(f"error: {exc}", file=sys.stderr) |
| 180 | return USAGE |
189 | return USAGE |
| 181 | write_manifest(signed, args.out or args.manifest) |
190 | write_manifest(signed, args.out or args.manifest) |
| @@ -189,7 +198,7 @@ def _cmd_ts_request(args) -> int: |
| 189 | try: |
198 | try: |
| 190 | manifest = load_manifest(args.manifest) |
199 | manifest = load_manifest(args.manifest) |
| 191 | request = build_request(manifest["id"]) |
200 | request = build_request(manifest["id"]) |
| 192 | except (RuntimeError, FileNotFoundError, ValueError, KeyError, OSError) as exc: |
201 | except (RuntimeError, ValueError, KeyError, OSError) as exc: |
| 193 | print(f"error: {exc}", file=sys.stderr) |
202 | print(f"error: {exc}", file=sys.stderr) |
| 194 | return USAGE |
203 | return USAGE |
| 195 | out = args.out or f"{args.manifest}.tsq" |
204 | out = args.out or f"{args.manifest}.tsq" |
| @@ -210,7 +219,7 @@ def _cmd_ts_apply(args) -> int: |
| 210 | try: |
219 | try: |
| 211 | manifest = load_manifest(args.manifest) |
220 | manifest = load_manifest(args.manifest) |
| 212 | stamped = apply_timestamp(manifest, load_der(args.token)) |
221 | stamped = apply_timestamp(manifest, load_der(args.token)) |
| 213 | except (RuntimeError, FileNotFoundError, ValueError, OSError) as exc: |
222 | except (RuntimeError, ValueError, OSError) as exc: |
| 214 | print(f"error: {exc}", file=sys.stderr) |
223 | print(f"error: {exc}", file=sys.stderr) |
| 215 | return USAGE |
224 | return USAGE |
| 216 | write_manifest(stamped, args.out or args.manifest) |
225 | write_manifest(stamped, args.out or args.manifest) |
| @@ -228,7 +237,7 @@ def _cmd_ts_submit(args) -> int: |
| 228 | except ValueError as exc: |
237 | except ValueError as exc: |
| 229 | print(f"error: {exc}", file=sys.stderr) |
238 | print(f"error: {exc}", file=sys.stderr) |
| 230 | return FAILED |
239 | return FAILED |
| 231 | except (RuntimeError, FileNotFoundError, KeyError, OSError) as exc: |
240 | except (RuntimeError, KeyError, OSError) as exc: |
| 232 | print(f"error: {exc}", file=sys.stderr) |
241 | print(f"error: {exc}", file=sys.stderr) |
| 233 | return USAGE |
242 | return USAGE |
| 234 | write_manifest(stamped, args.out or args.manifest) |
243 | write_manifest(stamped, args.out or args.manifest) |
| @@ -288,7 +297,7 @@ def _build_parser() -> argparse.ArgumentParser: |
| 288 | p_sign = sub.add_parser("sign", help="sign an existing manifest") |
297 | p_sign = sub.add_parser("sign", help="sign an existing manifest") |
| 289 | p_sign.add_argument("manifest") |
298 | p_sign.add_argument("manifest") |
| 290 | p_sign.add_argument("--key", required=True, metavar="PRIVATE_KEY") |
299 | 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) |
| 292 | p_sign.set_defaults(func=_cmd_sign) |
301 | p_sign.set_defaults(func=_cmd_sign) |
| 293 | |
302 | |
| 294 | _add_timestamp_commands(sub) |
303 | _add_timestamp_commands(sub) |
| @@ -307,14 +316,14 @@ def _add_timestamp_commands(sub) -> None: |
| 307 | p_apply = ts.add_parser("apply", help="bind a TSA response/token into the manifest") |
316 | p_apply = ts.add_parser("apply", help="bind a TSA response/token into the manifest") |
| 308 | p_apply.add_argument("manifest") |
317 | p_apply.add_argument("manifest") |
| 309 | p_apply.add_argument("--token", required=True, metavar="TSR", help="TSA response or token (DER)") |
318 | 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) |
| 311 | p_apply.set_defaults(func=_cmd_ts_apply) |
320 | p_apply.set_defaults(func=_cmd_ts_apply) |
| 312 | |
321 | |
| 313 | p_submit = ts.add_parser("submit", help="request, POST to a TSA, and bind in one step") |
322 | p_submit = ts.add_parser("submit", help="request, POST to a TSA, and bind in one step") |
| 314 | p_submit.add_argument("manifest") |
323 | p_submit.add_argument("manifest") |
| 315 | p_submit.add_argument("--tsa", required=True, metavar="URL", help="RFC 3161 TSA endpoint") |
324 | p_submit.add_argument("--tsa", required=True, metavar="URL", help="RFC 3161 TSA endpoint") |
| 316 | p_submit.add_argument("--timeout", type=float, default=30.0, help="network timeout (seconds)") |
325 | 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) |
| 318 | p_submit.set_defaults(func=_cmd_ts_submit) |
327 | p_submit.set_defaults(func=_cmd_ts_submit) |
| 319 | |
328 | |
| 320 | p_tsv = ts.add_parser("verify", help="verify the manifest's embedded timestamp") |
329 | p_tsv = ts.add_parser("verify", help="verify the manifest's embedded timestamp") |