carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/sha256"
7 "errors"
8 "fmt"
9 "strings"
10 "time"
11)
12
13// ManifestDigest is the sha256 of a manifest's canonical bytes: exactly what
14// the store holds at ManifestKey, never a re-marshal (#253, spec §4.6). The
15// manifest carries every artifact's sha256, so this one digest binds the
16// bytes a box will stage.
17func ManifestDigest(raw []byte) string { return etagOf(raw) }
18
19// ErrManifestDigest is a manifest whose bytes do not hash to the digest a
20// signed pointer (or, from plan 2, a promotion record) recorded for it: a
21// version name reused with other bytes, or a swapped object.
22var ErrManifestDigest = errors.New("release: manifest does not hash to the digest its signer recorded")
23
24// CheckManifestDigest reads version's canonical manifest and, when want is
25// set, refuses it unless its bytes hash to want. The manifest and bytes it
26// returns are the ones it checked: a caller must stage from these and never
27// re-read the manifest, or a swap between the check and the read would slip
28// through. want "" is today's read, unchecked.
29func CheckManifestDigest(ctx context.Context, s Store, acct, app, version, want string) (*Manifest, []byte, error) {
30 m, raw, err := GetManifestRaw(ctx, s, acct, app, version)
31 if err != nil {
32 return nil, nil, err
33 }
34 if want != "" {
35 if got := ManifestDigest(raw); got != want {
36 return nil, nil, fmt.Errorf("%w: %s/%s hashes to %s, the pointer signed %s",
37 ErrManifestDigest, app, version, digestShort(got), digestShort(want))
38 }
39 }
40 return m, raw, nil
41}
42
43// validDigest is the one shape a digest may take: 64 lowercase hex.
44func validDigest(d string) bool {
45 if len(d) != 2*sha256.Size {
46 return false
47 }
48 for _, c := range d {
49 if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
50 return false
51 }
52 }
53 return true
54}
55
56// digestShort is a digest as a person reads it in a log line.
57func digestShort(d string) string {
58 if len(d) > 12 {
59 return d[:12]
60 }
61 return d
62}
63
64// DigestOf is the signed digest of version as this pointer names it: the
65// head's when version is the head, else the History entry's. "" when the
66// pointer signs none for it, or does not name it.
67func (p *ChannelPointer) DigestOf(version string) string {
68 if p == nil {
69 return ""
70 }
71 if version == p.Version {
72 return p.Digest
73 }
74 for i, v := range p.History {
75 if v == version && i < len(p.HistoryDigests) {
76 return p.HistoryDigests[i]
77 }
78 }
79 return ""
80}
81
82// CarriesDigests is the test the ratchet and CARLOS_REQUIRE_POINTER_DIGEST
83// apply: a pointer "with digests" signs one for its head.
84func (p *ChannelPointer) CarriesDigests() bool { return p != nil && p.Digest != "" }
85
86// ChannelHasDigests reports whether any entry carries a digest: the rollback
87// rule's test (spec §4.6, "once its channel has any digest").
88func (p *ChannelPointer) ChannelHasDigests() bool {
89 if p == nil {
90 return false
91 }
92 if p.CarriesDigests() {
93 return true
94 }
95 for _, d := range p.HistoryDigests {
96 if d != "" {
97 return true
98 }
99 }
100 return false
101}
102
103// alignedHistoryDigests is p.HistoryDigests at History's length: an absent
104// list is all "". The copy is the writer's to change.
105func alignedHistoryDigests(p *ChannelPointer) []string {
106 out := make([]string, len(p.History))
107 copy(out, p.HistoryDigests)
108 return out
109}
110
111// inheritedDigests is the history half of the provenance rule for a write
112// that moves the head to version: each entry's digest copied verbatim from
113// the verified predecessor, aligned with the History the write builds, and
114// never computed (spec §4.6). Its length always equals that History's.
115func inheritedDigests(old *ChannelPointer, version string) []string {
116 hd := alignedHistoryDigests(old)
117 if old.Version != "" && old.Version != version {
118 hd = prependCapped(old.Digest, hd, historyMax)
119 }
120 return hd
121}
122
123// ResignNeededError is a move onto a history entry that carries no digest, on
124// a channel that carries some (spec §4.6): a rollback, or a promote of a build
125// already in the channel's history.
126type ResignNeededError struct{ App, Channel, Version string }
127
128func (e *ResignNeededError) Error() string {
129 return fmt.Sprintf(TextRollbackResign, e.App, e.Channel, e.Version, e.App, e.Channel)
130}
131
132// ChannelDigestState is one channel's line in the straggler report (spec
133// §4.6 "Watch for stragglers"). Err set means the channel's pointer could not
134// be read or verified: its digest state is unknown, and every other field is
135// zero. A report with such a line is incomplete, so it never says the fleet
136// is ready for CARLOS_REQUIRE_POINTER_DIGEST (Astra r1 #4).
137type ChannelDigestState struct {
138 Channel string `json:"channel"`
139 Version string `json:"version"`
140 PromotedAt time.Time `json:"promoted_at"`
141 HeadDigest bool `json:"head_digest"`
142 HistoryMissing int `json:"history_missing"`
143 Straggler bool `json:"straggler"`
144 Err string `json:"error,omitempty"`
145}
146
147// DigestStateOf is a verified pointer's line. A straggler is a pointer
148// without digests signed after the marker went on: an old writer is still in
149// use.
150func DigestStateOf(p *ChannelPointer, marker *DigestMarker) ChannelDigestState {
151 st := ChannelDigestState{Channel: p.Channel, Version: p.Version, PromotedAt: p.PromotedAt, HeadDigest: p.CarriesDigests()}
152 hd := alignedHistoryDigests(p)
153 for _, d := range hd {
154 if d == "" {
155 st.HistoryMissing++
156 }
157 }
158 st.Straggler = marker != nil && !st.HeadDigest && p.PromotedAt.After(marker.At)
159 return st
160}
161
162// DigestReport is every channel of one app, plus the account's marker. A
163// pointer that cannot be read or does not verify is never skipped: it comes
164// back as a line with Err set, its state unknown.
165func DigestReport(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app string) ([]ChannelDigestState, *DigestMarker, error) {
166 marker, err := ReadDigestMarker(ctx, s, pub, acct)
167 if err != nil {
168 return nil, nil, err
169 }
170 prefix := ChannelsPrefix(acct, app)
171 keys, err := s.List(ctx, prefix)
172 if err != nil {
173 return nil, nil, err
174 }
175 var out []ChannelDigestState
176 for _, k := range keys {
177 name, ok := strings.CutSuffix(strings.TrimPrefix(k, prefix), ".json")
178 if !ok || name == "" {
179 continue
180 }
181 p, err := Channel(ctx, s, pub, acct, app, name)
182 if err != nil {
183 out = append(out, ChannelDigestState{Channel: name, Err: fmt.Sprintf("%s: %v", ChannelKey(acct, app, name), err)})
184 continue
185 }
186 out = append(out, DigestStateOf(p, marker))
187 }
188 return out, marker, nil
189}
190