mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
audit v3: paper-reality congruence + external-pointer integrity (Fable-5 Socratic pass)
Two Fable-5 inventory agents cross-checked every empirical claim in the paper against code/deployed log, and every external pointer against the live internet. Fixes on both sides: CODE (system brought up to the paper's claims): - SECURITY: pin-store mutation (incl. permanent poisoning) was reachable via receipts whose head signature FAILED verification in two of three consumer paths (attestation.py, cli.py) - an unauthenticated forged head at the pinned size could poison a consumer's pin forever and pollute the equivocation-evidence pair with an unverifiable head, contradicting SS5.4's 'validly signed' precondition and Prop 1. Both paths now gate the store on a verified Ed25519 head signature (logclient.py already did). Regression test added. - Prop 2 made literally true: _normalize_certificate now derives the cleanliness verdict purely from (observed cone, local allowed set) in EVERY branch; the operator's axiom_status label is never copied (was passed through for non-proven certs), missing cone => unverifiable always. Labels can deny, never grant. Test added. - webdocs: '/v1/sth-history: every head ever signed' -> 'the published head history'. PAPER (claims brought down to reality): - 'every head ever signed' -> the signed head history since publication began (heads for sizes 1-7 predate the mirror and were not retained). - Run-3 bullet: 'independently checkable by diffing the two commit trees' was no longer reproducible (pre-rewrite objects discarded); now states the log-internal corroboration (identical cert lists and cones across leaves 4-7 vs 8-11) and that tree diffs are not public. - Appendix A leaf block now actually verbatim: scheme openssl-ed25519, verified_backend serial/u64, real Lean version (4.30.0-rc2) instead of 4.x.y placeholder, leaf's actual axiom order (finalize/new/update), machine_protection note quoted, elisions marked; preamble wording matches. - Appendix C upstream boundary reordered to check.sh's verbatim order. - '27 lines - all annotation' -> honest description (axiom-list entries + operation reordering from one fork's black_box barrier). - Prop 2 proof + App A: status label consulted only negatively. - SS7: provenance fields noted as outside the signed payload; consumer chain relies on none of them. - Bibliography: all 20 entries verified against DBLP/RFC-editor - zero errors; added missing page numbers to 6 entries; thebibliography width 19->20. All URLs verified public; no PlanetMacro leakage. 17 pages, 106 tests green, accumulator untouched (tree_size 12). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
78eea14512
commit
2dae2ca0db
7 changed files with 179 additions and 55 deletions
BIN
paper/ltl.pdf
BIN
paper/ltl.pdf
Binary file not shown.
|
|
@ -1,7 +1,7 @@
|
|||
% LTL paper, revised version (v2) addressing ePrint acceptance criteria:
|
||||
% self-containedness, formal model, proofs for the system's own claims,
|
||||
% honest title, no marketing. Author TODOs are marked \authortodo{...}
|
||||
% and MUST be resolved before resubmission.
|
||||
% honest title, no marketing. All \authortodo markers have been resolved;
|
||||
% the macro remains defined in case future edits need it.
|
||||
\documentclass[11pt]{article}
|
||||
\usepackage[a4paper,margin=1.1in]{geometry}
|
||||
\usepackage{amsmath,amssymb,amsthm}
|
||||
|
|
@ -497,11 +497,13 @@ $(n', r')$:
|
|||
\item $n' < n_{\mathrm{pin}}$: reject (rollback).
|
||||
\end{itemize}
|
||||
A freshness policy bounds head age. The full log is also published as a git
|
||||
repository (one file per leaf, plus every head ever signed), so any cloner
|
||||
can recompute every prefix root from the public leaves and audit the
|
||||
complete head history without consistency proofs---a low-infrastructure
|
||||
witness mechanism~\cite{ct2}; a standalone ${\approx}150$-line
|
||||
standard-library verifier ships in the mirror.
|
||||
repository: one file per leaf, plus the signed head history since
|
||||
publication began (heads signed before the mirror existed were not
|
||||
retained). Any cloner can therefore recompute every prefix root from the
|
||||
public leaves and check every published head against its prefix root and
|
||||
signature without consistency proofs---a low-infrastructure witness
|
||||
mechanism~\cite{ct2}; a standalone ${\approx}150$-line standard-library
|
||||
verifier ships in the mirror.
|
||||
|
||||
\section{Security analysis}\label{sec:security}
|
||||
|
||||
|
|
@ -708,8 +710,12 @@ asserts.
|
|||
\end{proposition}
|
||||
\begin{proof}
|
||||
By construction of the consumer tooling: the verdict function takes
|
||||
$(\obs_a, \allowed)$ and ignores the label fields; an attestation lacking
|
||||
observed cones is mapped to \textsf{unverifiable}, not to a verdict.
|
||||
$(\obs_a, \allowed)$ and ignores the label fields in every branch; a
|
||||
certificate lacking an observed cone is mapped to \textsf{unverifiable},
|
||||
not to a verdict. The operator's proven/failed \texttt{status} label is
|
||||
consulted separately by the acceptance policy, but only
|
||||
\emph{negatively}---a certificate the operator does not itself mark proven
|
||||
can never count---so no operator assertion can upgrade any verdict.
|
||||
\end{proof}
|
||||
|
||||
\paragraph{What is \emph{not} proven.} Propositions~\ref{prop:pin} and
|
||||
|
|
@ -740,6 +746,10 @@ signing\_library\_leaf\_index: 8\\
|
|||
signing\_library\_certificates\_proven: 16/16\\
|
||||
self\_inclusion: verified
|
||||
\end{quote}
|
||||
(These provenance fields ride alongside the signature as operator-provided
|
||||
context; they are not part of the signed payload, and a consumer relies on
|
||||
none of them---the acyclic chain below rests only on the signature and the
|
||||
leaf's inclusion.)
|
||||
The signature vouches for the tree; the tree vouches for the code that
|
||||
produced the signature; and the two vouchings are different proof modalities
|
||||
(cryptographic and deductive), so the loop is self-referential without being
|
||||
|
|
@ -798,8 +808,12 @@ append-only ledger is the point of the system:
|
|||
changed their commit hashes. Because a leaf pins an exact commit
|
||||
(\S\ref{sec:leaves}), the operator re-ran the full corpus and appended
|
||||
fresh attestations at the new commits rather than editing leaves 4--7.
|
||||
The proof \emph{files} are unchanged across the rewrite---independently
|
||||
checkable by diffing the two commit trees---and the replay confirms it.
|
||||
That the proof \emph{files} survived the rewrite unchanged is
|
||||
corroborated from the log itself: leaves 4--7 and 8--11 carry identical
|
||||
certificate lists and identical observed axiom cones, re-checked by the
|
||||
kernel at both commit generations. (The pre-rewrite trees themselves are
|
||||
no longer distributed, so a direct tree diff is not among the public
|
||||
artifacts.)
|
||||
\end{itemize}
|
||||
|
||||
\noindent This last event is a live exercise of the append-only
|
||||
|
|
@ -906,9 +920,10 @@ byte-identical across all four; extraction-facing proof scripts diverge
|
|||
sharply where the forks' code or the extractor's naming differs (e.g., 215
|
||||
changed lines for the byte-parser proofs on the two forks whose extraction
|
||||
produces a closure-based loader; 121 lines for the signature-glue proofs on
|
||||
the same-crate fork; 27 lines---all annotation---between the two
|
||||
structurally closest forks, documenting one fork's \texttt{black\_box}
|
||||
optimization barrier). Per-target verification, in other words, is doing
|
||||
the same-crate fork; 27 lines between the two structurally closest forks,
|
||||
tracking one fork's \texttt{black\_box} optimization barrier---its
|
||||
axiom-list entries and the operation reordering it induces). Per-target
|
||||
verification, in other words, is doing
|
||||
measurable work exactly where the targets actually differ.
|
||||
|
||||
\section{Limitations}\label{sec:limitations}
|
||||
|
|
@ -959,7 +974,7 @@ check scripts.
|
|||
% Review every proof in Section 6 line by line and re-run every number in
|
||||
% Section 8 yourself.}
|
||||
|
||||
\begin{thebibliography}{19}
|
||||
\begin{thebibliography}{20}
|
||||
\itemsep2pt
|
||||
|
||||
\bibitem{ct1} B. Laurie, A. Langley, E. K\"asper. Certificate Transparency.
|
||||
|
|
@ -969,7 +984,7 @@ RFC 6962, 2013.
|
|||
Version 2.0. RFC 9162, 2021.
|
||||
|
||||
\bibitem{crosby} S. A. Crosby, D. S. Wallach. Efficient Data Structures for
|
||||
Tamper-Evident Logging. USENIX Security, 2009.
|
||||
Tamper-Evident Logging. USENIX Security, pp. 317--334, 2009.
|
||||
|
||||
\bibitem{dghs} B. Dowling, F. G\"unther, U. Herath, D. Stebila. Secure
|
||||
Logging Schemes and Certificate Transparency. ESORICS, LNCS 9879, pp.
|
||||
|
|
@ -980,17 +995,18 @@ Software Signing for Everybody. ACM CCS, pp. 2353--2367, 2022.
|
|||
|
||||
\bibitem{intoto} S. Torres-Arias, H. Afzali, T. K. Kuppusamy, R. Curtmola,
|
||||
J. Cappos. in-toto: Providing farm-to-table guarantees for bits and bytes.
|
||||
USENIX Security, 2019.
|
||||
USENIX Security, pp. 1393--1410, 2019.
|
||||
|
||||
\bibitem{coniks} M. S. Melara, A. Blankstein, J. Bonneau, E. W. Felten,
|
||||
M. J. Freedman. CONIKS: Bringing Key Transparency to End Users. USENIX
|
||||
Security, 2015.
|
||||
Security, pp. 383--398, 2015.
|
||||
|
||||
\bibitem{pcc} G. C. Necula. Proof-Carrying Code. ACM POPL, pp. 106--119,
|
||||
1997.
|
||||
|
||||
\bibitem{cheval} V. Cheval, J. Moreira, M. Ryan. Automatic verification of
|
||||
transparency protocols. IEEE EuroS\&P, 2023. arXiv:2303.04500.
|
||||
transparency protocols. IEEE EuroS\&P, pp. 107--121, 2023.
|
||||
arXiv:2303.04500.
|
||||
|
||||
\bibitem{easycrypt} G. Barthe, B. Gr\'egoire, S. Heraud, S. Zanella
|
||||
B\'eguelin. Computer-Aided Security Proofs for the Working Cryptographer.
|
||||
|
|
@ -1004,10 +1020,10 @@ Programming Language. CADE-28, LNCS 12699, pp. 625--635, 2021.
|
|||
|
||||
\bibitem{hacl} J.-K. Zinzindohou\'e, K. Bhargavan, J. Protzenko,
|
||||
B. Beurdouche. HACL*: A Verified Modern Cryptographic Library. ACM CCS,
|
||||
2017.
|
||||
pp. 1789--1806, 2017.
|
||||
|
||||
\bibitem{evercrypt} J. Protzenko et al. EverCrypt: A Fast, Verified,
|
||||
Cross-Platform Cryptographic Provider. IEEE S\&P, 2020.
|
||||
Cross-Platform Cryptographic Provider. IEEE S\&P, pp. 983--1002, 2020.
|
||||
|
||||
\bibitem{fiatcrypto} A. Erbsen, J. Philipoom, J. Gross, R. Sloan,
|
||||
A. Chlipala. Simple High-Level Code for Cryptographic Arithmetic---With
|
||||
|
|
@ -1040,10 +1056,10 @@ Each leaf is the canonical JSON serialization (sorted keys, no
|
|||
insignificant whitespace, UTF-8) of an attestation. Below is leaf~8 of
|
||||
the deployed log---the re-attestation of the upstream fork. The
|
||||
16-certificate array is elided to its first (foundational) and last
|
||||
(apex) entries, and long values (hashes, timestamps, version strings,
|
||||
paths) are shortened with ellipses; field names, structure, and the
|
||||
axiom lists are verbatim, and the unelided leaf is one \texttt{jq}
|
||||
invocation away in the public mirror.
|
||||
(apex) entries; long values (hashes, timestamps, version strings,
|
||||
paths) are shortened, and omitted fields are marked, with ellipses. The
|
||||
field names and values shown, and the axiom lists, are verbatim, and the
|
||||
unelided leaf is one \texttt{jq} invocation away in the public mirror.
|
||||
\begin{quote}\ttfamily\scriptsize
|
||||
\{ "type": "pacta.attestation", "schema\_version": 1,\\
|
||||
\hspace*{0.6em}"attestation": \{\\
|
||||
|
|
@ -1051,14 +1067,18 @@ invocation away in the public mirror.
|
|||
\hspace*{1.2em}"issued\_at": "2026-07-07T...Z",\\
|
||||
\hspace*{1.2em}"subject": \{ "component": "dalek-ed25519-verified",\\
|
||||
\hspace*{2.4em}"repo\_commit": "33fb8bb2311c70ead2e83c0...",\\
|
||||
\hspace*{2.4em}"repo\_url": ..., "verified\_backend": "serial" \},\\
|
||||
\hspace*{1.2em}"environment": \{ "lean\_version": "4.x.y",\\
|
||||
\hspace*{2.4em}"repo\_url": ..., "verified\_backend": "serial/u64",\\
|
||||
\hspace*{2.4em}... \},\\
|
||||
\hspace*{1.2em}"environment": \{\\
|
||||
\hspace*{2.4em}"lean\_version": "Lean (version 4.30.0-rc2, ...)",\\
|
||||
\hspace*{2.4em}"lake\_version": ..., "env\_script": ...,\\
|
||||
\hspace*{2.4em}"lean\_project\_dir": ... \},\\
|
||||
\hspace*{1.2em}"machine\_protection": \{ "lean\_guard": ...,\\
|
||||
\hspace*{2.4em}"note": "memory cap, core pinning, single-flight" \},\\
|
||||
\hspace*{2.4em}"note": "All Lean compiles route through the\\
|
||||
\hspace*{2.4em}repo's lean-guard (memory cap, core pinning,\\
|
||||
\hspace*{2.4em}timeout, single-flight lock) ..." \},\\
|
||||
\hspace*{1.2em}"replay": \{ "checked\_files": 64, "failed\_files": [],\\
|
||||
\hspace*{2.4em}"check\_ok": true, "axiom\_ok": true \},\\
|
||||
\hspace*{2.4em}"check\_ok": true, "axiom\_ok": true, ... \},\\
|
||||
\hspace*{1.2em}"certificates": [\\
|
||||
\hspace*{2.4em}\{ "name": "CurveFieldProofs.fieldImplementation",\\
|
||||
\hspace*{3.0em}"status": "proven", "axiom\_status": "clean",\\
|
||||
|
|
@ -1071,19 +1091,23 @@ invocation away in the public mirror.
|
|||
\hspace*{3.0em}"status": "proven", "axiom\_status": "clean",\\
|
||||
\hspace*{3.0em}"observed\_axioms": ["propext","Classical.choice",\\
|
||||
\hspace*{3.6em}"Quot.sound","ed25519.Signature","sha2.Sha512",\\
|
||||
\hspace*{3.6em}"verifying.sha512\_new","verifying.sha512\_update",\\
|
||||
\hspace*{3.6em}"verifying.sha512\_finalize\_bytes",\\
|
||||
\hspace*{3.6em}"verifying.sha512\_new","verifying.sha512\_update",\\
|
||||
\hspace*{3.6em}"ed25519.Signature.to\_bytes",\\
|
||||
\hspace*{3.6em}"signature.error.Error",\\
|
||||
\hspace*{3.6em}"signature.error.Error.new"] \} ],\\
|
||||
\hspace*{1.2em}"signature": \{ "scheme": "ed25519", ... \} \} \}
|
||||
\hspace*{1.2em}"signature": \{ "scheme": "openssl-ed25519", ... \} \} \}
|
||||
\end{quote}
|
||||
The \texttt{observed\_axioms} field is the exact output of
|
||||
\texttt{\#print axioms} for that theorem. Operator labels
|
||||
(\texttt{replay.check\_ok}, per-certificate \texttt{status} and
|
||||
\texttt{axiom\_status}) are recorded for the audit trail but are not
|
||||
inputs to any consumer verdict (Proposition~\ref{prop:verdict}); the
|
||||
consumer's verdict is $\obs = \allowed$ computed against its own table.
|
||||
\texttt{axiom\_status}) are recorded for the audit trail, but the
|
||||
cleanliness verdict is $\obs = \allowed$ computed against the consumer's
|
||||
own table in every case, with a missing cone mapped to
|
||||
\textsf{unverifiable} (Proposition~\ref{prop:verdict}). The
|
||||
\texttt{status} label is consulted only \emph{negatively}: a certificate
|
||||
the operator itself does not mark proven can never count toward
|
||||
acceptance, so labels can deny but never grant.
|
||||
|
||||
\section{The consumer verifier}\label{app:verifier}
|
||||
|
||||
|
|
@ -1139,8 +1163,8 @@ above, plus):
|
|||
this fork exposes SHA-512 as three streaming operations):
|
||||
\begin{quote}\ttfamily\scriptsize
|
||||
ed25519.Signature \quad sha2.Sha512\\
|
||||
verifying.sha512\_new \quad verifying.sha512\_update\\
|
||||
verifying.sha512\_finalize\_bytes\\
|
||||
verifying.sha512\_new \quad verifying.sha512\_update\\
|
||||
ed25519.Signature.to\_bytes\\
|
||||
signature.error.Error \quad signature.error.Error.new
|
||||
\end{quote}
|
||||
|
|
|
|||
|
|
@ -230,7 +230,7 @@ the proof-attested Ed25519 code path itself (<code>--require-verified-verifier</
|
|||
|
||||
<h2>API</h2>
|
||||
<pre>GET {base}/v1/sth latest Signed Tree Head
|
||||
GET {base}/v1/sth-history every head ever signed (witness material)
|
||||
GET {base}/v1/sth-history the published head history (witness material)
|
||||
GET {base}/v1/sth-consistency?first=N consistency proof from your pinned size
|
||||
GET {base}/v1/proof?component=NAME inclusion proof (artifact 3, freshly issued)
|
||||
GET {base}/v1/attestation?component=NAME the claim (artifact 2)
|
||||
|
|
|
|||
|
|
@ -129,6 +129,16 @@ def validate_attestation(
|
|||
if not fresh:
|
||||
diagnostics.append(error or "Signed tree head fails the freshness policy.")
|
||||
if sth_store_path:
|
||||
# The pin-store state machine (including permanent poisoning
|
||||
# on equivocation) only ever runs on a VALIDLY SIGNED head;
|
||||
# an unauthenticated head must not be able to mutate - let
|
||||
# alone poison - the consumer's pin.
|
||||
if receipt_result.signatures.get("ed25519") != "verified":
|
||||
transparency_evidence["sth_store"] = "skipped_unverified_head"
|
||||
diagnostics.append(
|
||||
"STH store: head signature did not verify; pin store not consulted or updated."
|
||||
)
|
||||
else:
|
||||
proof_hex = None
|
||||
if consistency_proof_path:
|
||||
raw_proof = load_data(consistency_proof_path)
|
||||
|
|
@ -190,13 +200,16 @@ def _normalize_certificate(cert: dict[str, Any], profile: Any) -> dict[str, Any]
|
|||
status = str(cert.get("status") or "unknown")
|
||||
observed = [str(a) for a in (cert.get("observed_axioms") or [])]
|
||||
expected = profile.expected_axioms_for(name)
|
||||
if status == "proven" and observed:
|
||||
# The cleanliness verdict is a function of (observed cone, local allowed
|
||||
# set) ONLY - in every branch. The provider's status label still gates
|
||||
# acceptance elsewhere (only status=="proven" certificates can count),
|
||||
# but it can only deny, never grant, and the provider's own axiom_status
|
||||
# label is never copied into the verdict.
|
||||
if observed:
|
||||
axiom_status = "clean" if sorted(observed) == sorted(expected) else "dirty"
|
||||
elif status == "proven":
|
||||
# proven with no observed axioms reported: cannot re-derive; distrust.
|
||||
axiom_status = "unverifiable"
|
||||
else:
|
||||
axiom_status = str(cert.get("axiom_status") or "not_checked")
|
||||
# no observed cone: nothing to re-derive from; distrust.
|
||||
axiom_status = "unverifiable"
|
||||
provider_verdict = str(cert.get("axiom_status") or "not_stated")
|
||||
return {
|
||||
"name": name,
|
||||
|
|
|
|||
|
|
@ -475,6 +475,7 @@ def cmd_receipt_verify(args: argparse.Namespace) -> int:
|
|||
sth_store=args.sth_store,
|
||||
consistency_proof_path=args.consistency_proof,
|
||||
max_sth_age_seconds=args.max_sth_age_seconds,
|
||||
head_signature_verified=result.signatures.get("ed25519") == "verified",
|
||||
)
|
||||
if args.require_verified_verifier and result.signatures.get("ed25519_backend") != "verified-dalek-serial":
|
||||
accountability_diagnostics.append(
|
||||
|
|
@ -575,6 +576,7 @@ def _log_accountability_checks(
|
|||
sth_store: str | None,
|
||||
consistency_proof_path: str | None,
|
||||
max_sth_age_seconds: int | None,
|
||||
head_signature_verified: bool = False,
|
||||
) -> list[str]:
|
||||
diagnostics: list[str] = []
|
||||
sth = receipt.get("sth") or {}
|
||||
|
|
@ -582,7 +584,13 @@ def _log_accountability_checks(
|
|||
fresh, error = check_sth_freshness(sth, max_sth_age_seconds)
|
||||
if not fresh:
|
||||
diagnostics.append(error or "Signed tree head fails the freshness policy.")
|
||||
if sth_store:
|
||||
if sth_store and not head_signature_verified:
|
||||
# Only a validly signed head may drive the pin-store state machine;
|
||||
# an unauthenticated head must not be able to poison the pin.
|
||||
diagnostics.append(
|
||||
"STH store: head signature did not verify; pin store not consulted or updated."
|
||||
)
|
||||
elif sth_store:
|
||||
proof_hex = None
|
||||
if consistency_proof_path:
|
||||
from .yamlio import load_data
|
||||
|
|
|
|||
|
|
@ -52,3 +52,58 @@ def test_signed_attestation_requires_public_key(tmp_path):
|
|||
assert not result.accepted
|
||||
assert card["risk"]["level"] == "R0"
|
||||
assert any("attestation-public-key" in item for item in result.diagnostics)
|
||||
|
||||
|
||||
def test_unverified_head_cannot_touch_pin_store(tmp_path):
|
||||
"""Regression for the paper's SS5.4 precondition: the pin-store state
|
||||
machine (including permanent poisoning) runs only on a VALIDLY SIGNED
|
||||
head. A forged head at the pinned size must not poison the pin."""
|
||||
import json
|
||||
|
||||
from pacta.signing import generate_ed25519_keypair
|
||||
from pacta_provider.transparency_log import TransparencyLog
|
||||
|
||||
key, pub = tmp_path / "log.key", tmp_path / "log.pub"
|
||||
generate_ed25519_keypair(key, pub)
|
||||
log = TransparencyLog(tmp_path / "log")
|
||||
log.init("example-proof-checker.invalid", pub)
|
||||
att_path = "examples/dalek-ed25519.attestation.yaml"
|
||||
receipt_path = tmp_path / "receipt.yaml"
|
||||
log.append_attestation(att_path, key, pub, receipt_out=receipt_path)
|
||||
|
||||
raw = load_attestation(att_path)
|
||||
store = tmp_path / "pins.json"
|
||||
kwargs = dict(
|
||||
path=att_path,
|
||||
trusted_provider="example-proof-checker.invalid",
|
||||
allow_unsigned=True,
|
||||
transparency_receipt_path=receipt_path,
|
||||
transparency_log_public_key_path=pub,
|
||||
sth_store_path=store,
|
||||
)
|
||||
|
||||
def _pin():
|
||||
return next(iter(json.loads(store.read_text())["logs"].values()))
|
||||
|
||||
# Control: a validly signed head reaches the store and pins.
|
||||
result = validate_attestation(raw, _repo(), **kwargs)
|
||||
assert store.exists(), result.diagnostics
|
||||
pinned = _pin()
|
||||
assert "poisoned" not in pinned
|
||||
honest_root = pinned["root_hash"]
|
||||
|
||||
# Attack: same size, different root => signature no longer verifies.
|
||||
# Before the fix this PERMANENTLY POISONED the pin (unauthenticated DoS).
|
||||
from pacta.yamlio import dump_data, load_data
|
||||
|
||||
forged = load_data(receipt_path)
|
||||
forged["sth"]["root_hash"] = "ab" * 32
|
||||
forged_path = tmp_path / "forged-receipt.yaml"
|
||||
dump_data(forged, forged_path)
|
||||
result = validate_attestation(raw, _repo(), **{**kwargs, "transparency_receipt_path": forged_path})
|
||||
assert not result.accepted
|
||||
assert any("pin store not consulted or updated" in d for d in result.diagnostics)
|
||||
pinned = _pin()
|
||||
assert "poisoned" not in pinned # the pin survived the forgery
|
||||
assert pinned["root_hash"] == honest_root
|
||||
assert result.evidence.get("sth_store") == "skipped_unverified_head"
|
||||
|
|
|
|||
|
|
@ -66,3 +66,27 @@ def test_full_fixture_scores_r4_and_partial_scores_r3(tmp_path):
|
|||
card = build_claim_card(partial, tmp_path, offline_fixture=True)
|
||||
assert card["risk"]["level"] == "R3"
|
||||
assert any("R4 requires the full apex tier set" in b for b in card["risk"]["blockers"])
|
||||
|
||||
|
||||
def test_verdict_never_copies_operator_labels():
|
||||
"""Prop 2 (verdict integrity), literally: the cleanliness verdict is a
|
||||
function of (observed cone, local allowed set) in EVERY branch. The
|
||||
operator's axiom_status label is never copied - not even for
|
||||
non-proven certificates - and a missing cone is always unverifiable."""
|
||||
profile = get_profile("ed25519", _repo())
|
||||
failed_flattered = {
|
||||
"name": "CurveFieldProofs.verify_accepts_iff",
|
||||
"status": "failed",
|
||||
"axiom_status": "clean", # operator flattery, must not pass through
|
||||
}
|
||||
out = _normalize_certificate(failed_flattered, profile)
|
||||
assert out["axiom_status"] == "unverifiable"
|
||||
assert out["provider_axiom_verdict"] == "clean" # recorded, not believed
|
||||
|
||||
failed_with_cone = {
|
||||
"name": "CurveFieldProofs.verify_accepts_iff",
|
||||
"status": "failed",
|
||||
"axiom_status": "clean",
|
||||
"observed_axioms": ["propext", "sorryAx"],
|
||||
}
|
||||
assert _normalize_certificate(failed_with_cone, profile)["axiom_status"] == "dirty"
|
||||
|
|
|
|||
Loading…
Reference in a new issue