src/pqc_content_provenance/assertions/base.py
| 1 | """Assertion base class -- pluggable claims about content.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import hashlib |
| 6 | import json |
| 7 | from dataclasses import asdict, dataclass |
| 8 | from typing import Any, ClassVar |
| 9 | |
| 10 | |
| 11 | @dataclass |
| 12 | class Assertion: |
| 13 | """Base class for provenance assertions (C2PA-style claim facts).""" |
| 14 | |
| 15 | label: ClassVar[str] = "c2pa.generic" |
| 16 | |
| 17 | def to_dict(self) -> dict[str, Any]: |
| 18 | d = asdict(self) |
| 19 | d["label"] = self.label |
| 20 | return d |
| 21 | |
| 22 | @classmethod |
| 23 | def from_dict(cls, data: dict[str, Any]) -> Assertion: |
| 24 | d = dict(data) |
| 25 | d.pop("label", None) |
| 26 | return cls(**d) |
| 27 | |
| 28 | def canonical_bytes(self) -> bytes: |
| 29 | """Deterministic serialization used for hashing assertions.""" |
| 30 | return json.dumps( |
| 31 | self.to_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=False |
| 32 | ).encode("utf-8") |
| 33 | |
| 34 | def hash(self) -> str: |
| 35 | return hashlib.sha3_256(self.canonical_bytes()).hexdigest() |
| 36 | |