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>
Attesto Go SDK
Official Go SDK for Attesto 2.0 Proofstream. The default API base URL is
https://verify.attesto.eu. Use it from server-side, infrastructure, security
tooling, CI, evidence exporters, and operator automation. Do not embed Attesto API keys in browser bundles, mobile apps, or public artifacts.
Install
go get go.attesto.eu/sdk
CLI binaries: curl -fsSL https://get.attesto.eu | sh (checksum-verified).
Verify the release signature before you trust its verifier:
curl -fsSO https://get.attesto.eu/cosign.pub
curl -fsSO https://get.attesto.eu/0.3.0/SHA256SUMS
curl -fsSO https://get.attesto.eu/0.3.0/SHA256SUMS.sig
cosign verify-blob --key cosign.pub --insecure-ignore-tlog --signature SHA256SUMS.sig SHA256SUMS
The first release is VCS-resolved from the Attesto repository. It intentionally uses only the Go standard library.
Quickstart
package main
import (
"context"
"fmt"
"log"
"os"
"time"
attesto "go.attesto.eu/sdk"
)
func main() {
ctx := context.Background()
client, err := attesto.NewClient(os.Getenv("ATTESTO_API_KEY"))
if err != nil {
log.Fatal(err)
}
stream, err := client.CreateStream(ctx, attesto.StreamCreateInput{
UseCase: "ai-governance",
PolicyID: "policy-main",
})
if err != nil {
log.Fatal(err)
}
receipt, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{
SourceRef: "decision-42",
OccurredAt: time.Now().UTC().Format(time.RFC3339Nano),
Payload: attesto.M{
"model": "risk-classifier",
"score": 0.92,
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(receipt.StreamEventID, receipt.EventHash)
}
Attesto stores source-system time separately from backend ingest time.
OccurredAt must be RFC3339 with a timezone offset. The Go SDK fills it with
time.Now().UTC() when omitted, but production integrations should pass the
real upstream event timestamp whenever the source system provides one.
Canonicalization is specified normatively in ATTESTO-CANONICAL-JSON-001; the parity corpus golden-vectors/sdk-parity/ is its conformance set.
Committed payload number rule
When events are committed to a Proofstream, payload and metadata numbers must
serialize identically across Python, Go, and JavaScript. Non-integer numbers
and integers beyond ±(2^53−1) are rejected at ingestion (HTTP 422); encode
decimals and large integers as strings (e.g. {"score": "0.87"}). This keeps
cross-language commitment recomputation byte-exact (CanonicalJSON).
The SDK enforces the same rule locally before sending, so you see it at dev
time rather than as a production 422. LogEvent / LogEvents return an
*UnsafeNumberError (with .Path, the JSON path to the offending value). Set
RequestOptions{SkipPreflight: true} to defer to the server.
// Commitment a Proofstream stores for a payload, byte-identical to the server
// (and to the Python / TypeScript SDKs):
commitment, _ := attesto.PayloadCommitment(map[string]any{"decision": "approve", "score_bp": 8700})
// commitment["canonical_payload_hash"] == server's stored hash
ok, _ := attesto.VerifyPayloadCommitment(myPayload, event) // recompute and compare
Verification
Remote verification uses Attesto's public /v2/verify API. Offline receipt
verification uses ATTESTO-PROOFSTREAM-001 canonical JSON, domain-separated
hashes, and Ed25519 signature verification locally.
report := attesto.VerifyReceiptOffline(receipt.Receipt, publicKeyHex)
if !report.OK {
log.Fatalf("receipt failed verification: %v", report.Problems)
}
The offline trust model extends across the whole proof chain — all client-side:
ok, _ := attesto.VerifyInclusionProof(leafHash, proof, windowRoot) // event in a window root
ok, _ = attesto.VerifyCheckpointRoot(windowHashes, checkpointRoot) // windows fold to checkpoint root
ext := attesto.VerifyCheckpointExtension(previous, current) // one checkpoint continues the previous
comp := attesto.VerifyCompleteness(events, 5, 8) // no events omitted in [5, 8]
VerifyCompleteness proves no events were omitted in a range: the sequence
numbers must be gap-free and each event's prev_event_hash must chain to the
previous event's event_hash.
Your SDK is a witness
The client remembers the last accepted (seqNo, eventHash) per stream and checks
every new receipt links forward. If the server ever rewinds a sequence number or
presents a divergent lineage, LogEvent / LogEvents return a
*ForkDetectedError and the stored head is not advanced. The default store is
in-memory; use a file store for fork detection across process invocations, or
disable it.
// Persist across CLI invocations (atomic, 0600 at ~/.attesto/heads.json):
client, _ := attesto.NewClient(apiKey, attesto.WithHeadStore(attesto.NewFileHeadStore("")))
// Disable fork detection:
client, _ = attesto.NewClient(apiKey, attesto.WithHeadStore(nil))
Typed compliance events and the evidence report
decision := attesto.ModelDecision{Model: "credit-v1", Decision: "approve", ConfidenceBp: 8700}
payload, _ := decision.ToPayload() // regulation_refs attached, number-policy validated
client.LogEvent(ctx, streamID, attesto.EventInput{
SourceRef: "d-1", EventType: decision.EventType(), Payload: payload,
})
attesto report article12 --stream str_... --output report.md
The report is a deterministic template (never LLM-generated) stating what is recorded and independently verifiable — it never asserts conformity.
Testing without Attesto: attestotest
go.attesto.eu/sdk/attestotest starts a local httptest emulator with real
hash-chain semantics; point the real client at it and run your full pipeline
in CI with zero network:
server := attestotest.NewServer()
defer server.Close()
client, _ := attesto.NewClient(server.APIKey, attesto.WithBaseURL(server.URL))
stream, _ := client.CreateStream(ctx, attesto.StreamCreateInput{UseCase: "ci", PolicyID: "mock-policy"})
receipt, _ := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{SourceRef: "e1"})
stored, _ := client.GetReceipt(ctx, receipt.StreamEventID)
report := attesto.VerifyReceiptOffline(stored.Receipt, server.PublicKeyHex)
Mock evidence can never pass as real: every object carries mock: true, the
signer kid is attesto-mock-ed25519, and verification against any real
witness key fails.
Built-in self-test and doctor
On the first hashing operation per process the SDK verifies itself against an
embedded copy of the cross-language parity vectors and fails closed with
ErrSelfTest on any divergence. attesto doctor (CLI) prints a deterministic
JSON report: self-test, head-store writability, number-policy dry-run
(--sample-payload file.json), and — with credentials configured —
reachability and protocol acceptance.
Iterating long listings
Paginated List* methods have Iter* twins that walk limit/offset pages
transparently; Next returns (nil, nil) when the listing is exhausted:
it := client.IterTenantStreamEvents("str_...", 200)
for {
event, err := it.Next(ctx)
if err != nil || event == nil {
break
}
process(event)
}
Verify anchors on-chain
VerifyAnchorOnchain checks an anchor epoch against the chain itself — one raw
JSON-RPC eth_call to the anchoring contract's getCommitment(batchId)
(comparing the on-chain merkle root) plus a transaction-receipt check (status,
block). No web3 dependency; the RPC endpoint is yours, so this never asks
Attesto to confirm Attesto.
anchor, _ := client.GetAnchorEpoch(ctx, "aep_...")
report := attesto.VerifyAnchorOnchain(ctx, anchor, "https://polygon-rpc.example", 15*time.Second)
if !report.OK {
log.Fatalf("anchor failed on-chain verification: %v", report.Problems)
}
CLI equivalent (fetch + on-chain check in one step):
attesto anchors verify aep_... --rpc-url https://polygon-rpc.example
Receiving Attesto webhooks
func handler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
headers := map[string]string{
"X-Attesto-Timestamp": r.Header.Get("X-Attesto-Timestamp"),
"X-Attesto-Signature": r.Header.Get("X-Attesto-Signature"),
}
if !attesto.VerifyWebhook(body, headers, webhookSecret, 300) {
w.WriteHeader(http.StatusUnauthorized)
return
}
process(body)
}
Verification recomputes hmac_sha256(secret, "<timestamp>." + body) from the
X-Attesto-Timestamp / X-Attesto-Signature headers, rejects timestamps more
than the allowed skew from now (replay protection), and compares with
hmac.Equal (constant time).
Operator and Admin Endpoints
System-key clients are created with attesto.NewClient. Tenant/operator
endpoints, including connector installation and Local Vault installation
management, use attesto.NewBearerClient with a tenant bearer token obtained
from the dashboard session flow.
Secrets returned once by connector creation are present only in the returned struct and are never logged by the SDK.