// 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_00000000000000000000000000000000", 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"]) 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_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 }