Option C. A verifier bundle over a provenance stream carries provenance_root
(Merkle over one leaf per event: seq_no, capsule_root, installation, key,
assurance, occurred_at, under attesto.provenance.v1.bundle_tree), the event
count and vault_key_lifecycle, all conditional so legacy bundle hashes are
unchanged. Rust is normative (edge/src/bundle_tree.rs, nine golden vectors);
Python, Go and TypeScript verify an inclusion and apply the frozen revocation
rule against the receipt time offline. The inclusion endpoint in router.py
lands with the next commit, which carries the shared router edits.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Python derived the assurance ladder and the other two clients did not, so a
TypeScript verifier -- which is what the product UI is -- had no way to
present it without inventing one. The rule that L3 is derived and never
signed only holds if every client applies it, so this is the property rather
than tidiness.
All three now report four facts kept apart: what the vault signed, whether a
quorum was met, whether an anchor confirmed, and what a verifier may
therefore report. A single badge would hide which of them was observed, and
that matters most exactly when one is missing.
Each carries the two asymmetries in its own tests. A witness outage withholds
L3 without reducing what the vault signed, because event-time assurance is a
fact about the past that no later outage changes. And an anchor never
promotes anything -- the report says so out loud, so a reader does not infer
it did.
The nine-case table is enumerated in each language, which is the only way two
implementations of a rule this narrow can be shown to agree. Python and
TypeScript were checked against each other directly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
noble rejects multiply(0n) exactly as libsodium rejects a zero scalar, and
the TypeScript SDK read the refusal as an invalid opening the same way
Python did. Both now multiply a zero scalar to the identity by hand.
gtank/ristretto255 accepts it, so the Go module was correct all along. Three
implementations: two agreed with each other and both were wrong, and the one
that matched the Rust core stood alone. That is the argument for a shared
corpus rather than per-language tests -- two implementations agreeing is not
evidence.
The zero vector is now consumed by all three, so the coverage contract holds
every client to it: 27 of 27 in Python, Go and TypeScript.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All three clients now verify a presentation the Local Vault really built through
the real edge core. The fixture is checked in rather than written to satisfy the
verifiers: three implementations agreeing with each other proves less than three
agreeing with the producer.
The fixture is generated once and not regenerated on every run — a disclosure
carries fresh randomizers and a fresh signature, so comparing regenerated bytes
would fail by design. Drift is caught the other way round: the Local Vault's own
verifier checks the checked-in fixture, so a format change makes the producer
reject its own past output. CI runs that.
`bytesForSubtle` and `hexToBytes` move from private to exported in the
TypeScript proofstream module rather than being duplicated. Two hex decoders
that could disagree is a worse outcome than one shared internal helper.
Drift testing found that **nothing tested inclusion at all**. Removing the
two-hop check left every disclosure test passing in all three languages: a
tampered value was caught by the commitment check, a tampered signature by the
signature check, but a leaf belonging to an entirely different capsule would
have been accepted. That is the one thing a disclosure is for. Each SDK now has
a test that corrupts a sibling in the subtree path and another in the top path,
leaving value and randomizer untouched so only the fold can catch it.
Corpus coverage 26/26 and 12/12 in all three languages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The eight private-numeric vectors were a declared boundary: verifying them needs
ristretto255 scalar arithmetic no SDK carried. Adding one everywhere would have
cost something real — the Go and TypeScript SDKs have *zero* dependencies, which
is a property their consumers get for free today.
So it is optional in each, and the shape differs per ecosystem: a `attesto[zk]`
extra in Python, an optional peer dependency in TypeScript, and a separate
`go.attesto.eu/sdk/zk` module in Go. A consumer who never opens a private
numeric inherits nothing. `not_checked` now means "this installation did not
check" rather than "nobody can", which is a better answer to the same question.
Each was verified against a real Rust commitment before being chosen: pysodium
over libsodium, @noble/curves, and gtank/ristretto255 all reproduce the core's
bytes exactly. PyNaCl was tried first and ruled out — 1.6.2 exposes no
ristretto255 bindings at all.
This closes Pedersen opening verification, not range proofs. A range proof needs
a full bulletproofs implementation, not curve arithmetic, and stays the core's
job.
Two things the last vector forced:
* A value outside the descriptor's declared domain now returns invalid for the
right reason. The commitment would fail to match anyway, but attributing that
to the arithmetic when the real answer is "that value is outside the declared
domain" blames the wrong layer. All three match the Rust core here.
* A skipped suite is a gate that proves nothing, so CI sets
ATTESTO_REQUIRE_ZK_EXTRA and an environment that was supposed to install the
dependency and did not now fails instead of reporting green over skips.
Corpus coverage is 25/25 and 12/12 in all three languages, with no exemption
left. The exemption mechanism is removed rather than emptied: reintroducing one
should be a visible decision, not a constant someone left lying around.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dependency scan reported 14 high/critical findings across the backend and
the marketplace frontend. All of them are now closed, and the scan is green for
the first time.
**Bumped, with the suite as the check.** aiohttp 3.14.1 -> 3.14.3, pyasn1 0.6.3
-> 0.6.4, pydantic-settings 2.14.1 -> 2.15.0, and cryptography 48.0.1 -> 50.0.0.
That last one crosses two majors, which is why it was flagged as blast radius
rather than a routine bump; the full backend suite passes unchanged. nanoid and
postcss in the marketplace frontend are patched and the frontend still builds.
**ecdsa has no fix and never will.** CVE-2024-23342 is a Minerva timing attack on
P-256, and the project considers side channels out of scope. It arrives through
python-jose, and only signing, key generation and ECDH are affected —
verification is not. The backend signs tenant tokens with the symmetric
JWT_SECRET, so only HMAC families are coherent there anyway.
That was true by habit, not by construction: `jwt_algorithm` had no validation at
all, so JWT_ALGORITHM=ES256 would have signed through the vulnerable path with
nothing to say so. app/core/security.py now refuses any algorithm outside
HS256/HS384/HS512, on both the encode and decode paths, and
tests/test_jwt_algorithm_guard.py fails if that control is removed. `none` is
refused alongside ES*: an unsigned token is not a lesser problem than a badly
signed one.
The advisory is accepted by exact ID with that control named, using a mechanism
added here rather than by silencing the tool. A new advisory on ecdsa still
fails, and a package whose every finding is accepted stops being listed as
vulnerable so the field keeps meaning something.
Backend 1419 passed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The statement is what gets folded into the proof transcript, and the width is
derived from its bounds. A client that ordered the fields differently or picked
a different width would produce proofs nobody else could verify — and the
symptom would read as a broken proof rather than a divergent implementation.
Both are pure arithmetic and canonical JSON, so every SDK can check them and now
does.
Each client gains `zk_range_width` and `validate_range_statement`. The field set
is exact rather than a minimum: an extra field would bind to nothing and a
missing one would change the challenges. A float bound is refused rather than
truncated, which is the encoding registry's whole purpose one layer up.
The width table is checked in as a vector and the Rust core asserts against that
file directly rather than against a second copy of the table. Changing one now
fails the other, which a duplicated constant would not have done.
Corpus coverage: 17/17 provenance and 12/12 zk-range in Python, Go and
TypeScript. The cross-SDK vector item is closed; Sprint 12 is down to three open
items, all of which need something local work cannot supply — other
architectures, a curve-library decision, and a UI.
Python 107, Go ok, TypeScript 119, Local Vault 375, edge 117.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sprint 12's own evidence named this the feature's largest risk: not a broken
proof, but a correct proof rendered as "AI generated: true". The SDKs had no
result surface at all — only cryptographic primitives — so nothing stopped a
consumer from reading a predicate result however it liked.
All three now carry `inspect_predicate_result`, and the rule that shapes it is
that a client without ristretto255 arithmetic cannot verify a range proof and
must say so. `verified_here.zk_predicate` is always `not_checked`; the issuer's
own verification block is carried separately under `reported_by_issuer`. An SDK
that merged the two would be passing the prover's word through as though it had
confirmed it, which is the failure the whole construction exists to prevent.
Inclusion, which is SHA-256, is reported as genuinely checked when the caller
checked it.
A result is refused outright if it omits one of the three non-claims or carries
a field a consumer could render as a verdict — at any nesting depth, since
`predicate.confidence` misleads exactly as well as a top-level one.
The corpus is generated by calling the real evaluator, so the fixture cannot
drift from the implementation, and a contract compares rather than regenerates.
Extending the coverage contract to a second corpus surfaced a third one:
`provenance-envelope-v0.1` matched nothing. It turned out to be guarded a
different but equally strict way — its own contract pins an explicit inventory —
so the contract now models both shapes. "Checked somewhere else" and "checked by
nobody" can no longer look the same, and a new corpus fails until one model or
the other covers it.
Python 100, Go ok, TypeScript 115, Local Vault 375.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twelve of twenty-five provenance vectors were consumed by no SDK. From outside
the repository that looked exactly like full coverage, which is the problem: a
corpus proves nothing about an implementation that never loads it.
Vectors now declare what they require. `sha256` vectors use SHA-256 and
canonical JSON, which all three SDKs have, so an unconsumed one is a gap and
fails a contract. `ristretto255` vectors need curve scalar arithmetic no SDK
carries; those are a declared boundary with a stated reason rather than a silent
skip, so the exemption cannot spread by habit.
Coverage went from 13/17 reachable in each SDK to 17/17. Closing the four gaps
surfaced a real verifier weakness: `capsule_root` receives digests, so by then a
role is no longer visible, and a tree carrying `evidence_root` twice with
`vault_identity_commitment` missing folds to a root all three SDKs accepted.
Each gains `ordered_top_leaf_digests`, which requires each of the six roles
exactly once, and the safe path is now the easy one.
Two findings of my own drift:
* The Go corpus-typing test accepted only `valid` and `invalid`, so it had been
failing since the Sprint 1 recovery added vectors carrying `differs` and
`rejected`. I updated Python's typing test then and not Go's, and no gate
caught it because the SDK parity suites are not in the sprint gates. Fixed,
and both Go and TypeScript now also require the capability declaration.
* TypeScript's strict indexing caught that a missing randomizer would have
reached the hash as the string "undefined". Both halves are now checked.
Python 88, Go ok, TypeScript 107, Local Vault 375, edge 116, backend 1404.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sprint 1 recovery, first item. §5.2 of REVIEW-02 adds three domains the registry
did not carry: attesto.disclosure.v2, attesto.zk.range.v1.statement and
attesto.zk.range.v1.transcript.
They are not in the attesto.provenance.v1. namespace, and that is deliberate:
disclosure v2 and the ZK range protocol are separate protocols with their own
versions, so a preimage space is named after the protocol that owns it rather
than the one it happens to travel with.
That namespace difference meant the parity contract could not see them at all —
its pattern matched attesto.provenance.v1.* only, so three new domains would have
been silently unguarded. The pattern now names each protocol explicitly rather
than loosening to a prefix wildcard: a looser first attempt also matched prose
that mentions a namespace without a terminal segment and reported it as an
unknown domain.
All four registry locations updated together with the golden vector, and all
three SDK parity suites plus the contract are green. The Go failure message was
also corrected: it printed "rust=21 go=21" while failing on a third hardcoded
expectation it never named.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Establishes the single normative cryptographic authority for the provenance
lane, and freezes the boundary and Merkle semantics before any ingestion path
exists to depend on them.
New crate edge/ (attesto-edge)
- domains.rs — the closed 18-domain v1 registry. Unknown domains are errors,
never a fallback: a generic attesto.provenance.v1.commitment would let two
unrelated objects share a preimage space, which is what domain separation
exists to prevent.
- canonical.rs — conforming ATTESTO-CANONICAL-JSON-001, not a second
serializer. Floats and integers past 2^53-1 are refused with their JSON path.
- commitment.rs — randomized, domain-separated commitments. Legacy Proofstream
commitments stay deterministic; provenance values are low-entropy, so
claim_type = "c2pa_manifest_valid" hashed deterministically is a dictionary
lookup and a deterministic asset digest links a file across events. Debug for
Randomizer prints <redacted>: it is C1 and Debug output reaches logs.
- merkle.rs — the two-level capsule forest. A claim leaf cannot verify against
evidence_root on two independent grounds: subtrees fold under different node
domains, and the top leaf binds leaf_role. Odd nodes are promoted, never
duplicated, matching the rule inclusion.json already pins for Proofstream.
- boundary.rs — derives nothing. It shapes a request for attesto-nova, reusing
the existing event-payload 16 KiB size class so Nova's closed
boundary_max_len() allowlist needs no new entry. On-wire artifact is N10.R
redacted.
- main.rs — NDJSON surface (handshake, canonicalize, commit, capsule-root,
boundary-derive, self-test), the transport the backend already speaks.
Poseidon is deliberately absent. It stays in proofs/nova, reached through that
crate's public boundary API, so there remains exactly one Poseidon authority.
The only Nova change is making CIRCUIT_ID and PROVER_VERSION pub so the edge
handshake can report the prover it wraps; its 40 tests are unchanged.
Test-only randomizers are gated behind the `test-vectors` cargo feature and
compiled out of release builds. A caller who can choose the randomizer can make
production commitments deterministic — that is not a debug convenience, it is
the vulnerability. A release build refuses one and reports
accepts_caller_randomizers: false in its handshake.
Conformance
- golden-vectors/provenance-v0.1-dev/ — 14 Rust-authored vectors, 9 valid and 5
invalid. CI regenerates them and requires git diff --exit-code, so the
committed corpus cannot drift from what the normative core produces.
- Python (sdk/python/src/attesto/provenance.py, 19 tests) and Go
(sdk/go/provenance.go, 14 tests) reproduce every valid vector and refuse every
invalid one. Both reuse their existing canonical-JSON primitives rather than
forking a second implementation.
- provenance_domain_registry_contract.py pins Rust = spec = Python = Go =
vector, and that no registry declares the forbidden fallback. It reads each
declaration block rather than whole files, so the negative test cases that
must name the fallback do not trip it.
TypeScript parity is still owed and Sprint 1's DoD is not fully closed: the
sdk/typescript build break recorded in the Sprint 0 baseline makes its whole
suite unrunnable.
Also fixes a Sprint 0 guard found by the guard itself: the naming lint scanned
only tracked files, so new work read green until it was committed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Brings the published packages level with the code shipped since the last
publish (offline-verify exports, attestedFetch, OTel bridges, portable
receipts, head-tracking fix, etc. for the SDK; init+doctor for local-vault).
Versions move in lockstep as the publication-parity contract requires:
PyPI/npm/Go SDK/CLI all 0.4.0. n8n node bumps to 0.2.0 with its @attesto/sdk
dep widened to ^0.4.0. attesto-mcp stays 0.1.0 (first publish).
No package contains source maps or non-runtime source: npm ships compiled
.js + .d.ts only (zero .ts, zero .map, verified), Python wheels ship runtime
.py only (no sdist, no tests), and no wheel/tarball contains anything from
backend/, gateway/, or the Rust prover.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nova_e2e_contract's cargo fmt --check now passes (formatting from the
Plan B circuit work), and the two scanner-flagged test fixtures use
allowlisted fake-markers: the gateway test provider key carries "dummy"
and the Go emulator API key is atto_test_abc123... (valid 32-hex,
"abc123" marker). Gateway + Go suites green; secret scan 0 findings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`attesto connector init <slug> [--name --category --dir]` generates
attesto.connector.json (v2 manifest), webhook_handler.py wired to the
P1.4 verify_webhook helper with its real signature, and a README with the
submission flow; `--validate-only <dir>` re-runs the marketplace
validator (the same connectorkit.ValidateManifest code) as a local
pre-submission check.
Honesty rule: a fresh scaffold cannot claim a green assurance canary, so
runtime.canary ships as "pending" and the validator's single remaining
finding IS the submission to-do list; the scaffold errors if its template
ever drifts into any other finding. Verified end-to-end: generated stub
accepts a genuinely signed webhook and rejects a forged signature against
the published Python SDK; overwrite refusal tested; Go suite green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AttestoSpanProcessor (attesto.otel / @attesto/sdk) turns ended OTel spans
into commitment events: source_ref otel:{trace_id}:{span_id} so resending
a span is idempotent, only allowlisted attributes committed (as a
commitment, never raw — non-allowlisted values provably absent from
stored objects), fail-open with onError, strict opt-in. Both
implementations are structurally compatible with the SpanProcessor
interface, so neither SDK gains an opentelemetry dependency.
Building this surfaced two real gaps, fixed in all three languages:
- Emulators now deduplicate on (source_kind, source_ref) like real
ingestion (resend returns the existing receipt; anonymous empty refs
exempt) — previously a resend silently appended a duplicate event.
- P1.6 head tracking treated an exact idempotent replay (same seq_no AND
same event_hash as the stored head) as a fork; it is now a benign no-op,
while same-seq/different-hash remains AttestoForkDetected (regression
tests in Python, TypeScript-path via emulator test, and Go).
Suites: Python 109 passed, TypeScript 77 passed, Go 4/4 packages ok.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Portable receipt export (*.attesto.json): export_receipt_file /
verify_receipt_file in Python, exportReceiptFile / verifyReceiptFile in
TypeScript, ExportReceiptFile / VerifyReceiptExport in Go, plus
`attesto verify file` in the CLI. New normative corpus
golden-vectors/sdk-parity/receipt-export.json (valid, tampered-inner,
linkage-mismatch, wrong-format, embedded-hint-only) passes identically in
all three SDKs; a Python-made export verifies through the Go CLI
end-to-end. Embedded witness keys are explicit second-class hints
(kind=receipt-export-selfcontained).
attestedFetch (TS) attests AI calls at the transport exactly like the
gateway: OpenAI-compatible paths -> attesto.model_decision with
commitments only (SSE reassembled after byte-for-byte pass-through),
anything else -> http_call; fail-open by default with onError, strict
rejects; attest() wraps any function with a commitment event +
lastReceipt. 5 emulator tests prove raw prompt/completion text never
appears in any stored object.
Edge runtimes: new guard test fails the build if any node: builtin enters
the dist/index.js module graph (FileHeadStore stays out by design), and
the receipt+export corpora now run on Bun in CI (10 cases green locally).
render_receipt_pdf ships behind the attesto[receipt-pdf] extra (fpdf2 +
qrcode, pure Python; core stays light) — one-page rendering with a QR of
{receipt_hash, event_hash} and a disclaimer that the JSON, not the PDF,
is the evidence; clean ImportError naming the extra when absent.
Also fixed a stale CI assertion: the npm package-install smoke pinned
SDK_VERSION 0.1.1; it now reads the version from package.json.
Suites: Python 106 passed, TypeScript 67+5 passed, Go green, package
policy contract green. Connectorkit already exists in all three languages
(no port needed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sdk/go/cmd/attesto-verify-wasm compiles the offline verification functions
(receipt, inclusion, checkpoint root, completeness) — and nothing else —
to WebAssembly, exported on a global attestoVerify object.
scripts/build_wasm_verifier.sh prefers TinyGo and falls back to Go stdlib
(current build: stdlib, 5.9 MB; the <4 MB target applies when TinyGo is in
the toolchain). docs-site /verify is a drag-drop page that verifies
receipts entirely in the browser against a user-pinned witness key.
Verified, both wired into CI as a new wasm-verifier job:
- scripts/wasm_verifier_smoke.mjs loads the wasm in Node with no network
and reproduces all 19 sdk-parity corpus cases (receipts + inclusion +
checkpoint-root + completeness) — the same corpus gating the three SDKs;
- the smoke also asserts the /verify page is zero-network: its only fetch
is the same-origin wasm asset and no script references an absolute URL.
wasm + page hashed into the release manifest; docs-hub contract green
(shared chrome + content rules).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ATTESTO-CANONICAL-JSON-001 freezes the byte-level rules every hash and
signature depends on (normalization table, no-whitespace serialization,
FIX-11 number policy, domain framing) and names golden-vectors/sdk-parity/
as the normative conformance corpus with a 6-step checklist for new
implementations; hashed into the release manifest and linked from all
three SDK READMEs + the crypto review checklist.
ADR-0006 (client countersignatures) specifies the full P4.1 scheme —
signed bytes under attesto.v2.client-event over commitments, kid registry
with rotation-safe resolution at occurred_at, replay analysis, binding
claim wording — status proposed; no code until approved (P4 rule).
ADR-0009 (independent witness network) records the W.1 design: verbatim
purpose line, privacy-preserving framing rule, hashes-only observation,
opt-in pseudonymous stream digests, the four CI-enforced separation rules
(zero SDK coupling, never a transitive dep, never auto-enroll, never
background on install), backend surface spec, v1 observational-only scope,
and the claims-guarded evolution note kept ADR-internal.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Keyless OIDC signing is unavailable off GitHub, so releases are signed with a
managed cosign key: the private half lives only in the operator keystore and
the CI secret (COSIGN_KEY); the public half is pinned in-repo at
ops/release-signing/cosign.pub and served at https://get.attesto.eu/cosign.pub.
scripts/sign_release_artifacts.sh signs dist/cli/SHA256SUMS (classic detached
signature; cosign v3 flags pinned), verifies its own output against the
in-repo public anchor before declaring success, and normalizes the signature
to world-readable. The CI cli-release-binaries job now signs on every v* tag
and FAILS CLOSED when the secret is missing — no unsigned release can ship.
The live 0.3.0 release on get.attesto.eu is signed and the full public
auditor path is verified end-to-end: download SHA256SUMS + .sig + cosign.pub
from get.attesto.eu, cosign verify-blob -> Verified OK. "Verify this SDK
before you trust its verifier" commands added to the Go README and to the
Due-Diligence publication evidence (contract green).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Typed events as SDK-side conventions (no backend change): ModelDecision /
HumanOverride / IncidentReport (NIS2 field names) / DataAccess as Python
dataclasses, TypeScript builders, and Go structs — each serializing to a plain
payload with regulation_refs (EU AI Act Art.12/14, NIS2 Art.23, AI-Act Art.62,
GDPR Art.30/6) and self-validating against the committed-payload number policy.
Python ergonomics: @attest(client, stream_id=...) wraps any function — one
event per call with commitments over args/kwargs and result (raw values never
leave the process), .last_receipt on the wrapper, exceptions log an
IncidentReport-shaped event (commitment over the traceback) and re-raise;
logging failures never break the workload (log-and-continue; strict=True is
the only raising mode — all test-enforced). session(...) groups typed events
under shared session_id/actor_ref metadata.
Evidence report: attesto.reports.article12(...) in Python and
`attesto report article12 --stream ... --output report.md` in the Go CLI —
deterministic templating (never LLM-generated) built only from existing tenant
endpoints: Art.12(2) coverage table, per-type event counts, P1.3 completeness
verdict, checkpoint -> anchor-tx -> block path, and replayable verification
commands. Claims discipline test-enforced in both languages: the words
"compliant"/"compliance guaranteed" never appear — the report states evidence
recorded and independently verifiable. The mock emulators now expose
event_type in tenant listings so report tests run end-to-end against P2.3.
Sweep green: Python 94, TS 59, Go all packages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Customers can now test their full ingest-and-verify pipeline in CI with zero
network and zero Attesto account. Python attesto.testing.MockAttesto (context
manager over a local HTTP server + pytest-fixture friendly), TypeScript
createMockServer() (fetch-compatible handler, WebCrypto Ed25519, edge-safe),
and Go attestotest.NewServer() (httptest) implement the v2 subset the SDKs
use — streams, single+batch events, head, receipts, tenant event listings —
with REAL seq/hash-chain semantics via the same frozen canonical functions,
the server-side number-policy mirror (422), and windows/checkpoints built on
demand with per-leaf inclusion proofs (promote-odd-node fold).
Hard rule, test-enforced in all three languages: mock evidence is structurally
incapable of passing as real — every emitted object carries "mock": true,
receipts are signed by a per-instance throwaway key under kid
attesto-mock-ed25519, and verify_receipt against any real witness key fails.
Acceptance: the P1 verify suite (receipt, payload commitment, inclusion,
completeness) passes against the emulator with real clients in all three
SDKs; head tracking sees an honestly chained sequence. READMEs gain a
"Testing without Attesto" quickstart.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
go.mod becomes module go.attesto.eu/sdk; all internal imports (CLI,
connectorkit, examples), the publication-evidence/registry contracts, docs,
and the README install line follow. No rotz.ai hostname remains in the
customer-visible Go chain. All Go packages build and pass under the new path.
All three SDKs bump to 0.3.0 (Python version.py/pyproject, TS package.json +
SDK_VERSION, Go SDKVersion + cliVersion) — the Phase-1 release version,
shipped atomically with the registry publish so the publication-evidence
contract stays consistent. Full sweep green: Python 84, TS 55, Go 3 packages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A trimmed (~1.7 KB) copy of the cross-language parity vectors now ships inside
each package (Python package-data JSON, Go go:embed, TS generated module). On
the first hashing operation per process each SDK recomputes the commitment
hash, the receipt domain-hash, and an inclusion fold against the vendored
vectors and fails closed (AttestoSelfTestError / ErrSelfTest) on any mismatch
— a corrupted install or diverging runtime can never silently produce wrong
evidence. Result is cached (including failure); cost <5 ms once. Corrupting a
vendored vector is test-asserted to fail closed in all three languages. The
frozen canonical primitives are untouched; the gate lives in the commitment/
verify entry points built on top of them.
attesto doctor: Go CLI subcommand and Python attesto.doctor(), producing a
deterministic {"ok", "checks"} report — vendored self-test, head-store
writability, number-policy dry-run on a sample payload, Ed25519 availability
(Python), and with credentials: reachability, protocol-header acceptance, and
clock skew vs the server Date header (warn >30 s; webhooks break at 300 s).
package_artifact_policy allows exactly attesto/_selftest_vectors.json in the
wheel (verified: built wheel contains it, policy green). READMEs updated.
This completes the last Phase-1 build item.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All three SDKs now send X-Attesto-Sdk (attesto-<lang>/<version>) and
X-Attesto-Protocol (ATTESTO-PROOFSTREAM-001/0.1-alpha) on every request. A new
backend ProtocolVersionMiddleware logs both headers (operators can see the
SDK/protocol mix in traffic) and, when the protocol header is present on a /v2
request and names a different protocol identifier or major version, answers
426 Upgrade Required with a structured body (error/supported/received/hint).
Absent or unparseable headers change nothing — old clients and curl stay fully
compatible (test-asserted, including /v1 never being handshake-gated).
SDKs surface the 426 as a typed error: Python AttestoProtocolMismatch,
TypeScript AttestoProtocolMismatch, Go IsProtocolMismatch(err) over *APIError
(Go-idiomatic). Tests cover the mismatch rules, the 426 mapping, and that the
handshake headers are actually sent.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each limit/offset list method gains an iterator twin that walks pages
transparently and stops on the first short page — same endpoints, no new API
surface: Python generators (iter_tenant_streams / _stream_events / _windows /
_checkpoints / iter_fork_evidence / iter_tenant_ivc_epochs), TypeScript async
iterators (for await ... of client.iterTenantStreamEvents(...)), and a Go
Iterator with Next(ctx) returning (nil, nil) at exhaustion, plus Iter* twins
on the client. Tests drain a 3-page mocked response set in order and confirm
a short first page ends iteration after exactly one request. READMEs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
verify_anchor_onchain / verifyAnchorOnchain / VerifyAnchorOnchain check an
anchor epoch against the chain itself in all three SDKs: one raw JSON-RPC
eth_call to the anchoring contract's getCommitment(batchId) comparing the
on-chain merkle root with the anchor's merkle_root, plus one
eth_getTransactionReceipt confirming status == 0x1 in the expected block.
The customer chooses the RPC endpoint — nothing asks Attesto to confirm
Attesto, and no web3/ethers dependency is added anywhere.
The getCommitment(string) selector (keccak256 first 4 bytes = a7b09e2a) is
pinned as a constant with the dynamic-string ABI encoding done manually;
a worked calldata example (computed once against web3 keccak) is asserted in
all three test suites, and APSProvenance.abi.json is copied into each SDK's
testdata with a test that flags the pinned selector for review if the ABI's
getCommitment signature ever changes. The contract address is read from the
anchor epoch's hashed payload (payload.contract_address).
Mocked-RPC tests cover match / root-mismatch / failed-tx / wrong-block /
missing-fields in each language with identical problem strings; a live test
against the production contract runs only when ATTESTO_LIVE_RPC_URL is set.
Go CLI gains `attesto anchors verify <id> --rpc-url <url>` (API fetch +
on-chain check in one step; existing get/remote-verify behavior unchanged).
READMEs updated per SDK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the verification chain (P1.2 -> P1.1 -> P1.3 -> P1.6). The client
remembers the last accepted (seq_no, event_hash) per stream and checks every
new receipt links forward; if the server rewinds a sequence number or presents
a divergent lineage, log_event / log_events raise AttestoForkDetected (Go:
*ForkDetectedError) and the stored head is NOT advanced. The customer's own
machine becomes the fork detector — no trust in any Attesto-side check.
- Python: HeadStore protocol + FileHeadStore (~/.attesto/heads.json, atomic,
0600, default) + MemoryHeadStore; wired into sync and async v2 clients;
head_store=None disables.
- TypeScript: HeadStore + MemoryHeadStore (default, edge-safe); Node-only
FileHeadStore kept in a separate module (@attesto/sdk/heads-file) so the core
bundle imports no node:fs; headStore: null disables.
- Go: HeadStore interface + MemoryHeadStore (default) + NewFileHeadStore;
WithHeadStore option; WithHeadStore(nil) disables.
Same forward/rewind/divergence/gap semantics across all three (unit-tested:
in-order advance, forged-rewind fork, divergent-next fork, forward-gap accept,
file-store restart persistence). Existing v2 client tests pin head_store=None
(they replay overlapping seq). READMEs gain a "Your SDK is a witness" section.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the offline verification stack (P1.2 -> P1.1 -> P1.3) in all three
SDKs, each a faithful port of the backend windows.py / checkpoints.py math on
top of the frozen canonical/domain-hash primitives:
- verify_inclusion_proof: fold a window inclusion proof to the window root
(domain attesto.v2.window; left sibling -> node(sibling,current), right ->
node(current,sibling)).
- verify_checkpoint_root: recompute a checkpoint root from window hashes
(domain attesto.v2.checkpoint), with an odd node at any level **promoted
unchanged** rather than duplicated/hashed with itself (the place a naive
Merkle port silently diverges).
- verify_checkpoint_extension: current.from_seq_no == previous.to_seq_no + 1
and current.previous_checkpoint_hash == previous.checkpoint_hash.
- verify_completeness: proves no events were omitted in a range -- gap-free
seq_no coverage plus prev_event_hash chaining to the previous event_hash.
New corpus golden-vectors/sdk-parity/inclusion.json (5-leaf window exercising
the promoted odd node, 3-window checkpoint root, extension + completeness
negatives), exported from the backend functions. Proven: Python = TypeScript =
Go = backend agree on every case. READMEs updated per SDK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the trust-model gap where Python/TS could only verify receipts by
calling the server (asking the party being distrusted). Both now verify
entirely client-side, mirroring the Go SDK's VerifyReceiptOffline one-to-one
with identical problem strings so reports are comparable cross-language.
- Python: new attesto.verify.verify_receipt + frozen VerifyReport dataclass,
using cryptography>=42 (new dependency; not PyNaCl) for Ed25519.
- TypeScript: verifyReceipt via WebCrypto subtle.verify({name:"Ed25519"}),
throwing a clear AttestoError on runtimes without Ed25519 (Node < 20) rather
than silently falling back to the server.
Both recompute domain_hash("attesto.v2.receipt", payload) and verify the
signature over domain + 0x00 + canonical_json_bytes(payload), reusing the
frozen canonical functions.
New corpus golden-vectors/sdk-parity/receipts.json (valid + payload/hash/
signature/wrong-key negatives). Proven: all five cases agree across Go,
Python, and TypeScript. READMEs document the offline function and note the
existing client.verify_receipt as the server-assisted variant.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds payload_commitment / metadata_commitment / verify_payload_commitment
and assert_commitment_safe_numbers to the Python, TypeScript, and Go SDKs,
each building on the frozen canonical_json/domain_hash primitives (no change
to their byte output). The number preflight is a byte-for-byte port of the
backend assert_commitment_safe_numbers (floats rejected, |int| > 2^53-1
rejected, bool exempt) and is wired into the v2 log_event / log_events send
path, raising a typed AttestoUnsafeNumberError with the JSON path so the rule
fails at dev time rather than as a production 422; preflight=False /
SkipPreflight defers to the server.
New shared corpus golden-vectors/sdk-parity/canonical-numbers.json (15 accept
+ 8 reject), accept-hashes generated from the backend _commitment. Proven:
Python = TypeScript = Go = backend produce byte-identical commitment hashes
for every accept vector and identical reject paths (the Go float64-vs-Python-
int serialization parity holds). READMEs updated per SDK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FIX 11 — reject cross-language-divergent numbers in committed payloads.
append_stream_event now rejects non-integer numbers and integers beyond
±(2^53−1) in payload/metadata at ingestion (HTTP 422), so a customer
recomputing a commitment in a Go/TS verifier can never read it as
tampering. Documented in the protocol spec + all three SDK READMEs; 9
tests.
FIX 12 — Nova IVC chain-continuity invariant. record_ivc_epoch now takes
a per-stream transaction-scoped advisory lock and fails closed (409) if
an epoch does not extend the latest verified epoch's next_state_root or
would skip an unproven checkpoint — so two concurrent provers cannot fork
Attesto's own lane and a failed proof cannot be chained over. The worker
stops the pass on a failed proof (break, not continue) and always
backfills the oldest unproven checkpoint first so holes heal. Tests cover
both 409 paths plus in-order record + replay.
FIX 13 — pin the e2e v1 checkpoint shape. Config now requires
PROOFSTREAM_WINDOW_MAX_EVENTS=1 (alongside CHECKPOINT_MAX_WINDOWS=4) when
Nova is enabled, and the worker refuses to close an aged checkpoint below
the 4-window shape (which would be unprovable). Documented as the
deliberate fail-closed v1 behavior; config + worker tests.
VERIFY-1 — investigation: the standard production ingestion path
(SDK → append_stream_event → persist_stream_event_receipt) does NOT write
the nova_e2e boundary metadata the prover requires; no writer exists in
backend/app, and _e2e_vector_for_checkpoint requires (not derives) it. So
Nova proofs currently cover golden-vector/harness inputs, not live
customer receipts — the open Route A/B item. Recorded in
CRYPTO_REVIEW_CHECKLIST as a coverage-scope note; no Route A/B
implementation in this release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>