sdk(P2.3): MockAttesto — local emulator in all three SDKs
Customers can now test their full ingest-and-verify pipeline in CI with zero network and zero Attesto account. Python attesto.testing.MockAttesto (context manager over a local HTTP server + pytest-fixture friendly), TypeScript createMockServer() (fetch-compatible handler, WebCrypto Ed25519, edge-safe), and Go attestotest.NewServer() (httptest) implement the v2 subset the SDKs use — streams, single+batch events, head, receipts, tenant event listings — with REAL seq/hash-chain semantics via the same frozen canonical functions, the server-side number-policy mirror (422), and windows/checkpoints built on demand with per-leaf inclusion proofs (promote-odd-node fold). Hard rule, test-enforced in all three languages: mock evidence is structurally incapable of passing as real — every emitted object carries "mock": true, receipts are signed by a per-instance throwaway key under kid attesto-mock-ed25519, and verify_receipt against any real witness key fails. Acceptance: the P1 verify suite (receipt, payload commitment, inclusion, completeness) passes against the emulator with real clients in all three SDKs; head tracking sees an honestly chained sequence. READMEs gain a "Testing without Attesto" quickstart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
413
attestotest/server.go
Normal file
413
attestotest/server.go
Normal file
@@ -0,0 +1,413 @@
|
||||
// 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
|
||||
}
|
||||
132
attestotest/server_test.go
Normal file
132
attestotest/server_test.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package attestotest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
attesto "go.attesto.eu/sdk"
|
||||
)
|
||||
|
||||
func newClient(t *testing.T, s *Server) *attesto.Client {
|
||||
t.Helper()
|
||||
client, err := attesto.NewClient(s.APIKey, attesto.WithBaseURL(s.URL))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func toSignedReceipt(t *testing.T, wire attesto.M) attesto.SignedReceipt {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(wire["receipt"])
|
||||
var receipt attesto.SignedReceipt
|
||||
if err := json.Unmarshal(raw, &receipt); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return receipt
|
||||
}
|
||||
|
||||
func TestFullPipelineAgainstTheEmulator(t *testing.T) {
|
||||
server := NewServer()
|
||||
defer server.Close()
|
||||
client := newClient(t, server)
|
||||
ctx := context.Background()
|
||||
|
||||
stream, err := client.CreateStream(ctx, attesto.StreamCreateInput{UseCase: "ci", PolicyID: "mock-policy"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receipt, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{
|
||||
SourceRef: "e1", Payload: attesto.M{"decision": "approve", "score_bp": 8700},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.LogEvents(ctx, stream.StreamID, []attesto.EventInput{
|
||||
{SourceRef: "e2", Payload: attesto.M{"n": 2}},
|
||||
{SourceRef: "e3", Payload: attesto.M{"n": 3}},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stored, err := client.GetReceipt(ctx, receipt.StreamEventID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
report := attesto.VerifyReceiptOffline(stored.Receipt, server.PublicKeyHex)
|
||||
if !report.OK {
|
||||
t.Fatalf("offline verification failed: %v", report.Problems)
|
||||
}
|
||||
|
||||
events, err := client.ListTenantStreamEvents(ctx, stream.StreamID, 100, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plain := make([]map[string]any, len(events))
|
||||
for i, e := range events {
|
||||
plain[i] = e
|
||||
}
|
||||
comp := attesto.VerifyCompleteness(plain, 1, 3)
|
||||
if !comp.OK {
|
||||
t.Fatalf("completeness failed: %v", comp.Problems)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInclusionProofsFromBuiltWindowVerify(t *testing.T) {
|
||||
server := NewServer()
|
||||
defer server.Close()
|
||||
client := newClient(t, server)
|
||||
ctx := context.Background()
|
||||
stream, _ := client.CreateStream(ctx, attesto.StreamCreateInput{UseCase: "ci", PolicyID: "mock-policy"})
|
||||
for i := 0; i < 5; i++ { // odd leaf count exercises the promote rule
|
||||
if _, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{
|
||||
SourceRef: "e", Payload: attesto.M{"i": i},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
window, err := server.BuildWindow(stream.StreamID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, leaf := range window.Leaves {
|
||||
ok, err := attesto.VerifyInclusionProof(leaf.LeafHash, leaf.Proof, window.RootHash)
|
||||
if err != nil || !ok {
|
||||
t.Fatalf("leaf %d failed inclusion: ok=%v err=%v", leaf.LeafIndex, ok, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMockReceiptsCannotPassAsReal(t *testing.T) {
|
||||
server := NewServer()
|
||||
defer server.Close()
|
||||
client := newClient(t, server)
|
||||
ctx := context.Background()
|
||||
stream, _ := client.CreateStream(ctx, attesto.StreamCreateInput{UseCase: "ci", PolicyID: "mock-policy"})
|
||||
receipt, err := client.LogEvent(ctx, stream.StreamID, attesto.EventInput{SourceRef: "e1"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
stored, err := client.GetReceipt(ctx, receipt.StreamEventID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Structurally marked.
|
||||
if stored.Receipt.Payload["mock"] != true {
|
||||
t.Error("mock receipt payload must declare mock: true")
|
||||
}
|
||||
signer, _ := stored.Receipt.Payload["signer"].(map[string]any)
|
||||
if signer["kid"] != MockKid {
|
||||
t.Errorf("kid = %v, want %s", signer["kid"], MockKid)
|
||||
}
|
||||
// Rejected against a different ("real") witness key.
|
||||
realPub, _, _ := ed25519.GenerateKey(rand.Reader)
|
||||
report := attesto.VerifyReceiptOffline(stored.Receipt, hex.EncodeToString(realPub))
|
||||
if report.OK {
|
||||
t.Fatal("mock receipt verified against a real key — must never happen")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user