actproof 0.3.2__tar.gz → 0.3.3__tar.gz
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-0.3.2 → actproof-0.3.3}/PKG-INFO +3 -1
- {actproof-0.3.2 → actproof-0.3.3}/actproof/__init__.py +1 -1
- {actproof-0.3.2 → actproof-0.3.3}/actproof/catalogue.py +229 -28
- {actproof-0.3.2 → actproof-0.3.3}/actproof/manifest.py +130 -10
- {actproof-0.3.2 → actproof-0.3.3}/pyproject.toml +3 -1
- actproof-0.3.3/tests/test_catalogue_package_provenance_bugfix.py +518 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_catalogue_v3.py +31 -14
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_signers_google_kms.py +24 -0
- {actproof-0.3.2 → actproof-0.3.3}/.gitignore +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/CHANGELOG.md +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/DEPLOY.md +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/LICENSE +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/README.md +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/SECURITY.md +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/anchor.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/canonical.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/cli.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/py.typed +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/receipt.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/signers/__init__.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/signers/google_kms.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/signers/interface.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/signers/mnemonic.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/timestamp.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/actproof/verify.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/docs/ANCHOR_BACKENDS.md +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/docs/INTEGRATION.md +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/docs/STS-STANDARDS-APPLICATION.md +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/__init__.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_anchor.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_canonical.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_catalogue.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_cli.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_manifest.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_receipt.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_signers_interface.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_signers_mnemonic.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_signers_v030_policy.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_timestamp.py +0 -0
- {actproof-0.3.2 → actproof-0.3.3}/tests/test_verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: actproof
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Verifiable receipts of regulated acts. Canonical JSON (RFC 8785), RFC 3161 trusted timestamps, Algorand ARC-2 anchoring, independent verification.
|
|
5
5
|
Project-URL: Homepage, https://github.com/deyan-paroushev/actproof-py
|
|
6
6
|
Project-URL: Documentation, https://github.com/deyan-paroushev/actproof-py/tree/main/docs
|
|
@@ -45,6 +45,8 @@ Requires-Dist: types-requests; extra == 'dev'
|
|
|
45
45
|
Provides-Extra: docs
|
|
46
46
|
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
|
|
47
47
|
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
48
|
+
Provides-Extra: events
|
|
49
|
+
Requires-Dist: actproof-events>=1.4.0rc1; extra == 'events'
|
|
48
50
|
Provides-Extra: gcp
|
|
49
51
|
Requires-Dist: google-cloud-kms<4.0,>=2.0; extra == 'gcp'
|
|
50
52
|
Requires-Dist: google-crc32c<2.0,>=1.5; extra == 'gcp'
|
|
@@ -32,20 +32,27 @@ declares the corresponding blocks; absent blocks remain ``None``.
|
|
|
32
32
|
Path resolution
|
|
33
33
|
---------------
|
|
34
34
|
|
|
35
|
-
The catalogue is located on disk. How the bytes get there (
|
|
36
|
-
vendored copy, volume mount, fresh
|
|
37
|
-
library does not constrain.
|
|
35
|
+
The catalogue is located on disk. How the bytes get there (pip-installed
|
|
36
|
+
``actproof-events`` package, git submodule, vendored copy, volume mount, fresh
|
|
37
|
+
clone in CI) is a deployment decision the library does not constrain.
|
|
38
|
+
Resolution order:
|
|
38
39
|
|
|
39
40
|
1. The ``acts_path`` argument to ``load_catalogue`` if provided.
|
|
40
41
|
2. ``$ACTPROOF_CATALOGUE_PATH`` environment variable.
|
|
41
|
-
3.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
3. The installed ``actproof-events`` Python package, when importable. This
|
|
43
|
+
covers ``pip install actproof[events]`` and editable installs of the
|
|
44
|
+
events repo.
|
|
45
|
+
4. ``./actproof-events/catalogue/acts/`` relative to the current working dir.
|
|
46
|
+
5. ``./vendor/actproof-events/catalogue/acts/`` relative to the current working dir.
|
|
47
|
+
|
|
48
|
+
The schema file is located relative to the acts path. Two layouts are
|
|
49
|
+
tried: ``../../spec/schemas/`` (source-tree layout where ``catalogue/`` and
|
|
50
|
+
``spec/`` are siblings at the repo root) and ``../../schemas/`` (installed
|
|
51
|
+
package layout where the wheel bundles ``catalogue/`` and ``schemas/``
|
|
52
|
+
side by side under ``actproof_events/data/``). Within each layout
|
|
53
|
+
resolution tries v3 first (``act_catalogue_entry.v3.json``), falls back to
|
|
54
|
+
v2 (``act_catalogue_entry.v2.json``). Whichever file is found is hashed
|
|
55
|
+
into ``Catalogue.schema_hash`` for receipt binding.
|
|
49
56
|
|
|
50
57
|
What gets loaded
|
|
51
58
|
----------------
|
|
@@ -200,15 +207,25 @@ _FALLBACK_ACTS_PATHS: tuple[str, ...] = (
|
|
|
200
207
|
)
|
|
201
208
|
"""Filesystem locations to try if no path is given and the env var is unset."""
|
|
202
209
|
|
|
203
|
-
|
|
204
|
-
"..", "..", "spec", "schemas", "act_catalogue_entry.v3.json",
|
|
210
|
+
_SCHEMA_RELATIVE_PATHS_V3: tuple[tuple[str, ...], ...] = (
|
|
211
|
+
("..", "..", "spec", "schemas", "act_catalogue_entry.v3.json"),
|
|
212
|
+
("..", "..", "schemas", "act_catalogue_entry.v3.json"),
|
|
205
213
|
)
|
|
206
|
-
"""Default v3 schema
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
214
|
+
"""Default v3 schema paths relative to the acts directory. First tuple is the
|
|
215
|
+
source-tree layout (``catalogue/acts/`` sibling of ``spec/schemas/`` under the
|
|
216
|
+
repo root). Second tuple is the installed-package layout (``data/catalogue/acts/``
|
|
217
|
+
sibling of ``data/schemas/`` inside the bundled wheel)."""
|
|
218
|
+
|
|
219
|
+
_SCHEMA_RELATIVE_PATHS_V2: tuple[tuple[str, ...], ...] = (
|
|
220
|
+
("..", "..", "spec", "schemas", "act_catalogue_entry.v2.json"),
|
|
221
|
+
("..", "..", "schemas", "act_catalogue_entry.v2.json"),
|
|
210
222
|
)
|
|
211
|
-
"""Default v2 schema
|
|
223
|
+
"""Default v2 schema paths relative to the acts directory, same two layouts."""
|
|
224
|
+
|
|
225
|
+
# Backward-compatible single-tuple aliases for any external consumer that
|
|
226
|
+
# imported the private names. Public API is via _resolve_schema_path().
|
|
227
|
+
_SCHEMA_RELATIVE_PATH_V3: tuple[str, ...] = _SCHEMA_RELATIVE_PATHS_V3[0]
|
|
228
|
+
_SCHEMA_RELATIVE_PATH_V2: tuple[str, ...] = _SCHEMA_RELATIVE_PATHS_V2[0]
|
|
212
229
|
|
|
213
230
|
|
|
214
231
|
# ─────────────────────────────────────────────────────────────────
|
|
@@ -495,12 +512,27 @@ class Catalogue:
|
|
|
495
512
|
git working tree or the caller did not provide it.
|
|
496
513
|
schema_hash: ``"sha256:..."`` of the catalogue schema file bytes.
|
|
497
514
|
Empty string if the schema file could not be located.
|
|
515
|
+
source_package_name: Optional Python distribution name of the
|
|
516
|
+
package that supplied the catalogue bytes. Populated
|
|
517
|
+
automatically by :func:`load_catalogue` when the catalogue is
|
|
518
|
+
resolved from an installed ``actproof-events`` package (i.e.
|
|
519
|
+
``pip install actproof[events]``). ``None`` when the catalogue
|
|
520
|
+
is sourced via explicit path, environment variable, or
|
|
521
|
+
filesystem fallback. Callers building manifests can pass this
|
|
522
|
+
value through to ``build_manifest(catalogue_source_package_name=...)``
|
|
523
|
+
so the resulting receipt records pip-installable provenance
|
|
524
|
+
alongside the git-based pinning.
|
|
525
|
+
source_package_version: Optional version string of the source
|
|
526
|
+
package at load time (e.g. ``"1.4.0rc1"``). Set whenever
|
|
527
|
+
``source_package_name`` is set; ``None`` otherwise.
|
|
498
528
|
"""
|
|
499
529
|
entries: Mapping[str, CatalogueEntry]
|
|
500
530
|
source_root: str
|
|
501
531
|
source_uri: Optional[str]
|
|
502
532
|
git_commit: Optional[str]
|
|
503
533
|
schema_hash: str
|
|
534
|
+
source_package_name: Optional[str] = None
|
|
535
|
+
source_package_version: Optional[str] = None
|
|
504
536
|
|
|
505
537
|
def get(self, act_type_id: str) -> Optional[CatalogueEntry]:
|
|
506
538
|
"""Look up an entry by ``act_type_id``. Returns ``None`` if absent."""
|
|
@@ -530,6 +562,14 @@ class ValidationIssue:
|
|
|
530
562
|
what's in the loaded catalogue.
|
|
531
563
|
- ``SCHEMA_HASH_MISMATCH``: manifest's schema_hash differs from
|
|
532
564
|
what's in the loaded catalogue.
|
|
565
|
+
- ``SOURCE_PACKAGE_NAME_MISMATCH``: manifest pins one
|
|
566
|
+
installable package name (e.g. ``"actproof-events"``) and the
|
|
567
|
+
loaded catalogue was sourced from a differently-named
|
|
568
|
+
package. Only fires when both sides have set the optional
|
|
569
|
+
field.
|
|
570
|
+
- ``SOURCE_PACKAGE_VERSION_MISMATCH``: same as above for the
|
|
571
|
+
version string. Only fires when both sides have set the
|
|
572
|
+
optional field.
|
|
533
573
|
- ``MISSING_REQUIRED_CLAIM_FIELD``: a required claim field is
|
|
534
574
|
absent or empty in the manifest's claim.
|
|
535
575
|
- ``MISSING_REQUIRED_EVIDENCE_LABEL``: a required evidence label
|
|
@@ -549,8 +589,53 @@ class ValidationIssue:
|
|
|
549
589
|
# PATH RESOLUTION
|
|
550
590
|
# ─────────────────────────────────────────────────────────────────
|
|
551
591
|
|
|
592
|
+
def _resolve_from_packaged_events() -> Optional[Path]:
|
|
593
|
+
"""Try to resolve the acts path from the installed ``actproof-events`` package.
|
|
594
|
+
|
|
595
|
+
Returns the catalogue ``acts`` directory path if the package is importable
|
|
596
|
+
and exposes ``get_catalogue_path()`` (introduced in actproof-events
|
|
597
|
+
v1.4.0rc1) and that path resolves to an existing directory. Returns
|
|
598
|
+
``None`` if the package is not installed, the function is absent (older
|
|
599
|
+
package version), or the bundled directory cannot be located on disk.
|
|
600
|
+
|
|
601
|
+
The returned value from ``get_catalogue_path()`` may be a :class:`Path`,
|
|
602
|
+
a :class:`str`, or any :class:`os.PathLike`; we coerce via ``Path(value)``
|
|
603
|
+
so future actproof-events releases that change their return type do not
|
|
604
|
+
silently break catalogue resolution.
|
|
605
|
+
|
|
606
|
+
This branch sits between the environment variable and the filesystem
|
|
607
|
+
fallbacks in :func:`_resolve_acts_path`'s priority order, so callers
|
|
608
|
+
using ``pip install actproof[events]`` get the bundled catalogue
|
|
609
|
+
automatically while explicit configuration (constructor arg or env var)
|
|
610
|
+
continues to win.
|
|
611
|
+
"""
|
|
612
|
+
try:
|
|
613
|
+
import actproof_events # type: ignore[import-not-found]
|
|
614
|
+
except ImportError:
|
|
615
|
+
return None
|
|
616
|
+
get_path = getattr(actproof_events, "get_catalogue_path", None)
|
|
617
|
+
if get_path is None:
|
|
618
|
+
return None
|
|
619
|
+
try:
|
|
620
|
+
raw = get_path()
|
|
621
|
+
except Exception: # pragma: no cover - defensive against package bugs
|
|
622
|
+
return None
|
|
623
|
+
if raw is None:
|
|
624
|
+
return None
|
|
625
|
+
# Coerce to Path: accepts Path, str, and os.PathLike. TypeError when
|
|
626
|
+
# the return value is none of those (e.g. int) drops us back to None
|
|
627
|
+
# rather than propagating.
|
|
628
|
+
try:
|
|
629
|
+
path = Path(raw).expanduser().resolve()
|
|
630
|
+
except TypeError:
|
|
631
|
+
return None
|
|
632
|
+
if not path.is_dir():
|
|
633
|
+
return None
|
|
634
|
+
return path
|
|
635
|
+
|
|
636
|
+
|
|
552
637
|
def _resolve_acts_path(explicit: Optional[Path]) -> Path:
|
|
553
|
-
"""Resolve the catalogue acts path from arg, env var, or fallbacks."""
|
|
638
|
+
"""Resolve the catalogue acts path from arg, env var, installed package, or fallbacks."""
|
|
554
639
|
if explicit is not None:
|
|
555
640
|
path = explicit.expanduser().resolve()
|
|
556
641
|
if not path.is_dir():
|
|
@@ -569,6 +654,10 @@ def _resolve_acts_path(explicit: Optional[Path]) -> Path:
|
|
|
569
654
|
)
|
|
570
655
|
return path
|
|
571
656
|
|
|
657
|
+
packaged = _resolve_from_packaged_events()
|
|
658
|
+
if packaged is not None:
|
|
659
|
+
return packaged
|
|
660
|
+
|
|
572
661
|
for fallback in _FALLBACK_ACTS_PATHS:
|
|
573
662
|
candidate = Path(fallback).resolve()
|
|
574
663
|
if candidate.is_dir():
|
|
@@ -577,6 +666,7 @@ def _resolve_acts_path(explicit: Optional[Path]) -> Path:
|
|
|
577
666
|
raise CatalogueLoadError(
|
|
578
667
|
f"Could not locate the catalogue acts directory. "
|
|
579
668
|
f"Pass acts_path explicitly, set {ENV_CATALOGUE_PATH}, "
|
|
669
|
+
f"install the actproof-events package (e.g. pip install actproof[events]), "
|
|
580
670
|
f"or place the catalogue at one of: {', '.join(_FALLBACK_ACTS_PATHS)}."
|
|
581
671
|
)
|
|
582
672
|
|
|
@@ -585,13 +675,19 @@ def _resolve_schema_path(acts_path: Path) -> Optional[Path]:
|
|
|
585
675
|
"""Find the schema file relative to the acts directory.
|
|
586
676
|
|
|
587
677
|
Tries v3 first (``act_catalogue_entry.v3.json``), falls back to v2
|
|
588
|
-
(``act_catalogue_entry.v2.json``).
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
``
|
|
678
|
+
(``act_catalogue_entry.v2.json``). For each version tries two layouts
|
|
679
|
+
in order: the source-tree layout (``../../spec/schemas/`` from acts,
|
|
680
|
+
where ``catalogue/`` and ``spec/`` are siblings at the repo root) and
|
|
681
|
+
the installed-package layout (``../../schemas/`` from acts, where the
|
|
682
|
+
wheel bundles ``catalogue/acts/`` and ``schemas/`` side by side under
|
|
683
|
+
``actproof_events/data/``).
|
|
684
|
+
|
|
685
|
+
Returns ``None`` if no candidate file exists. Catalogues that ship v3
|
|
686
|
+
entries should have the v3 schema file present; catalogues that ship
|
|
687
|
+
only v2 entries may have only the v2 schema file. Whichever file is
|
|
688
|
+
found is what gets hashed into ``Catalogue.schema_hash``.
|
|
593
689
|
"""
|
|
594
|
-
for relative_parts in (
|
|
690
|
+
for relative_parts in (*_SCHEMA_RELATIVE_PATHS_V3, *_SCHEMA_RELATIVE_PATHS_V2):
|
|
595
691
|
candidate = acts_path.joinpath(*relative_parts).resolve()
|
|
596
692
|
if candidate.is_file():
|
|
597
693
|
return candidate
|
|
@@ -805,6 +901,13 @@ def load_catalogue(
|
|
|
805
901
|
|
|
806
902
|
Returns:
|
|
807
903
|
A ``Catalogue`` with all v2 and v3 entries indexed by ``act_type_id``.
|
|
904
|
+
When the catalogue was resolved from the installed
|
|
905
|
+
``actproof-events`` package (priority 3 in the resolution order),
|
|
906
|
+
the returned ``Catalogue`` also carries ``source_package_name`` and
|
|
907
|
+
``source_package_version`` set to that package's distribution name
|
|
908
|
+
and version. Callers can pass these to ``build_manifest`` so
|
|
909
|
+
receipts record pip-installable provenance alongside the existing
|
|
910
|
+
git-based catalogue pinning.
|
|
808
911
|
|
|
809
912
|
Raises:
|
|
810
913
|
CatalogueLoadError: If the path cannot be resolved or contains
|
|
@@ -826,6 +929,41 @@ def load_catalogue(
|
|
|
826
929
|
# If we couldn't find the schema, schema_hash stays empty. Callers
|
|
827
930
|
# populating CatalogueBinding will need to provide it some other way.
|
|
828
931
|
|
|
932
|
+
# Detect pip-installable provenance: if no explicit path was given and
|
|
933
|
+
# the resolved acts path came from the actproof-events Python package,
|
|
934
|
+
# record the package name and version so receipts can pin to
|
|
935
|
+
# ``pip install actproof-events==<version>`` alongside the git pinning.
|
|
936
|
+
# This is best-effort and never raises: receipts can still be built
|
|
937
|
+
# without these fields by callers who source the catalogue via git
|
|
938
|
+
# submodule, vendoring, or volume mount.
|
|
939
|
+
source_package_name: Optional[str] = None
|
|
940
|
+
source_package_version: Optional[str] = None
|
|
941
|
+
if acts_path is None:
|
|
942
|
+
packaged = _resolve_from_packaged_events()
|
|
943
|
+
if packaged is not None and packaged == resolved_acts:
|
|
944
|
+
# Read the version from the installed distribution metadata
|
|
945
|
+
# rather than the package's __version__ attribute. The
|
|
946
|
+
# distribution metadata is the authoritative source (set at
|
|
947
|
+
# install time from pyproject.toml or the wheel metadata);
|
|
948
|
+
# the __version__ attribute is convenience that may or may
|
|
949
|
+
# not be set, and can drift from the metadata if the package
|
|
950
|
+
# forgot to update one side. importlib.metadata is in the
|
|
951
|
+
# standard library from Python 3.8 onward.
|
|
952
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
953
|
+
try:
|
|
954
|
+
source_package_version = version("actproof-events")
|
|
955
|
+
source_package_name = "actproof-events"
|
|
956
|
+
except PackageNotFoundError:
|
|
957
|
+
# The package files are present on disk (we resolved
|
|
958
|
+
# the path through _resolve_from_packaged_events) but
|
|
959
|
+
# the distribution metadata is missing. This is a
|
|
960
|
+
# corner case for editable installs that did not run
|
|
961
|
+
# ``pip install -e .`` correctly, or for namespace
|
|
962
|
+
# packages without distribution metadata. Fall through
|
|
963
|
+
# silently: the catalogue still loads, callers just
|
|
964
|
+
# get no package provenance.
|
|
965
|
+
pass
|
|
966
|
+
|
|
829
967
|
logger.info(
|
|
830
968
|
"Loaded %d catalogue entries from %s: %s",
|
|
831
969
|
len(entries), resolved_acts, sorted(entries.keys()),
|
|
@@ -837,6 +975,8 @@ def load_catalogue(
|
|
|
837
975
|
source_uri=source_uri,
|
|
838
976
|
git_commit=git_commit,
|
|
839
977
|
schema_hash=schema_hash,
|
|
978
|
+
source_package_name=source_package_name,
|
|
979
|
+
source_package_version=source_package_version,
|
|
840
980
|
)
|
|
841
981
|
|
|
842
982
|
|
|
@@ -889,9 +1029,19 @@ def validate_manifest(
|
|
|
889
1029
|
) -> list[ValidationIssue]:
|
|
890
1030
|
"""Validate a manifest's claim and evidence against its catalogue entry.
|
|
891
1031
|
|
|
892
|
-
Performs
|
|
893
|
-
|
|
894
|
-
|
|
1032
|
+
Performs up to nine checks. Seven always run; two additional ones
|
|
1033
|
+
(SOURCE_PACKAGE_NAME_MISMATCH, SOURCE_PACKAGE_VERSION_MISMATCH) only
|
|
1034
|
+
fire when both the manifest and the loaded catalogue carry the
|
|
1035
|
+
optional package-provenance fields. See the ``ValidationIssue``
|
|
1036
|
+
docstring for the full code list. Returns a list of issues; an
|
|
1037
|
+
empty list means the manifest is valid against the loaded catalogue.
|
|
1038
|
+
|
|
1039
|
+
The two package-provenance checks are diagnostic. The cryptographic
|
|
1040
|
+
binding between a manifest and the catalogue bytes it implements is
|
|
1041
|
+
``entry_hash`` and ``schema_hash``; those checks already cover any
|
|
1042
|
+
byte-level drift. The package name and version checks give a
|
|
1043
|
+
clearer error message when the operator's environment has a
|
|
1044
|
+
different installed version than the issuer used at issue time.
|
|
895
1045
|
|
|
896
1046
|
Catalogue conformance is one of several validation layers a complete
|
|
897
1047
|
verifier runs. The others are: ``validate_manifest_shape`` (structural
|
|
@@ -967,6 +1117,57 @@ def validate_manifest(
|
|
|
967
1117
|
)
|
|
968
1118
|
)
|
|
969
1119
|
|
|
1120
|
+
# 4a. Source package name matches (only when both sides have set it)?
|
|
1121
|
+
# The optional source_package_name and source_package_version fields
|
|
1122
|
+
# are pip-installable-provenance metadata. They are NOT a substitute
|
|
1123
|
+
# for the cryptographic entry_hash and schema_hash checks above; the
|
|
1124
|
+
# bytes binding is still authoritative. These two checks give
|
|
1125
|
+
# operators a clearer diagnostic when a mismatch occurs ("you have
|
|
1126
|
+
# actproof-events 1.4.1 installed but the receipt was issued against
|
|
1127
|
+
# 1.4.0rc1") than the entry_hash mismatch alone would.
|
|
1128
|
+
if (
|
|
1129
|
+
manifest.catalogue.source_package_name
|
|
1130
|
+
and catalogue.source_package_name
|
|
1131
|
+
and manifest.catalogue.source_package_name != catalogue.source_package_name
|
|
1132
|
+
):
|
|
1133
|
+
issues.append(
|
|
1134
|
+
ValidationIssue(
|
|
1135
|
+
code="SOURCE_PACKAGE_NAME_MISMATCH",
|
|
1136
|
+
message=(
|
|
1137
|
+
f"manifest pins source_package_name "
|
|
1138
|
+
f"{manifest.catalogue.source_package_name!r} but loaded "
|
|
1139
|
+
f"catalogue was sourced from {catalogue.source_package_name!r}. "
|
|
1140
|
+
f"This is package-provenance metadata, not a cryptographic "
|
|
1141
|
+
f"binding (entry_hash already covers the bytes), but indicates "
|
|
1142
|
+
f"the operator's environment differs from the issuer's."
|
|
1143
|
+
),
|
|
1144
|
+
field="catalogue.source_package_name",
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
# 4b. Source package version matches (only when both sides have set it)?
|
|
1149
|
+
if (
|
|
1150
|
+
manifest.catalogue.source_package_version
|
|
1151
|
+
and catalogue.source_package_version
|
|
1152
|
+
and manifest.catalogue.source_package_version != catalogue.source_package_version
|
|
1153
|
+
):
|
|
1154
|
+
issues.append(
|
|
1155
|
+
ValidationIssue(
|
|
1156
|
+
code="SOURCE_PACKAGE_VERSION_MISMATCH",
|
|
1157
|
+
message=(
|
|
1158
|
+
f"manifest pins source_package_version "
|
|
1159
|
+
f"{manifest.catalogue.source_package_version!r} but loaded "
|
|
1160
|
+
f"catalogue was sourced from version "
|
|
1161
|
+
f"{catalogue.source_package_version!r}. "
|
|
1162
|
+
f"This is package-provenance metadata, not a cryptographic "
|
|
1163
|
+
f"binding (entry_hash already covers the bytes), but signals "
|
|
1164
|
+
f"that ``pip install`` with the receipt's pinned version may "
|
|
1165
|
+
f"be required to reproduce byte-for-byte."
|
|
1166
|
+
),
|
|
1167
|
+
field="catalogue.source_package_version",
|
|
1168
|
+
)
|
|
1169
|
+
)
|
|
1170
|
+
|
|
970
1171
|
# 5. Required claim fields all present and non-empty?
|
|
971
1172
|
for required_field in entry.required_claim_fields:
|
|
972
1173
|
value = manifest.claim.get(required_field)
|
|
@@ -211,11 +211,22 @@ class ManifestValidationError(ValueError):
|
|
|
211
211
|
class CatalogueBinding:
|
|
212
212
|
"""Pins which catalogue entry, version, and source revision this manifest implements.
|
|
213
213
|
|
|
214
|
-
Together the
|
|
214
|
+
Together the six required fields make the receipt independently verifiable:
|
|
215
215
|
anyone can fetch the catalogue source at the pinned commit, locate the
|
|
216
216
|
entry by ``act_type_id``, recompute the entry hash, and confirm that
|
|
217
217
|
what was used matches what the receipt claims.
|
|
218
218
|
|
|
219
|
+
Two additional optional fields name the pip-installable Python package
|
|
220
|
+
that provides the catalogue bytes. They are populated automatically when
|
|
221
|
+
the catalogue is loaded from an installed ``actproof-events`` package
|
|
222
|
+
(via ``pip install actproof[events]``); they remain ``None`` when the
|
|
223
|
+
catalogue is sourced via git submodule, vendored copy, or volume mount.
|
|
224
|
+
The package fields are convenience provenance: a verifier with the
|
|
225
|
+
package name and version can ``pip install`` the exact release whose
|
|
226
|
+
bytes hash to ``entry_hash``, without having to clone the git repo
|
|
227
|
+
and check out the ``git_commit``. The cryptographic binding remains
|
|
228
|
+
``entry_hash``; the package fields make the source easier to obtain.
|
|
229
|
+
|
|
219
230
|
Attributes:
|
|
220
231
|
act_type_id: The catalogue entry's act_type_id, e.g.
|
|
221
232
|
``"op:eu.nis2.art20.management_body_approval.v1"``.
|
|
@@ -224,6 +235,15 @@ class CatalogueBinding:
|
|
|
224
235
|
git_commit: 40-character git SHA-1 commit of the catalogue at issue time.
|
|
225
236
|
entry_hash: SHA-256 of the entry JSON file bytes, as ``"sha256:..."``.
|
|
226
237
|
schema_hash: SHA-256 of the catalogue schema JSON bytes, as ``"sha256:..."``.
|
|
238
|
+
source_package_name: Optional Python distribution name of the package
|
|
239
|
+
that provided the catalogue bytes (typically ``"actproof-events"``).
|
|
240
|
+
``None`` when the catalogue was sourced via git, vendoring, or
|
|
241
|
+
volume mount rather than a pip install.
|
|
242
|
+
source_package_version: Optional version string of the source package
|
|
243
|
+
at load time (e.g. ``"1.4.0rc1"``). Pairs with
|
|
244
|
+
``source_package_name`` to allow a verifier to
|
|
245
|
+
``pip install actproof-events==<version>`` and obtain byte-exact
|
|
246
|
+
catalogue bytes. ``None`` when ``source_package_name`` is ``None``.
|
|
227
247
|
"""
|
|
228
248
|
act_type_id: str
|
|
229
249
|
entry_version: int
|
|
@@ -231,6 +251,8 @@ class CatalogueBinding:
|
|
|
231
251
|
git_commit: str
|
|
232
252
|
entry_hash: str
|
|
233
253
|
schema_hash: str
|
|
254
|
+
source_package_name: Optional[str] = None
|
|
255
|
+
source_package_version: Optional[str] = None
|
|
234
256
|
|
|
235
257
|
|
|
236
258
|
@dataclass(frozen=True)
|
|
@@ -350,6 +372,8 @@ def build_manifest(
|
|
|
350
372
|
issued_at: str,
|
|
351
373
|
receipt_profile: str = RECEIPT_PROFILE_V1,
|
|
352
374
|
batching_profile: str = BATCHING_PROFILE_SINGLE,
|
|
375
|
+
catalogue_source_package_name: Optional[str] = None,
|
|
376
|
+
catalogue_source_package_version: Optional[str] = None,
|
|
353
377
|
) -> Manifest:
|
|
354
378
|
"""Construct a Manifest from raw fields.
|
|
355
379
|
|
|
@@ -376,10 +400,46 @@ def build_manifest(
|
|
|
376
400
|
issued_at: ISO 8601 UTC timestamp with ``Z`` suffix.
|
|
377
401
|
receipt_profile: Receipt profile discriminator. Defaults to v1.
|
|
378
402
|
batching_profile: Batching profile discriminator. Defaults to single.
|
|
403
|
+
catalogue_source_package_name: Optional pip distribution name of the
|
|
404
|
+
package that provided the catalogue bytes (typically
|
|
405
|
+
``"actproof-events"``). Leave ``None`` when sourcing the catalogue
|
|
406
|
+
from a git submodule, vendored copy, or volume mount. Pass both
|
|
407
|
+
this and ``catalogue_source_package_version`` together, or
|
|
408
|
+
neither.
|
|
409
|
+
catalogue_source_package_version: Optional version string of the
|
|
410
|
+
source package at issue time (e.g. ``"1.4.0rc1"``).
|
|
379
411
|
|
|
380
412
|
Returns:
|
|
381
413
|
A fully populated, immutable ``Manifest`` instance.
|
|
414
|
+
|
|
415
|
+
Raises:
|
|
416
|
+
ManifestValidationError: If exactly one of
|
|
417
|
+
``catalogue_source_package_name`` and
|
|
418
|
+
``catalogue_source_package_version`` is provided. The two
|
|
419
|
+
are both-or-neither; either commit to package provenance
|
|
420
|
+
fully or omit it entirely.
|
|
382
421
|
"""
|
|
422
|
+
# Both-or-neither enforcement (mirrors manifest_from_dict). One-sided
|
|
423
|
+
# package provenance would silently round-trip to a different shape
|
|
424
|
+
# via manifest_to_dict, which is a correctness hazard. Reject up front.
|
|
425
|
+
if (catalogue_source_package_name is None) != (catalogue_source_package_version is None):
|
|
426
|
+
raise ManifestValidationError(
|
|
427
|
+
"catalogue_source_package_name and catalogue_source_package_version "
|
|
428
|
+
"must either both be provided or both be None. "
|
|
429
|
+
f"Got catalogue_source_package_name={catalogue_source_package_name!r}, "
|
|
430
|
+
f"catalogue_source_package_version={catalogue_source_package_version!r}."
|
|
431
|
+
)
|
|
432
|
+
if catalogue_source_package_name is not None and not isinstance(catalogue_source_package_name, str):
|
|
433
|
+
raise ManifestValidationError(
|
|
434
|
+
f"catalogue_source_package_name must be a string when present, "
|
|
435
|
+
f"got {type(catalogue_source_package_name).__name__}."
|
|
436
|
+
)
|
|
437
|
+
if catalogue_source_package_version is not None and not isinstance(catalogue_source_package_version, str):
|
|
438
|
+
raise ManifestValidationError(
|
|
439
|
+
f"catalogue_source_package_version must be a string when present, "
|
|
440
|
+
f"got {type(catalogue_source_package_version).__name__}."
|
|
441
|
+
)
|
|
442
|
+
|
|
383
443
|
return Manifest(
|
|
384
444
|
receipt_profile=receipt_profile,
|
|
385
445
|
issued_at=issued_at,
|
|
@@ -390,6 +450,8 @@ def build_manifest(
|
|
|
390
450
|
git_commit=catalogue_git_commit,
|
|
391
451
|
entry_hash=catalogue_entry_hash,
|
|
392
452
|
schema_hash=catalogue_schema_hash,
|
|
453
|
+
source_package_name=catalogue_source_package_name,
|
|
454
|
+
source_package_version=catalogue_source_package_version,
|
|
393
455
|
),
|
|
394
456
|
issuer=Issuer(
|
|
395
457
|
org_name=issuer_org_name,
|
|
@@ -413,23 +475,41 @@ def manifest_to_dict(m: Manifest) -> dict[str, Any]:
|
|
|
413
475
|
The output is what gets passed to ``canonicalize()`` to produce the
|
|
414
476
|
canonical bytes that are hashed and anchored.
|
|
415
477
|
|
|
478
|
+
Backwards compatibility: the optional catalogue fields
|
|
479
|
+
``source_package_name`` and ``source_package_version`` are only
|
|
480
|
+
emitted when both are populated. When both are ``None``, they are
|
|
481
|
+
omitted entirely from the canonical output, so manifests built
|
|
482
|
+
before these fields existed produce byte-identical canonical bytes
|
|
483
|
+
(and therefore byte-identical hashes) as they did before.
|
|
484
|
+
|
|
416
485
|
Args:
|
|
417
486
|
m: The Manifest to serialise.
|
|
418
487
|
|
|
419
488
|
Returns:
|
|
420
489
|
A nested dict with all manifest fields.
|
|
421
490
|
"""
|
|
491
|
+
catalogue_block: dict[str, Any] = {
|
|
492
|
+
"act_type_id": m.catalogue.act_type_id,
|
|
493
|
+
"entry_version": m.catalogue.entry_version,
|
|
494
|
+
"source_uri": m.catalogue.source_uri,
|
|
495
|
+
"git_commit": m.catalogue.git_commit,
|
|
496
|
+
"entry_hash": m.catalogue.entry_hash,
|
|
497
|
+
"schema_hash": m.catalogue.schema_hash,
|
|
498
|
+
}
|
|
499
|
+
# Emit the optional package fields only when both are populated. This
|
|
500
|
+
# preserves byte-exact canonical bytes for manifests issued before
|
|
501
|
+
# these fields existed (where both are None).
|
|
502
|
+
if (
|
|
503
|
+
m.catalogue.source_package_name is not None
|
|
504
|
+
and m.catalogue.source_package_version is not None
|
|
505
|
+
):
|
|
506
|
+
catalogue_block["source_package_name"] = m.catalogue.source_package_name
|
|
507
|
+
catalogue_block["source_package_version"] = m.catalogue.source_package_version
|
|
508
|
+
|
|
422
509
|
return {
|
|
423
510
|
"receipt_profile": m.receipt_profile,
|
|
424
511
|
"issued_at": m.issued_at,
|
|
425
|
-
"catalogue":
|
|
426
|
-
"act_type_id": m.catalogue.act_type_id,
|
|
427
|
-
"entry_version": m.catalogue.entry_version,
|
|
428
|
-
"source_uri": m.catalogue.source_uri,
|
|
429
|
-
"git_commit": m.catalogue.git_commit,
|
|
430
|
-
"entry_hash": m.catalogue.entry_hash,
|
|
431
|
-
"schema_hash": m.catalogue.schema_hash,
|
|
432
|
-
},
|
|
512
|
+
"catalogue": catalogue_block,
|
|
433
513
|
"issuer": {
|
|
434
514
|
"org_name": m.issuer.org_name,
|
|
435
515
|
"authority_label": m.issuer.authority_label,
|
|
@@ -465,6 +545,20 @@ def manifest_from_dict(d: Mapping[str, Any]) -> Manifest:
|
|
|
465
545
|
manifest as a nested dict and needs to reconstruct the typed object
|
|
466
546
|
for inspection.
|
|
467
547
|
|
|
548
|
+
Backwards compatibility: the optional catalogue fields
|
|
549
|
+
``source_package_name`` and ``source_package_version`` are read via
|
|
550
|
+
``.get(...)`` so receipts predating these fields parse without
|
|
551
|
+
raising. Receipts that carry them populate the corresponding
|
|
552
|
+
``CatalogueBinding`` attributes.
|
|
553
|
+
|
|
554
|
+
The two package fields are both-or-neither: a manifest that carries
|
|
555
|
+
one of them but not the other is malformed and parsing raises
|
|
556
|
+
``ManifestValidationError``. This prevents a roundtrip discrepancy
|
|
557
|
+
where the input dict had one field, the parsed object stores it
|
|
558
|
+
silently, and re-serialisation via ``manifest_to_dict`` drops both
|
|
559
|
+
(because ``manifest_to_dict`` emits both only when both are set).
|
|
560
|
+
Either commit to package provenance fully or omit it entirely.
|
|
561
|
+
|
|
468
562
|
Args:
|
|
469
563
|
d: A dict matching the canonical manifest shape.
|
|
470
564
|
|
|
@@ -473,12 +567,36 @@ def manifest_from_dict(d: Mapping[str, Any]) -> Manifest:
|
|
|
473
567
|
|
|
474
568
|
Raises:
|
|
475
569
|
ManifestValidationError: If a required field is missing or has
|
|
476
|
-
the wrong type
|
|
570
|
+
the wrong type, or if exactly one of the optional package
|
|
571
|
+
fields is present.
|
|
477
572
|
"""
|
|
478
573
|
try:
|
|
479
574
|
catalogue_dict = d["catalogue"]
|
|
480
575
|
issuer_dict = d["issuer"]
|
|
481
576
|
|
|
577
|
+
# Both-or-neither enforcement for the optional package fields.
|
|
578
|
+
# Catches malformed manifests with one field set and the other
|
|
579
|
+
# absent or None, which would lose information on roundtrip.
|
|
580
|
+
source_package_name = catalogue_dict.get("source_package_name")
|
|
581
|
+
source_package_version = catalogue_dict.get("source_package_version")
|
|
582
|
+
if (source_package_name is None) != (source_package_version is None):
|
|
583
|
+
raise ManifestValidationError(
|
|
584
|
+
"catalogue.source_package_name and catalogue.source_package_version "
|
|
585
|
+
"must either both be present or both be omitted. "
|
|
586
|
+
f"Got source_package_name={source_package_name!r}, "
|
|
587
|
+
f"source_package_version={source_package_version!r}."
|
|
588
|
+
)
|
|
589
|
+
if source_package_name is not None and not isinstance(source_package_name, str):
|
|
590
|
+
raise ManifestValidationError(
|
|
591
|
+
f"catalogue.source_package_name must be a string when present, "
|
|
592
|
+
f"got {type(source_package_name).__name__}."
|
|
593
|
+
)
|
|
594
|
+
if source_package_version is not None and not isinstance(source_package_version, str):
|
|
595
|
+
raise ManifestValidationError(
|
|
596
|
+
f"catalogue.source_package_version must be a string when present, "
|
|
597
|
+
f"got {type(source_package_version).__name__}."
|
|
598
|
+
)
|
|
599
|
+
|
|
482
600
|
return Manifest(
|
|
483
601
|
receipt_profile=d["receipt_profile"],
|
|
484
602
|
issued_at=d["issued_at"],
|
|
@@ -489,6 +607,8 @@ def manifest_from_dict(d: Mapping[str, Any]) -> Manifest:
|
|
|
489
607
|
git_commit=catalogue_dict["git_commit"],
|
|
490
608
|
entry_hash=catalogue_dict["entry_hash"],
|
|
491
609
|
schema_hash=catalogue_dict["schema_hash"],
|
|
610
|
+
source_package_name=source_package_name,
|
|
611
|
+
source_package_version=source_package_version,
|
|
492
612
|
),
|
|
493
613
|
issuer=Issuer(
|
|
494
614
|
org_name=issuer_dict["org_name"],
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "actproof"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "Verifiable receipts of regulated acts. Canonical JSON (RFC 8785), RFC 3161 trusted timestamps, Algorand ARC-2 anchoring, independent verification."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -62,6 +62,8 @@ gcp = [
|
|
|
62
62
|
"google-crc32c>=1.5,<2.0",
|
|
63
63
|
]
|
|
64
64
|
|
|
65
|
+
events = ["actproof-events>=1.4.0rc1"]
|
|
66
|
+
|
|
65
67
|
[project.urls]
|
|
66
68
|
Homepage = "https://github.com/deyan-paroushev/actproof-py"
|
|
67
69
|
Documentation = "https://github.com/deyan-paroushev/actproof-py/tree/main/docs"
|