Codex 2276f4da09 docs(P3.5+P4.1+W.1): canonical JSON spec, countersignature ADR, witness ADR
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>
2026-06-12 07:59:23 +02:00

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^531) 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.

Description
Official Attesto Go SDK (read-only mirror of the sdk/go subtree). Module: go.attesto.eu/sdk
Readme 217 KiB
Languages
Go 100%