mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
Correctness:
- ledger: single-flight fcntl lock over read-modify-append + fsync. Two
concurrent writers previously could fork the hash chain (read same tail,
same prev_hash). New test races 8 threads x6 appends; chain stays intact
with contiguous indices.
- small-order list: the order-8 encodings were hand-typed and unverifiable
and diverged from the canonical libsodium blocklist. A BOGUS entry is the
only dangerous direction (it down-grades a real tamper to a note, skipping
the latch), so the list is now the certain-low-order set only (y in
{0,1,-1}, reduced/non-reduced, both sign bits); order-8 edges escalate to
tamper until a derived list lands. Fail-safe asymmetry documented + tested.
- freshness: removed a tautological .
Non-functional:
- quorum members now run concurrently (ThreadPoolExecutor): a verify costs
one member's latency, not the sum (~17ms for 4 members, live).
- Wallet.quorum() memoized per state_dir: binary swap-detection hashes run
once at assembly, not on every verify; documented rationale.
85 tests green; live 4-fork wallet re-verified end-to-end.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
221 lines
9.3 KiB
Python
221 lines
9.3 KiB
Python
"""Wallet core: ledger chaining, outbound firewall, latch, refusals.
|
|
|
|
Most tests seal a wallet by hand with fake verifier binaries so the
|
|
custody logic (hash-chained ledger, firewall quarantine, latch,
|
|
machine-actionable refusals) is exercised without cargo or the pinned
|
|
sources. A live integration test does the real R4 init against the built
|
|
quorum + fetched evidence when both are present, and skips otherwise.
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from pacta.quorum import binary_path
|
|
from pacta.signing import generate_ed25519_keypair
|
|
from pacta.wallet import AirgapSigner, PendingAirgap, Refusal, Wallet
|
|
|
|
|
|
def _fake_member(path: Path, verdict: str) -> None:
|
|
code = {"accept": 0, "reject": 1}[verdict]
|
|
out = {"accept": "OK", "reject": "INVALID"}[verdict]
|
|
path.write_text(f"#!/bin/sh\necho {out}\nexit {code}\n")
|
|
path.chmod(path.stat().st_mode | stat.S_IEXEC)
|
|
|
|
|
|
def _sha256(data: bytes) -> str:
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def _seal_wallet(tmp_path: Path, verdicts: dict[str, str], state_dir: Path) -> Wallet:
|
|
"""Create a wallet directory by hand: fake quorum members + a capsule
|
|
that pins their hashes, bypassing the (separately tested) R4 gate."""
|
|
state_dir.mkdir(parents=True, exist_ok=True)
|
|
members = []
|
|
for name, verdict in verdicts.items():
|
|
binary = binary_path(name, state_dir)
|
|
_fake_member(binary, verdict)
|
|
members.append({
|
|
"backend": name,
|
|
"component": f"{name}-ed25519-verified",
|
|
"semantics": "test",
|
|
"entry_point": "test",
|
|
"source_commit": "deadbeef" * 5,
|
|
"repo_commit": "cafe" * 10,
|
|
"binary_sha256": _sha256(binary.read_bytes()),
|
|
"backend_cfg": "test",
|
|
"risk_tier": "R4",
|
|
"evidence": {"leaf_hash": "00", "leaf_index": 0, "tree_size": 1,
|
|
"inclusion_proof": [], "sth": {"timestamp": "2099-01-01T00:00:00Z"}},
|
|
})
|
|
wallet_dir = tmp_path / "w"
|
|
wallet = Wallet(wallet_dir)
|
|
for sub in (wallet.keys_dir, wallet.incidents_dir, wallet.receipts_dir,
|
|
wallet.quarantine_dir, wallet.airgap_dir / "outbox", wallet.airgap_dir / "inbox"):
|
|
sub.mkdir(parents=True, exist_ok=True)
|
|
capsule = {
|
|
"type": "pacta.wallet.custody_capsule.v1",
|
|
"created_at": "2026-07-06T00:00:00Z",
|
|
"members": members,
|
|
"policy": {"require_unanimity": True, "min_members": 2, "require_tier": "R4",
|
|
"freshness_max_age_days": 0},
|
|
"signing": {"backend": "test"},
|
|
"problems_at_init": [],
|
|
}
|
|
capsule_bytes = json.dumps(capsule, sort_keys=True, separators=(",", ":")).encode()
|
|
wallet.capsule_path.write_text(json.dumps(capsule, indent=2, sort_keys=True) + "\n")
|
|
wallet._append_ledger("genesis", {"type": "pacta.wallet.ledger_genesis.v1",
|
|
"capsule_sha256": _sha256(capsule_bytes)})
|
|
generate_ed25519_keypair(wallet.keys_dir / "warden.key.pem", wallet.keys_dir / "warden.pub.pem")
|
|
wallet._test_state_dir = state_dir # type: ignore[attr-defined]
|
|
return wallet
|
|
|
|
|
|
def _payload_intent(payload: bytes) -> dict:
|
|
return {"purpose": "test signature", "payload_sha256": _sha256(payload)}
|
|
|
|
|
|
def test_ledger_is_hash_chained(tmp_path):
|
|
wallet = _seal_wallet(tmp_path, {"a": "accept", "b": "accept"}, tmp_path / "state")
|
|
payload = b"hello"
|
|
wallet.request_signature(_payload_intent(payload), payload, state_dir=wallet._test_state_dir)
|
|
ok, problems = wallet.verify_ledger()
|
|
assert ok, problems
|
|
# tamper with a middle entry and prove the chain catches it
|
|
lines = wallet.ledger_path.read_text().splitlines()
|
|
entry = json.loads(lines[1])
|
|
entry["body"]["intent"] = {"purpose": "SNEAKY"}
|
|
lines[1] = json.dumps(entry, sort_keys=True)
|
|
wallet.ledger_path.write_text("\n".join(lines) + "\n")
|
|
ok2, problems2 = wallet.verify_ledger()
|
|
assert not ok2 and problems2
|
|
|
|
|
|
def test_ledger_survives_concurrent_appends(tmp_path):
|
|
# The single-flight lock must keep the chain intact under parallel writers
|
|
# (two threads racing on the same wallet's ledger).
|
|
import threading
|
|
|
|
wallet = _seal_wallet(tmp_path, {"a": "accept", "b": "accept"}, tmp_path / "state")
|
|
start = threading.Barrier(8)
|
|
|
|
def worker(n):
|
|
start.wait()
|
|
for i in range(6):
|
|
wallet._append_ledger("stress", {"worker": n, "i": i})
|
|
|
|
threads = [threading.Thread(target=worker, args=(n,)) for n in range(8)]
|
|
for t in threads:
|
|
t.start()
|
|
for t in threads:
|
|
t.join()
|
|
ok, problems = wallet.verify_ledger()
|
|
assert ok, problems
|
|
# genesis + 48 appends, all with unique strictly-increasing indices
|
|
entries = wallet._ledger_entries()
|
|
indices = [e["index"] for e in entries]
|
|
assert indices == list(range(len(entries)))
|
|
assert len(entries) == 1 + 8 * 6
|
|
|
|
|
|
def test_outbound_firewall_releases_when_quorum_agrees(tmp_path):
|
|
from pacta.dogfood import locate_verifier
|
|
|
|
if locate_verifier() is None:
|
|
pytest.skip("dogfood signer not built")
|
|
wallet = _seal_wallet(tmp_path, {"a": "accept", "b": "accept"}, tmp_path / "state")
|
|
payload = b"release me"
|
|
result = wallet.request_signature(_payload_intent(payload), payload, state_dir=wallet._test_state_dir)
|
|
assert not isinstance(result, Refusal)
|
|
assert result["firewall"]["classification"] == "unanimous-accept"
|
|
|
|
|
|
def test_outbound_firewall_quarantines_and_latches(tmp_path):
|
|
from pacta.dogfood import locate_verifier
|
|
|
|
if locate_verifier() is None:
|
|
pytest.skip("dogfood signer not built")
|
|
# Members disagree on the produced signature -> quarantine + latch.
|
|
wallet = _seal_wallet(tmp_path, {"a": "accept", "b": "reject"}, tmp_path / "state")
|
|
payload = b"tampered path"
|
|
result = wallet.request_signature(_payload_intent(payload), payload, state_dir=wallet._test_state_dir)
|
|
assert isinstance(result, Refusal)
|
|
assert result.code == "FIREWALL_QUARANTINE"
|
|
assert wallet.latch_state()["latched"]
|
|
assert list(wallet.quarantine_dir.glob("*.json"))
|
|
# once latched, further outbound is refused with CUSTODY_LATCHED
|
|
again = wallet.request_signature(_payload_intent(b"x"), b"x", state_dir=wallet._test_state_dir)
|
|
assert isinstance(again, Refusal) and again.code == "CUSTODY_LATCHED"
|
|
# and the latched refusal is honestly unsigned
|
|
assert again.receipt["signature"]["status"] == "unsigned"
|
|
|
|
|
|
def test_unlatch_records_note(tmp_path):
|
|
wallet = _seal_wallet(tmp_path, {"a": "accept", "b": "reject"}, tmp_path / "state")
|
|
wallet._latch("test latch", "0000")
|
|
assert wallet.latch_state()["latched"]
|
|
wallet.unlatch("operator reviewed incident 0000, false alarm")
|
|
assert not wallet.latch_state()["latched"]
|
|
entries = [e for e in wallet._ledger_entries() if e["entry_type"] == "unlatch"]
|
|
assert entries and "false alarm" in entries[-1]["body"]["operator_note"]
|
|
|
|
|
|
def test_malformed_intent_refused(tmp_path):
|
|
wallet = _seal_wallet(tmp_path, {"a": "accept", "b": "accept"}, tmp_path / "state")
|
|
result = wallet.request_signature({"purpose": ""}, b"data", state_dir=wallet._test_state_dir)
|
|
assert isinstance(result, Refusal)
|
|
assert result.code == "MALFORMED_INTENT"
|
|
|
|
|
|
def test_intent_must_bind_payload(tmp_path):
|
|
wallet = _seal_wallet(tmp_path, {"a": "accept", "b": "accept"}, tmp_path / "state")
|
|
bad = {"purpose": "ok", "payload_sha256": "wrong"}
|
|
result = wallet.request_signature(bad, b"data", state_dir=wallet._test_state_dir)
|
|
assert isinstance(result, Refusal) and result.code == "MALFORMED_INTENT"
|
|
|
|
|
|
def test_binary_hash_change_is_detected(tmp_path):
|
|
wallet = _seal_wallet(tmp_path, {"a": "accept", "b": "accept"}, tmp_path / "state")
|
|
# Swap a member binary for a different one; the capsule pin must catch it.
|
|
_fake_member(binary_path("a", wallet._test_state_dir), "reject")
|
|
from pacta.wallet import WalletError
|
|
|
|
with pytest.raises(WalletError):
|
|
wallet.quorum(state_dir=wallet._test_state_dir)
|
|
|
|
|
|
def test_airgap_signer_parks_then_completes(tmp_path):
|
|
airgap = AirgapSigner(tmp_path / "airgap", wait_seconds=0.0)
|
|
payload = b"cross the gap"
|
|
with pytest.raises(PendingAirgap) as excinfo:
|
|
airgap.sign(payload, tmp_path / "unused.pem", request_id="req1")
|
|
assert (tmp_path / "airgap" / "outbox" / "req1.request.json").exists()
|
|
# simulate the device dropping a response
|
|
(tmp_path / "airgap" / "inbox" / "req1.response.json").write_text(
|
|
json.dumps({"signature_hex": "ab" * 64})
|
|
)
|
|
sig = airgap.sign(payload, tmp_path / "unused.pem", request_id="req1")
|
|
assert len(sig) == 64
|
|
assert excinfo.value.request_id == "req1"
|
|
|
|
|
|
def test_real_wallet_init_r4_gate(tmp_path):
|
|
"""The showcase: real R4 init against built binaries + example evidence."""
|
|
if not all(binary_path(b).exists() for b in ("dalek", "anza", "risc0", "betrusted")):
|
|
pytest.skip("real quorum not built")
|
|
evidence = Path("examples") / "wallet-evidence"
|
|
log_key = evidence / "log.pub"
|
|
if not evidence.exists() or not log_key.exists():
|
|
pytest.skip("example wallet evidence / log key not present")
|
|
wallet = Wallet.init(
|
|
tmp_path / "w",
|
|
evidence,
|
|
log_key,
|
|
trusted_provider="local-pacta-provider",
|
|
)
|
|
capsule = wallet.capsule()
|
|
assert len(capsule["members"]) >= 2
|
|
assert all(m["risk_tier"] == "R4" for m in capsule["members"])
|