Delivers the offline runnable specification foundation for the Trust Layer (TSD §3-§6), not a hosted Trust Service: - JSON Schemas for Phase Manifest, Ledger Entry, Extension Contract, and Conversion Attestation, encoding the Stage 0 working defaults (Q3 future license enum, Q6 single-currency Phases, Q8 required longstop_at). - src/target_revenue: pure Outstanding Target fold, SHA-256 hash-chain verification, Ed25519 signing helpers, extension conformance checks (including a core-term-redefinition heuristic), and conversion detection that never requires an attestation document to determine conversion status. - examples/phase-001: golden Phase package matching the concept doc's worked example, generated via scripts/generate_golden_phase.py so the hash chain is computed by the library itself, not hand-typed. - 32 passing pytest tests covering manifest/ledger/extension conformance, tamper/reorder detection, and the full lifecycle fold to conversion. - docs/adr/ADR-0001: proposed (not accepted) Stage 0 stack choice, per the WP-0002-T01 human-accept gate — implementation proceeded against the proposal as the workplan note permits, but the task stays open. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
75 lines
2.6 KiB
Python
75 lines
2.6 KiB
Python
"""Canonical serialization, SHA-256 hash chaining, and Ed25519 signing.
|
|
|
|
Working default Q14 (specs/OpenQuestions-WorkingDefaults.md): hash chain is
|
|
SHA-256 over a canonical JSON serialization; signatures are Ed25519 over the
|
|
same canonical bytes. This module is the single source of truth for what
|
|
"canonical serialization" means so hashing and signing never drift.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from typing import Any
|
|
|
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
|
|
Ed25519PrivateKey,
|
|
Ed25519PublicKey,
|
|
)
|
|
|
|
GENESIS = "GENESIS"
|
|
|
|
|
|
def canonical_bytes(record: dict[str, Any]) -> bytes:
|
|
"""Deterministic byte representation of a record for hashing/signing.
|
|
|
|
Sorted keys, no insignificant whitespace, UTF-8. The `signature` and
|
|
`previous_entry_hash` fields are excluded because they are computed from
|
|
(or alongside) this representation, not part of what they attest to.
|
|
"""
|
|
payload = {
|
|
k: v
|
|
for k, v in record.items()
|
|
if k not in ("signature", "previous_entry_hash")
|
|
}
|
|
return json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
|
|
|
|
def entry_hash(record: dict[str, Any]) -> str:
|
|
"""SHA-256 hex digest of a ledger entry's canonical bytes."""
|
|
return hashlib.sha256(canonical_bytes(record)).hexdigest()
|
|
|
|
|
|
def verify_chain(entries: list[dict[str, Any]]) -> None:
|
|
"""Raise ValueError on the first broken link in an entry chain.
|
|
|
|
Entries must already be in ledger (append) order. The first entry's
|
|
`previous_entry_hash` must equal GENESIS; every subsequent entry's
|
|
`previous_entry_hash` must equal the SHA-256 of the prior entry's
|
|
canonical bytes.
|
|
"""
|
|
expected = GENESIS
|
|
for i, entry in enumerate(entries):
|
|
actual = entry.get("previous_entry_hash")
|
|
if actual != expected:
|
|
raise ValueError(
|
|
f"hash chain broken at entry index {i} (id={entry.get('id')!r}): "
|
|
f"expected previous_entry_hash={expected!r}, got {actual!r}"
|
|
)
|
|
expected = entry_hash(entry)
|
|
|
|
|
|
def sign_record(record: dict[str, Any], private_key: Ed25519PrivateKey) -> str:
|
|
"""Ed25519 signature (hex) over the record's canonical bytes."""
|
|
return private_key.sign(canonical_bytes(record)).hex()
|
|
|
|
|
|
def verify_record_signature(
|
|
record: dict[str, Any], signature_hex: str, public_key: Ed25519PublicKey
|
|
) -> bool:
|
|
"""True if signature_hex is a valid Ed25519 signature over record's canonical bytes."""
|
|
try:
|
|
public_key.verify(bytes.fromhex(signature_hex), canonical_bytes(record))
|
|
return True
|
|
except Exception:
|
|
return False
|