Files
attesto-go/README.md
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

260 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
Canonicalization is specified normatively in [ATTESTO-CANONICAL-JSON-001](../../docs/protocol/ATTESTO-CANONICAL-JSON-001.md); 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.
```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, "<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.