carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package ledger
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "strings"
10
11 "github.com/carlosframework/platform/internal/release"
12)
13
14type Getter func(ctx context.Context, key string) ([]byte, error)
15
16func StoreGetter(st release.Store) Getter {
17 return func(ctx context.Context, key string) ([]byte, error) {
18 b, _, err := st.Get(ctx, key)
19 return b, err
20 }
21}
22
23func HTTPGetter(client *http.Client, base string) Getter {
24 return func(ctx context.Context, key string) ([]byte, error) {
25 req, err := http.NewRequestWithContext(ctx, http.MethodGet, base+"/"+key, nil)
26 if err != nil {
27 return nil, err
28 }
29 resp, err := client.Do(req)
30 if err != nil {
31 return nil, err
32 }
33 defer resp.Body.Close()
34 if resp.StatusCode != http.StatusOK {
35 return nil, fmt.Errorf("ledger: GET %s: %s", key, resp.Status)
36 }
37 return io.ReadAll(io.LimitReader(resp.Body, MaxBlob+1))
38 }
39}
40
41type VerifyResult struct {
42 Count int
43 HeadSHA256 string
44}
45
46// Verify walks HEAD back to the genesis entry, re-hashing every stored
47// byte. It proves: HEAD's sha matches the head entry's bytes, every
48// prev_sha256 matches the previous entry's bytes, the walk terminates at
49// prev_key "", and the walked length equals HEAD's count.
50func Verify(ctx context.Context, get Getter, sqid, app, name string) (VerifyResult, error) {
51 hb, err := get(ctx, HeadKey(sqid, app, name))
52 if err != nil {
53 return VerifyResult{}, err
54 }
55 var head Head
56 if err := json.Unmarshal(hb, &head); err != nil {
57 return VerifyResult{}, fmt.Errorf("ledger: HEAD corrupt: %w", err)
58 }
59
60 // The walk follows keys the CHAIN supplies (HEAD.json's head_key, then
61 // each entry's prev_key), so those keys are exactly as trustworthy as the
62 // bytes under verification. Pinning them to this ledger's own entries/
63 // prefix is what stops a chain from grafting another ledger's history —
64 // or any other object in the bucket — onto its own and still verifying
65 // clean: every hash would agree, because the hashes are only ever
66 // checked against what the chain itself claims.
67 want := Prefix(sqid, app, name) + "entries/"
68
69 key, wantSHA := head.HeadKey, head.HeadSHA256
70 walked := 0
71 for key != "" {
72 if walked >= head.Count {
73 return VerifyResult{}, fmt.Errorf("ledger: chain longer than HEAD count %d", head.Count)
74 }
75 if !strings.HasPrefix(key, want) {
76 return VerifyResult{}, fmt.Errorf("ledger: %s: chain leaves ledger %q — key is outside %s", key, name, want)
77 }
78 eb, err := get(ctx, key)
79 if err != nil {
80 return VerifyResult{}, fmt.Errorf("ledger: %s: %w", key, err)
81 }
82 if got := SHA256Hex(eb); got != wantSHA {
83 return VerifyResult{}, fmt.Errorf("ledger: %s: sha %s, chain says %s — bytes were altered", key, got, wantSHA)
84 }
85 var e Entry
86 if err := json.Unmarshal(eb, &e); err != nil {
87 return VerifyResult{}, fmt.Errorf("ledger: %s corrupt: %w", key, err)
88 }
89 if e.Ledger != name {
90 return VerifyResult{}, fmt.Errorf("ledger: %s: entry names ledger %q, walking %q", key, e.Ledger, name)
91 }
92 key, wantSHA = e.PrevKey, e.PrevSHA256
93 walked++
94 }
95 if walked != head.Count {
96 return VerifyResult{}, fmt.Errorf("ledger: walked %d entries, HEAD count %d", walked, head.Count)
97 }
98 return VerifyResult{Count: walked, HeadSHA256: head.HeadSHA256}, nil
99}
100