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>
210 lines
6.9 KiB
Markdown
210 lines
6.9 KiB
Markdown
# 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 git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go
|
||
```
|
||
|
||
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 "git.rotz.ai/rotzmediagroup/attesto-v1/sdk/go"
|
||
)
|
||
|
||
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))
|
||
```
|
||
|
||
## 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.
|