Files
attesto-go/attestotest/server.go
Codex b5cece2822 gates: cargo fmt + secret-scan-clean test fixtures
nova_e2e_contract's cargo fmt --check now passes (formatting from the
Plan B circuit work), and the two scanner-flagged test fixtures use
allowlisted fake-markers: the gateway test provider key carries "dummy"
and the Go emulator API key is atto_test_abc123... (valid 32-hex,
"abc123" marker). Gateway + Go suites green; secret scan 0 findings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 15:12:54 +02:00

429 lines
14 KiB
Go

// Package attestotest provides a local, in-memory Attesto v2 emulator for
// tests ([P2.3]). NewServer starts an httptest.Server implementing the v2
// subset the SDK uses, with REAL seq/hash-chain semantics via the same frozen
// canonical functions and receipts signed by a per-instance throwaway Ed25519
// key under kid "attesto-mock-ed25519". Every emitted object carries
// mock: true, so mock evidence is structurally incapable of passing as real.
package attestotest
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"sync"
"time"
attesto "go.attesto.eu/sdk"
)
// MockKid is the signer kid on every mock receipt.
const MockKid = "attesto-mock-ed25519"
type mockEvent struct {
Envelope attesto.M
EventHash string
StreamHeadHash string
StreamEventID string
TenantView attesto.M
}
// Server is the running emulator. Point a real client at URL; verify its
// receipts offline with PublicKeyHex.
type Server struct {
URL string
APIKey string
PublicKeyHex string
httpServer *httptest.Server
priv ed25519.PrivateKey
mu sync.Mutex
streams map[string]attesto.M
events map[string][]*mockEvent
receipts map[string]attesto.M
counter int
}
// Close shuts the emulator down.
func (s *Server) Close() { s.httpServer.Close() }
func (s *Server) id(prefix string) string {
s.counter++
return fmt.Sprintf("%s_mock%08d", prefix, s.counter)
}
func nowISO() string {
return time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
}
func safeNumbers(value any) bool {
switch v := value.(type) {
case json.Number:
if _, err := v.Int64(); err != nil {
return false
}
n, _ := v.Int64()
return n <= 1<<53-1 && n >= -(1<<53-1)
case float64:
return v == float64(int64(v)) && v <= float64(int64(1)<<53-1) && v >= -float64(int64(1)<<53-1)
case map[string]any:
for _, item := range v {
if !safeNumbers(item) {
return false
}
}
case []any:
for _, item := range v {
if !safeNumbers(item) {
return false
}
}
}
return true
}
func mustHash(domain string, value any) string {
h, err := attesto.DomainHashHex(domain, value)
if err != nil {
panic(err)
}
return h
}
// NewServer starts the emulator.
func NewServer() *Server {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(err)
}
s := &Server{
APIKey: "atto_test_abc12300000000000000000000000000",
PublicKeyHex: hex.EncodeToString(pub),
priv: priv,
streams: map[string]attesto.M{},
events: map[string][]*mockEvent{},
receipts: map[string]attesto.M{},
}
s.httpServer = httptest.NewServer(http.HandlerFunc(s.handle))
s.URL = s.httpServer.URL
return s
}
var (
reEvents = regexp.MustCompile(`^/v2/streams/([^/]+)/events$`)
reBatch = regexp.MustCompile(`^/v2/streams/([^/]+)/events/batch$`)
reHead = regexp.MustCompile(`^/v2/streams/([^/]+)/head$`)
reReceipt = regexp.MustCompile(`^/v2/receipts/([^/]+)$`)
reTenantEvents = regexp.MustCompile(`^/v2/tenant/streams/([^/]+)/events$`)
)
func writeJSON(w http.ResponseWriter, code int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(body)
}
func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
path := r.URL.Path
switch {
case r.Method == http.MethodPost && path == "/v2/streams":
var body attesto.M
_ = json.NewDecoder(r.Body).Decode(&body)
streamID := s.id("str")
stream := attesto.M{
"streamId": streamID, "systemId": "sys_mock",
"useCase": str(body["useCase"], "mock"), "policyId": str(body["policyId"], "mock-policy"),
"status": "active", "lastSeqNo": float64(0),
"lastEventHash": nil, "lastStreamHeadHash": nil,
"created": true, "mock": true,
}
s.streams[streamID] = stream
s.events[streamID] = nil
writeJSON(w, 201, stream)
case r.Method == http.MethodPost && reBatch.MatchString(path):
streamID := reBatch.FindStringSubmatch(path)[1]
var body struct {
Events []attesto.M `json:"events"`
}
_ = json.NewDecoder(r.Body).Decode(&body)
receipts, code, errBody := s.appendMany(streamID, body.Events)
if errBody != nil {
writeJSON(w, code, errBody)
return
}
writeJSON(w, 201, attesto.M{"accepted": len(receipts), "receipts": receipts})
case r.Method == http.MethodPost && reEvents.MatchString(path):
streamID := reEvents.FindStringSubmatch(path)[1]
var body attesto.M
_ = json.NewDecoder(r.Body).Decode(&body)
receipts, code, errBody := s.appendMany(streamID, []attesto.M{body})
if errBody != nil {
writeJSON(w, code, errBody)
return
}
writeJSON(w, 201, receipts[0])
case r.Method == http.MethodGet && reHead.MatchString(path):
stream, ok := s.streams[reHead.FindStringSubmatch(path)[1]]
if !ok {
writeJSON(w, 404, attesto.M{"detail": "stream not found"})
return
}
writeJSON(w, 200, attesto.M{
"streamId": stream["streamId"], "systemId": stream["systemId"],
"status": stream["status"], "lastSeqNo": stream["lastSeqNo"],
"lastEventHash": stream["lastEventHash"], "lastStreamHeadHash": stream["lastStreamHeadHash"],
"mock": true,
})
case r.Method == http.MethodGet && reReceipt.MatchString(path):
receipt, ok := s.receipts[reReceipt.FindStringSubmatch(path)[1]]
if !ok {
writeJSON(w, 404, attesto.M{"detail": "receipt not found"})
return
}
writeJSON(w, 200, receipt)
case r.Method == http.MethodGet && reTenantEvents.MatchString(path):
list := s.events[reTenantEvents.FindStringSubmatch(path)[1]]
out := make([]attesto.M, 0, len(list))
for _, e := range list {
out = append(out, e.TenantView)
}
writeJSON(w, 200, out)
case r.Method == http.MethodGet && path == "/health":
writeJSON(w, 200, attesto.M{"ok": true, "mock": true})
default:
writeJSON(w, 404, attesto.M{"detail": "not found"})
}
}
func str(v any, fallback string) string {
if s, ok := v.(string); ok && s != "" {
return s
}
return fallback
}
func (s *Server) appendMany(streamID string, bodies []attesto.M) ([]attesto.M, int, attesto.M) {
stream, ok := s.streams[streamID]
if !ok {
return nil, 404, attesto.M{"detail": "stream not found"}
}
for _, body := range bodies {
if !safeNumbers(orEmpty(body["payload"])) || !safeNumbers(orEmpty(body["metadata"])) {
return nil, 422, attesto.M{"detail": "unsafe numbers are not permitted in committed payloads"}
}
}
out := make([]attesto.M, 0, len(bodies))
for _, body := range bodies {
out = append(out, s.append(stream, body))
}
return out, 0, nil
}
func orEmpty(v any) any {
if v == nil {
return map[string]any{}
}
return v
}
func (s *Server) append(stream attesto.M, body attesto.M) attesto.M {
payload := orEmpty(body["payload"])
metadata := orEmpty(body["metadata"])
// Idempotent on (source_kind, source_ref), like real ingestion: a resend
// returns the existing event's receipt instead of appending.
sourceKind := str(body["sourceKind"], "sdk")
sourceRef := str(body["sourceRef"], "")
for _, existing := range s.events[stream["streamId"].(string)] {
if sourceRef == "" {
break // anonymous events never dedupe
}
source := existing.Envelope["source"].(attesto.M)
if source["kind"] == sourceKind && source["event_id"] == sourceRef {
return s.receipts[existing.StreamEventID]
}
}
seqNo := int64(stream["lastSeqNo"].(float64)) + 1
ingestedAt := nowISO()
payloadCanonical, _ := attesto.CanonicalJSON(payload)
metadataCanonical, _ := attesto.CanonicalJSON(metadata)
payloadSum := sha256.Sum256(payloadCanonical)
metadataSum := sha256.Sum256(metadataCanonical)
envelope := attesto.M{
"protocol": attesto.ProofstreamProtocol,
"protocol_version": attesto.ProtocolVersionAlpha,
"tenant_id": "ten_mock",
"system_id": stream["systemId"],
"stream_id": stream["streamId"],
"use_case": stream["useCase"],
"policy_id": stream["policyId"],
"seq_no": seqNo,
"prev_event_hash": stream["lastEventHash"],
"source": attesto.M{"kind": str(body["sourceKind"], "sdk"), "event_id": str(body["sourceRef"], "")},
"event_type": str(body["eventType"], "inference"),
"occurred_at": str(body["occurredAt"], ingestedAt),
"source_timezone": "Europe/Amsterdam",
"ingested_at": ingestedAt,
"payload_commitment": attesto.M{
"hash_alg": "sha256",
"canonical_payload_hash": hex.EncodeToString(payloadSum[:]),
},
"metadata_commitment": attesto.M{
"hash_alg": "sha256",
"canonical_metadata_hash": hex.EncodeToString(metadataSum[:]),
},
}
eventHash := mustHash(attesto.ProofstreamDomains["event"], envelope)
streamHead := attesto.M{
"protocol": attesto.ProofstreamProtocol,
"protocol_version": attesto.ProtocolVersionAlpha,
"tenant_id": "ten_mock",
"stream_id": stream["streamId"],
"seq_no": seqNo,
"event_hash": eventHash,
"prev_stream_head_hash": stream["lastStreamHeadHash"],
"accepted_at": ingestedAt,
}
streamHeadHash := mustHash(attesto.ProofstreamDomains["stream"], streamHead)
streamEventID := s.id("sev")
receiptPayload := attesto.M{
"mock": true,
"protocol": attesto.ProofstreamProtocol,
"protocol_version": attesto.ProtocolVersionAlpha,
"tenant_id": "ten_mock",
"system_id": stream["systemId"],
"stream_id": stream["streamId"],
"stream_event_id": streamEventID,
"event_id": str(body["sourceRef"], ""),
"seq_no": seqNo,
"event_hash": eventHash,
"prev_event_hash": stream["lastEventHash"],
"stream_head_hash": streamHeadHash,
"issued_at": ingestedAt,
"signer": attesto.M{"alg": "ed25519", "kid": MockKid, "key_epoch": MockKid},
}
receiptHash := mustHash(attesto.ProofstreamDomains["receipt"], receiptPayload)
canonical, _ := attesto.CanonicalJSON(receiptPayload)
message := append(append([]byte(attesto.ProofstreamDomains["receipt"]), 0), canonical...)
signature := ed25519.Sign(s.priv, message)
wire := attesto.M{
"streamId": stream["streamId"], "streamEventId": streamEventID,
"seqNo": seqNo, "eventHash": eventHash,
"prevEventHash": stream["lastEventHash"], "streamHeadHash": streamHeadHash,
"mock": true,
"receipt": attesto.M{
"payload": receiptPayload,
"receiptHash": receiptHash,
"signature": attesto.M{
"alg": "ed25519", "kid": MockKid, "keyEpoch": MockKid,
"signatureHex": hex.EncodeToString(signature),
},
},
}
s.events[stream["streamId"].(string)] = append(s.events[stream["streamId"].(string)], &mockEvent{
Envelope: envelope, EventHash: eventHash, StreamHeadHash: streamHeadHash,
StreamEventID: streamEventID,
TenantView: attesto.M{
"streamEventId": streamEventID, "seq_no": seqNo,
"event_type": envelope["event_type"],
"source_ref": envelope["source"].(attesto.M)["event_id"],
"event_hash": eventHash, "prev_event_hash": stream["lastEventHash"],
"stream_head_hash": streamHeadHash,
"payload_commitment": envelope["payload_commitment"], "mock": true,
},
})
s.receipts[streamEventID] = wire
stream["lastSeqNo"] = float64(seqNo)
stream["lastEventHash"] = eventHash
stream["lastStreamHeadHash"] = streamHeadHash
return wire
}
// WindowLeaf is one leaf of a built window, with its inclusion proof.
type WindowLeaf struct {
StreamEventID string
SeqNo int64
LeafIndex int
LeafHash string
Proof []attesto.InclusionStep
}
// Window is a built window over all events of a stream so far.
type Window struct {
WindowID string
StreamID string
RootHash string
Leaves []WindowLeaf
}
// BuildWindow folds all events so far into a window with per-leaf inclusion
// proofs (the P1.3 verify functions accept them unchanged).
func (s *Server) BuildWindow(streamID string) (*Window, error) {
s.mu.Lock()
defer s.mu.Unlock()
list := s.events[streamID]
if len(list) == 0 {
return nil, fmt.Errorf("no events to fold")
}
leafHashes := make([]string, len(list))
for i, e := range list {
leafHashes[i] = mustHash(attesto.ProofstreamDomains["window"], attesto.M{
"kind": "leaf", "protocol": attesto.ProofstreamProtocol,
"protocol_version": attesto.ProtocolVersionAlpha,
"tenant_id": "ten_mock", "system_id": "sys_mock",
"stream_id": streamID, "stream_event_id": e.StreamEventID,
"seq_no": e.Envelope["seq_no"], "leaf_index": i,
"event_hash": e.EventHash, "stream_head_hash": e.StreamHeadHash,
})
}
proofs := make([][]attesto.InclusionStep, len(leafHashes))
type node struct {
hash string
idx []int
}
level := make([]node, len(leafHashes))
for i, h := range leafHashes {
level[i] = node{h, []int{i}}
}
for len(level) > 1 {
var next []node
for offset := 0; offset < len(level); offset += 2 {
left := level[offset]
if offset+1 >= len(level) {
next = append(next, left) // promote
continue
}
right := level[offset+1]
for _, i := range left.idx {
proofs[i] = append(proofs[i], attesto.InclusionStep{Side: "right", Hash: right.hash})
}
for _, i := range right.idx {
proofs[i] = append(proofs[i], attesto.InclusionStep{Side: "left", Hash: left.hash})
}
parent := mustHash(attesto.ProofstreamDomains["window"], attesto.M{
"kind": "node", "left_hash": left.hash, "right_hash": right.hash,
})
next = append(next, node{parent, append(append([]int{}, left.idx...), right.idx...)})
}
level = next
}
window := &Window{WindowID: s.id("win"), StreamID: streamID, RootHash: level[0].hash}
for i, e := range list {
window.Leaves = append(window.Leaves, WindowLeaf{
StreamEventID: e.StreamEventID, SeqNo: e.Envelope["seq_no"].(int64),
LeafIndex: i, LeafHash: leafHashes[i], Proof: proofs[i],
})
}
return window, nil
}