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