# 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 ```shell 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: ```shell 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 ```go 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. ## 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. ```go // 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. ```go 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: ```go 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. ```go // 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 ```go 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, }) ``` ```bash 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: ```go 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: ```go 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. ```go 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): ```bash attesto anchors verify aep_... --rpc-url https://polygon-rpc.example ``` ## Receiving Attesto webhooks ```go 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, "." + 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.