actproof 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- actproof/__init__.py +234 -0
- actproof/anchor.py +586 -0
- actproof/canonical.py +369 -0
- actproof/catalogue.py +1031 -0
- actproof/cli.py +593 -0
- actproof/manifest.py +728 -0
- actproof/receipt.py +678 -0
- actproof/signers/__init__.py +89 -0
- actproof/signers/google_kms.py +392 -0
- actproof/signers/interface.py +298 -0
- actproof/signers/mnemonic.py +153 -0
- actproof/timestamp.py +527 -0
- actproof/verify.py +683 -0
- actproof-0.2.0.dist-info/METADATA +295 -0
- actproof-0.2.0.dist-info/RECORD +18 -0
- actproof-0.2.0.dist-info/WHEEL +4 -0
- actproof-0.2.0.dist-info/entry_points.txt +2 -0
- actproof-0.2.0.dist-info/licenses/LICENSE +21 -0
actproof/cli.py
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Deyan Paroushev
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""
|
|
4
|
+
actproof command-line interface.
|
|
5
|
+
|
|
6
|
+
Three subcommands wired to the actproof Python API:
|
|
7
|
+
|
|
8
|
+
* ``actproof anchor`` - read a manifest JSON file, optionally acquire an
|
|
9
|
+
RFC 3161 timestamp, anchor the manifest hash to the Algorand ledger,
|
|
10
|
+
write the resulting receipt.
|
|
11
|
+
|
|
12
|
+
* ``actproof verify`` - read a receipt JSON file and run the six checks
|
|
13
|
+
defined in ``actproof.verify``. Prints per-check status. Exits
|
|
14
|
+
non-zero if any check fails.
|
|
15
|
+
|
|
16
|
+
* ``actproof validate`` - read a manifest JSON file and validate against
|
|
17
|
+
an actproof-events catalogue. Prints any validation issues. Exits
|
|
18
|
+
non-zero if the manifest does not conform.
|
|
19
|
+
|
|
20
|
+
Security note on mnemonics
|
|
21
|
+
--------------------------
|
|
22
|
+
|
|
23
|
+
For testing/demo anchoring, the mnemonic is read from the
|
|
24
|
+
``ACTPROOF_MNEMONIC`` environment variable, NEVER from command-line
|
|
25
|
+
arguments. Command-line arguments leak to shell history (``~/.bash_history``,
|
|
26
|
+
``~/.zsh_history``), to the kernel's process table (visible via ``ps aux``
|
|
27
|
+
to other users on shared systems), to docker layer metadata, and to log
|
|
28
|
+
aggregation systems. The env var path is the lower-risk channel and the
|
|
29
|
+
only path supported.
|
|
30
|
+
|
|
31
|
+
Production anchoring uses GCP KMS via ``--kms-resource``; the key never
|
|
32
|
+
leaves the HSM. AWS users implement their own signer subclass (see
|
|
33
|
+
``actproof.signers.interface``).
|
|
34
|
+
|
|
35
|
+
Exit codes
|
|
36
|
+
----------
|
|
37
|
+
|
|
38
|
+
* 0 - success
|
|
39
|
+
* 1 - operation failed (verification failed, anchor failed, validation
|
|
40
|
+
found issues)
|
|
41
|
+
* 2 - usage error (missing file, bad arguments, etc.) - Click default
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
from __future__ import annotations
|
|
45
|
+
|
|
46
|
+
import base64
|
|
47
|
+
import json
|
|
48
|
+
import logging
|
|
49
|
+
import os
|
|
50
|
+
import sys
|
|
51
|
+
import warnings
|
|
52
|
+
from pathlib import Path
|
|
53
|
+
from typing import Any, Optional
|
|
54
|
+
|
|
55
|
+
import click
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ─────────────────────────────────────────────────────────────────
|
|
59
|
+
# VERSION (read lazily so the CLI works even before actproof is fully
|
|
60
|
+
# importable, e.g. for `actproof --version` early-exit cases)
|
|
61
|
+
# ─────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def _get_version() -> str:
|
|
64
|
+
try:
|
|
65
|
+
from actproof import __version__
|
|
66
|
+
return __version__
|
|
67
|
+
except Exception: # noqa: BLE001
|
|
68
|
+
return "unknown"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ─────────────────────────────────────────────────────────────────
|
|
72
|
+
# LOGGING SETUP
|
|
73
|
+
# ─────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def _setup_logging(verbose: bool) -> None:
|
|
76
|
+
"""Configure logging level based on the --verbose flag."""
|
|
77
|
+
level = logging.DEBUG if verbose else logging.WARNING
|
|
78
|
+
logging.basicConfig(
|
|
79
|
+
level=level,
|
|
80
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
81
|
+
stream=sys.stderr,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ─────────────────────────────────────────────────────────────────
|
|
86
|
+
# THE MAIN GROUP
|
|
87
|
+
# ─────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
@click.group()
|
|
90
|
+
@click.version_option(version=_get_version(), prog_name="actproof")
|
|
91
|
+
@click.option(
|
|
92
|
+
"-v", "--verbose",
|
|
93
|
+
is_flag=True,
|
|
94
|
+
help="Enable DEBUG logging to stderr.",
|
|
95
|
+
)
|
|
96
|
+
@click.pass_context
|
|
97
|
+
def main(ctx: click.Context, verbose: bool) -> None:
|
|
98
|
+
"""actproof: anchor signed JSON manifests; verify anchored receipts.
|
|
99
|
+
|
|
100
|
+
See ``actproof <command> --help`` for per-command help. The three
|
|
101
|
+
commands are ``anchor`` (commit), ``verify`` (audit), and ``validate``
|
|
102
|
+
(lint a manifest against a catalogue).
|
|
103
|
+
"""
|
|
104
|
+
_setup_logging(verbose)
|
|
105
|
+
ctx.ensure_object(dict)
|
|
106
|
+
ctx.obj["verbose"] = verbose
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ─────────────────────────────────────────────────────────────────
|
|
110
|
+
# COMMAND: validate
|
|
111
|
+
# ─────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
@main.command()
|
|
114
|
+
@click.argument(
|
|
115
|
+
"manifest_path",
|
|
116
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
117
|
+
)
|
|
118
|
+
@click.option(
|
|
119
|
+
"--catalogue", "catalogue_path",
|
|
120
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
121
|
+
required=True,
|
|
122
|
+
help="Path to the actproof-events catalogue acts/ directory.",
|
|
123
|
+
)
|
|
124
|
+
@click.option(
|
|
125
|
+
"--git-commit",
|
|
126
|
+
required=True,
|
|
127
|
+
help=(
|
|
128
|
+
"Git commit SHA at which the catalogue was loaded. "
|
|
129
|
+
"Recorded in the validation context for reproducibility."
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
@click.option(
|
|
133
|
+
"--source-uri",
|
|
134
|
+
required=True,
|
|
135
|
+
help=(
|
|
136
|
+
"Source URI of the catalogue "
|
|
137
|
+
"(e.g. https://github.com/deyan-paroushev/actproof-events)."
|
|
138
|
+
),
|
|
139
|
+
)
|
|
140
|
+
@click.option(
|
|
141
|
+
"--json", "as_json",
|
|
142
|
+
is_flag=True,
|
|
143
|
+
help="Output validation result as JSON for scripting.",
|
|
144
|
+
)
|
|
145
|
+
def validate(
|
|
146
|
+
manifest_path: Path,
|
|
147
|
+
catalogue_path: Path,
|
|
148
|
+
git_commit: str,
|
|
149
|
+
source_uri: str,
|
|
150
|
+
as_json: bool,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Validate a manifest against an actproof-events catalogue.
|
|
153
|
+
|
|
154
|
+
Reads MANIFEST_PATH, loads the catalogue at the specified git commit,
|
|
155
|
+
runs all catalogue checks, prints any issues. Exits non-zero if the
|
|
156
|
+
manifest does not conform.
|
|
157
|
+
"""
|
|
158
|
+
from actproof.catalogue import load_catalogue, validate_manifest
|
|
159
|
+
from actproof.manifest import manifest_from_dict
|
|
160
|
+
|
|
161
|
+
# Read the manifest.
|
|
162
|
+
try:
|
|
163
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
164
|
+
manifest = manifest_from_dict(data)
|
|
165
|
+
except Exception as exc: # noqa: BLE001
|
|
166
|
+
click.echo(
|
|
167
|
+
f"Error: Cannot read manifest {manifest_path}: {exc}",
|
|
168
|
+
err=True,
|
|
169
|
+
)
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
|
|
172
|
+
# Load the catalogue.
|
|
173
|
+
try:
|
|
174
|
+
catalogue = load_catalogue(
|
|
175
|
+
acts_path=catalogue_path,
|
|
176
|
+
source_uri=source_uri,
|
|
177
|
+
git_commit=git_commit,
|
|
178
|
+
)
|
|
179
|
+
except Exception as exc: # noqa: BLE001
|
|
180
|
+
click.echo(
|
|
181
|
+
f"Error: Cannot load catalogue {catalogue_path}: {exc}",
|
|
182
|
+
err=True,
|
|
183
|
+
)
|
|
184
|
+
sys.exit(1)
|
|
185
|
+
|
|
186
|
+
# Validate.
|
|
187
|
+
issues = validate_manifest(manifest, catalogue)
|
|
188
|
+
|
|
189
|
+
# Output.
|
|
190
|
+
if as_json:
|
|
191
|
+
result = {
|
|
192
|
+
"ok": len(issues) == 0,
|
|
193
|
+
"manifest_path": str(manifest_path),
|
|
194
|
+
"issues": [
|
|
195
|
+
{"code": i.code, "field": i.field, "message": i.message}
|
|
196
|
+
for i in issues
|
|
197
|
+
],
|
|
198
|
+
}
|
|
199
|
+
click.echo(json.dumps(result, indent=2))
|
|
200
|
+
else:
|
|
201
|
+
if not issues:
|
|
202
|
+
click.echo(click.style(f"OK ", fg="green", bold=True) + f"{manifest_path}")
|
|
203
|
+
click.echo(" No validation issues.")
|
|
204
|
+
else:
|
|
205
|
+
click.echo(
|
|
206
|
+
click.style(f"FAIL ", fg="red", bold=True)
|
|
207
|
+
+ f"{manifest_path}"
|
|
208
|
+
)
|
|
209
|
+
click.echo(f" {len(issues)} issue(s) found:")
|
|
210
|
+
for issue in issues:
|
|
211
|
+
where = f" at {issue.field}" if issue.field else ""
|
|
212
|
+
click.echo(
|
|
213
|
+
f" - {click.style(issue.code, fg='yellow')}"
|
|
214
|
+
f"{where}: {issue.message}"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
sys.exit(1 if issues else 0)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ─────────────────────────────────────────────────────────────────
|
|
221
|
+
# COMMAND: verify
|
|
222
|
+
# ─────────────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
@main.command()
|
|
225
|
+
@click.argument(
|
|
226
|
+
"receipt_path",
|
|
227
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
228
|
+
)
|
|
229
|
+
@click.option(
|
|
230
|
+
"--catalogue", "catalogue_path",
|
|
231
|
+
type=click.Path(exists=True, file_okay=False, path_type=Path),
|
|
232
|
+
help=(
|
|
233
|
+
"Optional path to the actproof-events catalogue acts/ directory. "
|
|
234
|
+
"If not provided, catalogue_conformance check is skipped."
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
@click.option(
|
|
238
|
+
"--git-commit",
|
|
239
|
+
help=(
|
|
240
|
+
"Git commit SHA at which the catalogue was loaded. Required if "
|
|
241
|
+
"--catalogue is provided."
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
@click.option(
|
|
245
|
+
"--source-uri",
|
|
246
|
+
help=(
|
|
247
|
+
"Source URI of the catalogue. Required if --catalogue is provided."
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
@click.option(
|
|
251
|
+
"--skip-anchor", is_flag=True,
|
|
252
|
+
help="Skip the on-chain anchor check.",
|
|
253
|
+
)
|
|
254
|
+
@click.option(
|
|
255
|
+
"--skip-timestamp", is_flag=True,
|
|
256
|
+
help="Skip the RFC 3161 timestamp signature check.",
|
|
257
|
+
)
|
|
258
|
+
@click.option(
|
|
259
|
+
"--json", "as_json", is_flag=True,
|
|
260
|
+
help="Output verification result as JSON for scripting.",
|
|
261
|
+
)
|
|
262
|
+
def verify(
|
|
263
|
+
receipt_path: Path,
|
|
264
|
+
catalogue_path: Optional[Path],
|
|
265
|
+
git_commit: Optional[str],
|
|
266
|
+
source_uri: Optional[str],
|
|
267
|
+
skip_anchor: bool,
|
|
268
|
+
skip_timestamp: bool,
|
|
269
|
+
as_json: bool,
|
|
270
|
+
) -> None:
|
|
271
|
+
"""Verify a receipt end-to-end.
|
|
272
|
+
|
|
273
|
+
Reads RECEIPT_PATH and runs the six checks defined in actproof.verify.
|
|
274
|
+
Prints per-check status. Exits non-zero if any check fails.
|
|
275
|
+
"""
|
|
276
|
+
from actproof.catalogue import load_catalogue
|
|
277
|
+
from actproof.receipt import read_receipt
|
|
278
|
+
from actproof.verify import verify_receipt
|
|
279
|
+
|
|
280
|
+
# Read the receipt.
|
|
281
|
+
try:
|
|
282
|
+
receipt = read_receipt(receipt_path)
|
|
283
|
+
except Exception as exc: # noqa: BLE001
|
|
284
|
+
click.echo(
|
|
285
|
+
f"Error: Cannot read receipt {receipt_path}: {exc}",
|
|
286
|
+
err=True,
|
|
287
|
+
)
|
|
288
|
+
sys.exit(1)
|
|
289
|
+
|
|
290
|
+
# Optionally load the catalogue.
|
|
291
|
+
catalogue = None
|
|
292
|
+
skip_catalogue = False
|
|
293
|
+
if catalogue_path is not None:
|
|
294
|
+
if not git_commit or not source_uri:
|
|
295
|
+
click.echo(
|
|
296
|
+
"Error: --git-commit and --source-uri are required when "
|
|
297
|
+
"--catalogue is provided.",
|
|
298
|
+
err=True,
|
|
299
|
+
)
|
|
300
|
+
sys.exit(2)
|
|
301
|
+
try:
|
|
302
|
+
catalogue = load_catalogue(
|
|
303
|
+
acts_path=catalogue_path,
|
|
304
|
+
source_uri=source_uri,
|
|
305
|
+
git_commit=git_commit,
|
|
306
|
+
)
|
|
307
|
+
except Exception as exc: # noqa: BLE001
|
|
308
|
+
click.echo(
|
|
309
|
+
f"Error: Cannot load catalogue {catalogue_path}: {exc}",
|
|
310
|
+
err=True,
|
|
311
|
+
)
|
|
312
|
+
sys.exit(1)
|
|
313
|
+
else:
|
|
314
|
+
skip_catalogue = True
|
|
315
|
+
|
|
316
|
+
# Run verification.
|
|
317
|
+
result = verify_receipt(
|
|
318
|
+
receipt,
|
|
319
|
+
catalogue=catalogue,
|
|
320
|
+
skip_anchor_check=skip_anchor,
|
|
321
|
+
skip_timestamp_check=skip_timestamp,
|
|
322
|
+
skip_catalogue_check=skip_catalogue,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Output.
|
|
326
|
+
if as_json:
|
|
327
|
+
out = {
|
|
328
|
+
"ok": result.ok,
|
|
329
|
+
"receipt_path": str(receipt_path),
|
|
330
|
+
"manifest_hash": receipt.manifest_hash,
|
|
331
|
+
"checks": [
|
|
332
|
+
{
|
|
333
|
+
"name": c.name,
|
|
334
|
+
"status": c.status.value,
|
|
335
|
+
"detail": c.detail,
|
|
336
|
+
"elapsed_seconds": c.elapsed_seconds,
|
|
337
|
+
}
|
|
338
|
+
for c in result.checks
|
|
339
|
+
],
|
|
340
|
+
}
|
|
341
|
+
click.echo(json.dumps(out, indent=2))
|
|
342
|
+
else:
|
|
343
|
+
header = (
|
|
344
|
+
click.style("OK ", fg="green", bold=True)
|
|
345
|
+
if result.ok
|
|
346
|
+
else click.style("FAIL ", fg="red", bold=True)
|
|
347
|
+
)
|
|
348
|
+
click.echo(header + f"{receipt_path}")
|
|
349
|
+
click.echo(f" manifest_hash: {receipt.manifest_hash}")
|
|
350
|
+
click.echo(f" Checks:")
|
|
351
|
+
for c in result.checks:
|
|
352
|
+
status_color = {
|
|
353
|
+
"pass": "green",
|
|
354
|
+
"fail": "red",
|
|
355
|
+
"skip": "yellow",
|
|
356
|
+
"error": "red",
|
|
357
|
+
}[c.status.value]
|
|
358
|
+
status_str = c.status.value.upper().rjust(5)
|
|
359
|
+
elapsed_ms = (c.elapsed_seconds or 0.0) * 1000
|
|
360
|
+
line = (
|
|
361
|
+
f" [{click.style(status_str, fg=status_color, bold=True)}]"
|
|
362
|
+
f" {c.name:<32} ({elapsed_ms:.2f} ms)"
|
|
363
|
+
)
|
|
364
|
+
click.echo(line)
|
|
365
|
+
if c.detail and c.status.value in ("fail", "error", "skip"):
|
|
366
|
+
# Wrap the detail to fit reasonable width.
|
|
367
|
+
detail = c.detail.replace("\n", " ")[:200]
|
|
368
|
+
click.echo(f" {detail}")
|
|
369
|
+
|
|
370
|
+
sys.exit(0 if result.ok else 1)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ─────────────────────────────────────────────────────────────────
|
|
374
|
+
# COMMAND: anchor
|
|
375
|
+
# ─────────────────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
_MNEMONIC_ENV_VAR = "ACTPROOF_MNEMONIC"
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
@main.command()
|
|
381
|
+
@click.argument(
|
|
382
|
+
"manifest_path",
|
|
383
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
384
|
+
)
|
|
385
|
+
@click.option(
|
|
386
|
+
"--mode",
|
|
387
|
+
type=click.Choice(["draft", "demo", "production"], case_sensitive=False),
|
|
388
|
+
required=True,
|
|
389
|
+
help=(
|
|
390
|
+
"Anchoring mode. draft = build the note bytes without submission. "
|
|
391
|
+
"demo = submit to testnet. production = submit to mainnet."
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
@click.option(
|
|
395
|
+
"--kms-resource",
|
|
396
|
+
help=(
|
|
397
|
+
"GCP KMS Ed25519 key version resource path for production signing. "
|
|
398
|
+
"Mutually exclusive with the ACTPROOF_MNEMONIC env var."
|
|
399
|
+
),
|
|
400
|
+
)
|
|
401
|
+
@click.option(
|
|
402
|
+
"--output", "output_path",
|
|
403
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
404
|
+
required=True,
|
|
405
|
+
help="Path to write the resulting receipt JSON.",
|
|
406
|
+
)
|
|
407
|
+
@click.option(
|
|
408
|
+
"--evidence-output", "evidence_output_path",
|
|
409
|
+
type=click.Path(dir_okay=False, path_type=Path),
|
|
410
|
+
help=(
|
|
411
|
+
"Optional path to write the issuer evidence JSON (the private "
|
|
412
|
+
"addendum containing plaintext recipient emails). If omitted, "
|
|
413
|
+
"no issuer evidence file is produced."
|
|
414
|
+
),
|
|
415
|
+
)
|
|
416
|
+
@click.option(
|
|
417
|
+
"--skip-timestamp", is_flag=True,
|
|
418
|
+
help=(
|
|
419
|
+
"Skip RFC 3161 timestamp acquisition. The receipt will contain a "
|
|
420
|
+
"placeholder TimestampToken; verifiers will fail the timestamp "
|
|
421
|
+
"signature check. Useful for offline tests only."
|
|
422
|
+
),
|
|
423
|
+
)
|
|
424
|
+
@click.option(
|
|
425
|
+
"--wait/--no-wait", default=True,
|
|
426
|
+
help=(
|
|
427
|
+
"If --wait (default), poll algod until the transaction confirms. "
|
|
428
|
+
"If --no-wait, return immediately after submission with block_round=None."
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
def anchor(
|
|
432
|
+
manifest_path: Path,
|
|
433
|
+
mode: str,
|
|
434
|
+
kms_resource: Optional[str],
|
|
435
|
+
output_path: Path,
|
|
436
|
+
evidence_output_path: Optional[Path],
|
|
437
|
+
skip_timestamp: bool,
|
|
438
|
+
wait: bool,
|
|
439
|
+
) -> None:
|
|
440
|
+
"""Anchor a manifest to the Algorand ledger and write a receipt.
|
|
441
|
+
|
|
442
|
+
Reads MANIFEST_PATH, builds a signer (from ACTPROOF_MNEMONIC env var
|
|
443
|
+
or --kms-resource), optionally acquires an RFC 3161 timestamp, anchors
|
|
444
|
+
the manifest hash to Algorand in the requested mode, writes the
|
|
445
|
+
resulting receipt to --output.
|
|
446
|
+
"""
|
|
447
|
+
from actproof.anchor import AnchorMode, anchor_manifest
|
|
448
|
+
from actproof.manifest import manifest_from_dict, hash_manifest
|
|
449
|
+
from actproof.receipt import TimestampToken, build_receipt, write_receipt
|
|
450
|
+
from actproof.signers import GoogleKMSSigner, MnemonicSigner
|
|
451
|
+
from actproof.timestamp import acquire_timestamp_token
|
|
452
|
+
|
|
453
|
+
# Read the manifest.
|
|
454
|
+
try:
|
|
455
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
456
|
+
manifest = manifest_from_dict(data)
|
|
457
|
+
except Exception as exc: # noqa: BLE001
|
|
458
|
+
click.echo(
|
|
459
|
+
f"Error: Cannot read manifest {manifest_path}: {exc}",
|
|
460
|
+
err=True,
|
|
461
|
+
)
|
|
462
|
+
sys.exit(1)
|
|
463
|
+
|
|
464
|
+
# Build the signer.
|
|
465
|
+
mnemonic_value = os.environ.get(_MNEMONIC_ENV_VAR)
|
|
466
|
+
if kms_resource and mnemonic_value:
|
|
467
|
+
click.echo(
|
|
468
|
+
f"Error: Both --kms-resource and {_MNEMONIC_ENV_VAR} are set. "
|
|
469
|
+
f"Choose one.",
|
|
470
|
+
err=True,
|
|
471
|
+
)
|
|
472
|
+
sys.exit(2)
|
|
473
|
+
|
|
474
|
+
signer: Any = None
|
|
475
|
+
if kms_resource:
|
|
476
|
+
try:
|
|
477
|
+
signer = GoogleKMSSigner(kms_resource_name=kms_resource)
|
|
478
|
+
except Exception as exc: # noqa: BLE001
|
|
479
|
+
click.echo(f"Error: Cannot build GoogleKMSSigner: {exc}", err=True)
|
|
480
|
+
sys.exit(1)
|
|
481
|
+
elif mnemonic_value:
|
|
482
|
+
try:
|
|
483
|
+
with warnings.catch_warnings():
|
|
484
|
+
# The MnemonicSigner emits a UserWarning every time; for CLI
|
|
485
|
+
# use the warning would interleave with the normal output.
|
|
486
|
+
# The "do not use for production" warning is documented at
|
|
487
|
+
# the env var level (this command's --help) and at the
|
|
488
|
+
# MnemonicSigner class level (its docstring), so suppressing
|
|
489
|
+
# it here keeps CLI output clean.
|
|
490
|
+
warnings.simplefilter("ignore", category=UserWarning)
|
|
491
|
+
signer = MnemonicSigner(mnemonic_value)
|
|
492
|
+
except Exception as exc: # noqa: BLE001
|
|
493
|
+
click.echo(
|
|
494
|
+
f"Error: Cannot build MnemonicSigner from "
|
|
495
|
+
f"{_MNEMONIC_ENV_VAR}: {exc}",
|
|
496
|
+
err=True,
|
|
497
|
+
)
|
|
498
|
+
sys.exit(1)
|
|
499
|
+
else:
|
|
500
|
+
click.echo(
|
|
501
|
+
f"Error: Either --kms-resource or {_MNEMONIC_ENV_VAR} env "
|
|
502
|
+
f"var must be set. The CLI does NOT read mnemonics from "
|
|
503
|
+
f"command-line arguments (they would leak to shell history).",
|
|
504
|
+
err=True,
|
|
505
|
+
)
|
|
506
|
+
sys.exit(2)
|
|
507
|
+
|
|
508
|
+
# Compute the manifest hash.
|
|
509
|
+
manifest_hash_bytes = hash_manifest(manifest)
|
|
510
|
+
|
|
511
|
+
# Optionally acquire a trusted timestamp.
|
|
512
|
+
if skip_timestamp:
|
|
513
|
+
click.echo(
|
|
514
|
+
click.style(
|
|
515
|
+
"Note: --skip-timestamp set; using placeholder TimestampToken.",
|
|
516
|
+
fg="yellow",
|
|
517
|
+
),
|
|
518
|
+
err=True,
|
|
519
|
+
)
|
|
520
|
+
token = TimestampToken(
|
|
521
|
+
tsa_url="", tsa_name="(placeholder)",
|
|
522
|
+
token_b64="",
|
|
523
|
+
policy_oid=None,
|
|
524
|
+
hash_alg="sha-256",
|
|
525
|
+
imprint_hex=manifest_hash_bytes.hex(),
|
|
526
|
+
timestamp="",
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
try:
|
|
530
|
+
ts_result = acquire_timestamp_token(manifest_hash_bytes)
|
|
531
|
+
if not ts_result.ok:
|
|
532
|
+
click.echo(
|
|
533
|
+
"Error: RFC 3161 timestamp acquisition failed for all "
|
|
534
|
+
"TSAs in the chain.", err=True,
|
|
535
|
+
)
|
|
536
|
+
for a in ts_result.attempts:
|
|
537
|
+
click.echo(
|
|
538
|
+
f" - {a.tsa_name}: {a.error}", err=True,
|
|
539
|
+
)
|
|
540
|
+
sys.exit(1)
|
|
541
|
+
token = ts_result.token # type: ignore[assignment]
|
|
542
|
+
except Exception as exc: # noqa: BLE001
|
|
543
|
+
click.echo(
|
|
544
|
+
f"Error: timestamp acquisition raised: {exc}", err=True,
|
|
545
|
+
)
|
|
546
|
+
sys.exit(1)
|
|
547
|
+
|
|
548
|
+
# Build the AnchorMode.
|
|
549
|
+
mode_map = {
|
|
550
|
+
"draft": AnchorMode.DRAFT,
|
|
551
|
+
"demo": AnchorMode.DEMO,
|
|
552
|
+
"production": AnchorMode.PRODUCTION,
|
|
553
|
+
}
|
|
554
|
+
anchor_mode = mode_map[mode.lower()]
|
|
555
|
+
|
|
556
|
+
# Anchor.
|
|
557
|
+
try:
|
|
558
|
+
anchor_record = anchor_manifest(
|
|
559
|
+
manifest_hash_bytes,
|
|
560
|
+
signer=signer,
|
|
561
|
+
mode=anchor_mode,
|
|
562
|
+
wait_for_confirmation=wait,
|
|
563
|
+
)
|
|
564
|
+
except Exception as exc: # noqa: BLE001
|
|
565
|
+
click.echo(f"Error: anchor failed: {exc}", err=True)
|
|
566
|
+
sys.exit(1)
|
|
567
|
+
|
|
568
|
+
# Build and write the receipt.
|
|
569
|
+
receipt = build_receipt(
|
|
570
|
+
manifest=manifest,
|
|
571
|
+
anchor=anchor_record,
|
|
572
|
+
trusted_timestamp=token,
|
|
573
|
+
)
|
|
574
|
+
try:
|
|
575
|
+
write_receipt(output_path, receipt)
|
|
576
|
+
except Exception as exc: # noqa: BLE001
|
|
577
|
+
click.echo(f"Error: cannot write receipt to {output_path}: {exc}", err=True)
|
|
578
|
+
sys.exit(1)
|
|
579
|
+
|
|
580
|
+
# Output: human-readable summary.
|
|
581
|
+
click.echo(click.style("OK ", fg="green", bold=True) + f"{output_path}")
|
|
582
|
+
click.echo(f" Mode: {anchor_mode.value}")
|
|
583
|
+
click.echo(f" Manifest hash: {receipt.manifest_hash}")
|
|
584
|
+
if anchor_record.txid:
|
|
585
|
+
click.echo(f" Txid: {anchor_record.txid}")
|
|
586
|
+
click.echo(f" Network: {anchor_record.network}")
|
|
587
|
+
if anchor_record.block_round:
|
|
588
|
+
click.echo(f" Block round: {anchor_record.block_round}")
|
|
589
|
+
else:
|
|
590
|
+
click.echo(f" Txid: (draft - not submitted)")
|
|
591
|
+
if token.tsa_name and token.tsa_name != "(placeholder)":
|
|
592
|
+
click.echo(f" TSA: {token.tsa_name}")
|
|
593
|
+
click.echo(f" TSA time: {token.timestamp}")
|