package ledger import ( "context" "encoding/json" "fmt" "io" "net/http" "strings" "github.com/carlosframework/platform/internal/release" ) type Getter func(ctx context.Context, key string) ([]byte, error) func StoreGetter(st release.Store) Getter { return func(ctx context.Context, key string) ([]byte, error) { b, _, err := st.Get(ctx, key) return b, err } } func HTTPGetter(client *http.Client, base string) Getter { return func(ctx context.Context, key string) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/"+key, nil) if err != nil { return nil, err } resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("ledger: GET %s: %s", key, resp.Status) } return io.ReadAll(io.LimitReader(resp.Body, MaxBlob+1)) } } type VerifyResult struct { Count int HeadSHA256 string } // Verify walks HEAD back to the genesis entry, re-hashing every stored // byte. It proves: HEAD's sha matches the head entry's bytes, every // prev_sha256 matches the previous entry's bytes, the walk terminates at // prev_key "", and the walked length equals HEAD's count. func Verify(ctx context.Context, get Getter, sqid, app, name string) (VerifyResult, error) { hb, err := get(ctx, HeadKey(sqid, app, name)) if err != nil { return VerifyResult{}, err } var head Head if err := json.Unmarshal(hb, &head); err != nil { return VerifyResult{}, fmt.Errorf("ledger: HEAD corrupt: %w", err) } // The walk follows keys the CHAIN supplies (HEAD.json's head_key, then // each entry's prev_key), so those keys are exactly as trustworthy as the // bytes under verification. Pinning them to this ledger's own entries/ // prefix is what stops a chain from grafting another ledger's history — // or any other object in the bucket — onto its own and still verifying // clean: every hash would agree, because the hashes are only ever // checked against what the chain itself claims. want := Prefix(sqid, app, name) + "entries/" key, wantSHA := head.HeadKey, head.HeadSHA256 walked := 0 for key != "" { if walked >= head.Count { return VerifyResult{}, fmt.Errorf("ledger: chain longer than HEAD count %d", head.Count) } if !strings.HasPrefix(key, want) { return VerifyResult{}, fmt.Errorf("ledger: %s: chain leaves ledger %q — key is outside %s", key, name, want) } eb, err := get(ctx, key) if err != nil { return VerifyResult{}, fmt.Errorf("ledger: %s: %w", key, err) } if got := SHA256Hex(eb); got != wantSHA { return VerifyResult{}, fmt.Errorf("ledger: %s: sha %s, chain says %s — bytes were altered", key, got, wantSHA) } var e Entry if err := json.Unmarshal(eb, &e); err != nil { return VerifyResult{}, fmt.Errorf("ledger: %s corrupt: %w", key, err) } if e.Ledger != name { return VerifyResult{}, fmt.Errorf("ledger: %s: entry names ledger %q, walking %q", key, e.Ledger, name) } key, wantSHA = e.PrevKey, e.PrevSHA256 walked++ } if walked != head.Count { return VerifyResult{}, fmt.Errorf("ledger: walked %d entries, HEAD count %d", walked, head.Count) } return VerifyResult{Count: walked, HeadSHA256: head.HeadSHA256}, nil }