proof-aware-crypto-tooling-.../src/pacta/sthstore.py

260 lines
10 KiB
Python
Raw Normal View History

Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
from __future__ import annotations
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .transparency import verify_consistency
STORE_TYPE = "pacta.transparency.sth_store.v1"
@dataclass(slots=True)
class SthCheckResult:
ok: bool
diagnostics: list[str] = field(default_factory=list)
action: str = "none" # pinned_first_use | matched | advanced | rejected
def evidence(self) -> dict[str, Any]:
return {
"transparency_sth_store_status": self.action if self.ok else "rejected",
"transparency_sth_store_diagnostics": self.diagnostics,
}
def load_store(path: str | Path) -> dict[str, Any]:
store_path = Path(path)
if not store_path.exists():
return {"schema_version": 1, "type": STORE_TYPE, "logs": {}}
raw = json.loads(store_path.read_text(encoding="utf-8"))
if not isinstance(raw, dict) or raw.get("type") != STORE_TYPE:
raise ValueError(f"Not an STH store: {store_path}")
raw.setdefault("logs", {})
return raw
def save_store(store: dict[str, Any], path: str | Path) -> None:
store_path = Path(path)
store_path.parent.mkdir(parents=True, exist_ok=True)
store_path.write_text(json.dumps(store, indent=2, sort_keys=True) + "\n", encoding="utf-8")
def check_sth_against_store(
sth: dict[str, Any],
store_path: str | Path,
consistency_proof_hex: list[str] | None = None,
consistency_from: dict[str, Any] | None = None,
update: bool = True,
now: datetime | None = None,
) -> SthCheckResult:
"""Split-view / rollback defense: compare a signed tree head against the
locally pinned view of the same log.
Policy:
- unknown log_id: pin it (trust-on-first-use, recorded as such);
- same tree_size: the root hash must match the pin exactly - two
different roots at one size is EQUIVOCATION, a hard, unrecoverable
rejection;
- larger tree_size: a consistency proof from the PINNED size is
required and must verify; only then does the pin advance;
- smaller tree_size: log rollback - hard rejection.
The consistency proof may come from the receipt itself (when the pinned
size equals the receipt's from_tree_size) or from the provider's
log-consistency command for older pins.
"""
diagnostics: list[str] = []
log_id = str(sth.get("log_id") or "")
if not log_id:
return SthCheckResult(False, ["Signed tree head has no log_id."], "rejected")
try:
tree_size = int(sth.get("tree_size"))
root_hash = str(sth.get("root_hash") or "")
assert root_hash
except (TypeError, ValueError, AssertionError):
return SthCheckResult(False, ["Signed tree head has invalid tree_size or root_hash."], "rejected")
store = load_store(store_path)
pinned = store["logs"].get(log_id)
stamp = (now or datetime.now(timezone.utc)).replace(microsecond=0).isoformat().replace("+00:00", "Z")
if pinned is None:
store["logs"][log_id] = {
"tree_size": tree_size,
"root_hash": root_hash,
"sth_timestamp": sth.get("timestamp"),
# The FULL signed head is retained, not just (size, root): if the
# log ever equivocates, the pinned head is one half of the
# transferable evidence pair (both are verifiable by anyone who
# holds the log's public key).
"sth": sth,
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
"first_seen": stamp,
"last_updated": stamp,
"trust_origin": "trust_on_first_use",
}
if update:
save_store(store, store_path)
return SthCheckResult(
True,
[f"Log {log_id[:16]}… pinned on first use at tree_size {tree_size} (trust-on-first-use)."],
"pinned_first_use",
)
# A poisoned pin is terminal: once this log has been caught presenting a
# split view, no later head - however consistent-looking - is accepted.
# The retained evidence pair survives restarts and is transferable.
if pinned.get("poisoned"):
poisoned = pinned["poisoned"]
return SthCheckResult(
False,
[
f"POISONED: this log was caught equivocating at {poisoned.get('at')} "
f"({poisoned.get('reason')}). The conflicting signed heads are retained in the "
"pin store as transferable evidence. This log must never be trusted again; "
"manual removal of the store entry is the only (deliberate) way back."
],
"rejected",
)
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
pinned_size = int(pinned["tree_size"])
pinned_root = str(pinned["root_hash"])
def _poison(reason: str) -> None:
pinned["poisoned"] = {
"at": stamp,
"reason": reason,
# both halves of the evidence: the head we pinned, and the head
# that contradicts it - each independently signature-verifiable.
"evidence": {
"pinned_sth": pinned.get("sth"),
"conflicting_sth": sth,
},
}
save_store(store, store_path) # evidence retention must not depend on `update`
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
if tree_size == pinned_size:
if root_hash == pinned_root:
return SthCheckResult(True, [], "matched")
_poison(
f"different root hash at the pinned tree size {pinned_size} "
f"(pinned {pinned_root[:16]}…, presented {root_hash[:16]}…)"
)
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
return SthCheckResult(
False,
[
"EQUIVOCATION: the log presented a different root hash at the pinned tree size "
f"{pinned_size} (pinned {pinned_root[:16]}…, presented {root_hash[:16]}…). "
"This log is maintaining a split view and must not be trusted again. Both signed "
"heads are retained in the pin store as transferable evidence; the pin is poisoned."
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
],
"rejected",
)
if tree_size < pinned_size:
return SthCheckResult(
False,
[
f"LOG ROLLBACK: presented tree_size {tree_size} is smaller than the pinned size "
REAL EVIDENCE: guarded replay of all four repos, attested, logged, dogfooded The provider ran its full honest replay against the four verified repositories on this machine - every Lean compile and axiom audit routed through lean-guard (memory-capped, core-pinned, single-flight, ~30 min per fork) - and the results are now shipped under evidence/: - 16/16 certificates proven per fork, every axiom cone boundary-exact (the four apex tiers carry their fork's documented SHA-512/wire boundary axiom-for-axiom), each attestation pinned to the exact repo commit (dalek 8ded7bc, anza 673c15e, risc0 98a13a6, betrusted 81f614a) and Ed25519-signed. - All four appended to the persistent transparency log. The log holds EIGHT leaves: the first four are the initial run's attestations, which honestly recorded an AUDIT FAILURE (the two pacta bugs fixed in e87f0e8) - an append-only trust ledger keeps its bad day, and the fixed run's leaves sit beside it. - Every receipt re-verified through the FULL stack: dogfood verifier (backend verified-dalek-serial recorded), STH pin store, freshness policy. Receipts are freshly issued against the final tree (a stale mid-run receipt tripped the pin store's rollback defense exactly as designed; the rollback diagnostic now hints at idempotent re-issue). - The capstone consequence ran for real: pacta agent with trusted provider + signature via the proven path + required receipt + pin store + --require-verified-verifier built the R4-gated library capsule from ATTESTED evidence (no local Lean replay needed by the consuming agent). Docs and teaching updated against the real artifacts: evidence/README (inventory + re-verify instructions), README "Real Evidence" section, lecture 5 now re-derives 16/16 verdicts from the REAL dalek attestation (signature checked on the proven path, provider labels ignored), and lecture 6 verifies all four REAL receipts and walks a fresh pin store over them. Every changed notebook cell executed before commit. 49/49 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:54:48 +00:00
f"{pinned_size}. Append-only logs never shrink. If this receipt is simply STALE "
"(issued before the log grew), request a freshly issued receipt for the same "
"leaf - the provider's log-append is idempotent and re-issues an inclusion "
"proof against the current tree."
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
],
"rejected",
)
# tree grew: demand a consistency proof from the pinned size
proof_hex = None
if consistency_from is not None:
from_size = int(consistency_from.get("from_tree_size", -1))
from_root = str(consistency_from.get("from_root_hash") or "")
if from_size == pinned_size:
if from_root != pinned_root:
_poison(
f"consistency anchor disagrees with the pinned root at tree_size {pinned_size} "
f"(pinned {pinned_root[:16]}…, anchor {from_root[:16]}…)"
)
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
return SthCheckResult(
False,
[
"EQUIVOCATION: the receipt's consistency anchor disagrees with the pinned root at "
f"tree_size {pinned_size}. The pin is poisoned and the evidence retained."
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
],
"rejected",
)
proof_hex = list(consistency_from.get("proof") or [])
if proof_hex is None and consistency_proof_hex is not None:
proof_hex = list(consistency_proof_hex)
if proof_hex is None:
return SthCheckResult(
False,
[
f"The log grew from pinned size {pinned_size} to {tree_size} but no consistency proof "
"from the pinned size was supplied. Obtain one (provider: log-consistency "
f"--from-size {pinned_size}) - growth without proof is indistinguishable from a split view."
],
"rejected",
)
try:
proof = [bytes.fromhex(item) for item in proof_hex]
old_root = bytes.fromhex(pinned_root)
new_root = bytes.fromhex(root_hash)
except ValueError as exc:
return SthCheckResult(False, [f"Consistency proof contains invalid hex: {exc}"], "rejected")
if not verify_consistency(pinned_size, tree_size, old_root, new_root, proof):
return SthCheckResult(
False,
[
f"Consistency proof from pinned size {pinned_size} to {tree_size} does NOT verify: "
"the new tree is not an append-only extension of the pinned tree."
],
"rejected",
)
pinned.update(
{
"tree_size": tree_size,
"root_hash": root_hash,
"sth_timestamp": sth.get("timestamp"),
"sth": sth,
Log accountability: STH pinning, consistency enforcement, freshness, monitor A transparency log without split-view defense is just a signature with extra steps: the provider could serve one tree to the agent and another to the world, or roll the log back, and standalone receipt verification would never notice. The primitives (RFC 9162 consistency proofs) were already implemented and correct; this closes the loop on the AGENT side. - src/pacta/sthstore.py: a local STH pin store. Unknown log -> pin (trust-on-first-use, recorded as such). Same tree size -> the root must match the pin byte-for-byte; a mismatch is named EQUIVOCATION and is a hard rejection. Larger tree -> a consistency proof FROM THE PINNED SIZE is required and verified before the pin advances (receipts already embed a from-previous anchor; the anchor's root is itself checked against the pin so a lying anchor cannot bridge a split view). Smaller tree -> LOG ROLLBACK, hard rejection. - Freshness policy: --max-sth-age-seconds rejects stale (or future-dated) tree heads - an old-but-valid STH can hide later entries. - Wired into receipt-verify, claims, and agent (--sth-store, --consistency-proof, --max-sth-age-seconds); evidence records the pin action; any accountability failure fails the receipt closed. - Provider: log-consistency --from-size N (serve proofs for pinning agents whose pin is older than the receipt's embedded anchor) and log-audit (monitor self-check: recompute the tree, verify the stored STH and per-entry leaf hashes). Live drill in this commit's validation: pin-on-first-use -> matched -> grown-with-proof advance -> a real forged same-size split view REJECTED with the equivocation diagnostic -> freshness rejection -> clean self-audit. tests/test_sthstore.py covers pin/match/equivocation, growth-without-proof, lying consistency anchors, rollback, freshness. 45/45 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 08:08:34 +00:00
"last_updated": stamp,
}
)
if update:
save_store(store, store_path)
return SthCheckResult(
True,
[f"Pin advanced {pinned_size}{tree_size} with a verified consistency proof."],
"advanced",
)
def check_sth_freshness(
sth: dict[str, Any],
max_age_seconds: int,
now: datetime | None = None,
) -> tuple[bool, str | None]:
"""Stale-root defense: an old-but-valid STH can hide later log entries
(or later revocations). Policies that require freshness reject tree
heads older than max_age_seconds."""
raw = sth.get("timestamp")
if not raw:
return False, "Signed tree head has no timestamp; freshness policy cannot be evaluated."
try:
stamp = datetime.fromisoformat(str(raw).replace("Z", "+00:00"))
except ValueError:
return False, f"Signed tree head timestamp is not ISO 8601: {raw!r}"
if stamp.tzinfo is None:
stamp = stamp.replace(tzinfo=timezone.utc)
current = now or datetime.now(timezone.utc)
age = (current - stamp).total_seconds()
if age < 0:
return False, f"Signed tree head timestamp is {int(-age)}s in the future; clock skew or forgery."
if age > max_age_seconds:
return False, (
f"Signed tree head is {int(age)}s old, beyond the freshness policy of {max_age_seconds}s. "
"Request a fresh tree head from the provider."
)
return True, None