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 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "os"
11 "time"
12)
13
14// PromotedPrefix is every promotion record of an app: what PurgeApp sweeps.
15func PromotedPrefix(acct, app string) string { return "releases/" + acct + "/" + app + "/promoted/" }
16
17// PromotionRecordKey is the durable record of one promoted build (spec §4.1).
18// It sits outside AppPrefix, beside the account's pointer digest marker, so
19// PurgeApp sweeps it explicitly.
20func PromotionRecordKey(acct, app, version string) string {
21 return PromotedPrefix(acct, app) + version
22}
23
24// PromotionRecord binds a version name to the exact manifest that was
25// promoted (or explicitly authorized). It keeps a build authorized after it
26// falls out of the pointer's history, and a reused name cannot inherit it:
27// the new name's manifest hashes to another digest.
28type PromotionRecord struct {
29 Schema int `json:"schema"` // 1
30 Account string `json:"account"`
31 App string `json:"app"`
32 Version string `json:"version"`
33 ManifestDigest string `json:"manifest_digest"`
34 Channel string `json:"channel,omitempty"`
35 Authorized bool `json:"authorized,omitempty"`
36 Actor string `json:"actor,omitempty"`
37 At time.Time `json:"at"`
38}
39
40// ErrNoPromotionRecord is a build with no record: promoted before records
41// existed, or never promoted. It wraps os.ErrNotExist.
42var ErrNoPromotionRecord = fmt.Errorf("release: no promotion record: %w", os.ErrNotExist)
43
44func validateRecord(r *PromotionRecord, acct, app, version string) error {
45 if r.Schema != 1 {
46 return fmt.Errorf("unsupported promotion record schema %d", r.Schema)
47 }
48 if r.Account != acct || r.App != app || r.Version != version {
49 return fmt.Errorf("promotion record names %s/%s/%s, expected %s/%s/%s", r.Account, r.App, r.Version, acct, app, version)
50 }
51 if !ValidAccount(acct) || !ValidApp(app) || !ValidVersion(version) || version == "dev" {
52 return fmt.Errorf("promotion record for invalid %s/%s/%s", acct, app, version)
53 }
54 if !validDigest(r.ManifestDigest) {
55 return fmt.Errorf("promotion record has an invalid manifest digest")
56 }
57 if r.Channel != "" && !ValidChannel(r.Channel) {
58 return fmt.Errorf("promotion record has an invalid channel %q", r.Channel)
59 }
60 if r.Channel == "" && !r.Authorized {
61 return fmt.Errorf("promotion record names no channel and is not an authorization")
62 }
63 if r.At.IsZero() {
64 return fmt.Errorf("promotion record has no time")
65 }
66 return nil
67}
68
69// WritePromotionRecord signs rec, stamped now, and writes it unless the key
70// already holds a verified record for the same digest: one record per
71// version, kept from its first promotion. A record for other bytes (a name
72// pruned and reshipped) is replaced. A scoped signer may write only a record
73// whose channel its grant covers, and never an authorization.
74func (sg Signer) WritePromotionRecord(ctx context.Context, s Store, rec PromotionRecord) error {
75 if err := sg.usable(); err != nil {
76 return err
77 }
78 rec.Schema = 1
79 rec.At = time.Now().UTC().Truncate(time.Second)
80 if err := validateRecord(&rec, rec.Account, rec.App, rec.Version); err != nil {
81 return err
82 }
83 if rec.Channel == "" || rec.Authorized {
84 if sg.scope != nil {
85 return fmt.Errorf("promotion record: an authorization needs the deployment's root key")
86 }
87 } else if err := sg.allows(rec.Channel, rec.At); err != nil {
88 return fmt.Errorf("promotion record: %w", err)
89 }
90 key := PromotionRecordKey(rec.Account, rec.App, rec.Version)
91 _, etag, err := s.Get(ctx, key)
92 switch {
93 case errors.Is(err, os.ErrNotExist):
94 etag = ""
95 case err != nil:
96 return err
97 default:
98 if cur, rerr := ReadPromotionRecord(ctx, s, sg.verifyPub(), rec.Account, rec.App, rec.Version); rerr == nil && cur.ManifestDigest == rec.ManifestDigest {
99 return nil
100 }
101 }
102 payload, err := json.MarshalIndent(rec, "", " ")
103 if err != nil {
104 return err
105 }
106 b64 := base64.StdEncoding.EncodeToString(payload)
107 wire, err := json.MarshalIndent(signedWrapper{
108 Payload: b64,
109 Sig: base64.StdEncoding.EncodeToString(ed25519.Sign(sg.key, []byte(b64))),
110 Grant: sg.grant,
111 }, "", " ")
112 if err != nil {
113 return err
114 }
115 _, err = s.Put(ctx, key, wire, etag)
116 return err
117}
118
119// ReadPromotionRecord verifies the record at version's key: root -> grant ->
120// record, the chain verifyPointer checks, and the signed payload must name
121// this account, app and version. (nil, ErrNoPromotionRecord) when absent; any
122// other failure is an error that is never read as "absent".
123func ReadPromotionRecord(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app, version string) (*PromotionRecord, error) {
124 raw, _, err := s.Get(ctx, PromotionRecordKey(acct, app, version))
125 if errors.Is(err, os.ErrNotExist) {
126 return nil, fmt.Errorf("%s/%s: %w", app, version, ErrNoPromotionRecord)
127 }
128 if err != nil {
129 return nil, err
130 }
131 var sw signedWrapper
132 if err := json.Unmarshal(raw, &sw); err != nil {
133 return nil, fmt.Errorf("promotion record wrapper: %v", err)
134 }
135 signer, scope := pub, (*Grant)(nil)
136 if sw.Grant != nil {
137 g, err := verifyGrant(pub, sw.Grant)
138 if err != nil {
139 return nil, err
140 }
141 gpub, err := ParsePublicKey(g.Pub, "key grant pub")
142 if err != nil {
143 return nil, err
144 }
145 signer, scope = gpub, g
146 }
147 sig, err := base64.StdEncoding.DecodeString(sw.Sig)
148 if err != nil || len(signer) != ed25519.PublicKeySize || !ed25519.Verify(signer, []byte(sw.Payload), sig) {
149 return nil, fmt.Errorf("promotion record %s/%s: signature verification failed", app, version)
150 }
151 payload, err := base64.StdEncoding.DecodeString(sw.Payload)
152 if err != nil {
153 return nil, fmt.Errorf("promotion record payload: %v", err)
154 }
155 var rec PromotionRecord
156 if err := json.Unmarshal(payload, &rec); err != nil {
157 return nil, fmt.Errorf("promotion record payload: %v", err)
158 }
159 if err := validateRecord(&rec, acct, app, version); err != nil {
160 return nil, err
161 }
162 if scope != nil {
163 if rec.Channel == "" || rec.Authorized {
164 return nil, fmt.Errorf("promotion record %s/%s: an authorization signed by a scoped key", app, version)
165 }
166 if err := scope.allows(rec.Channel, rec.At); err != nil {
167 return nil, fmt.Errorf("promotion record %s/%s: %w", app, version, err)
168 }
169 }
170 return &rec, nil
171}
172
173// AuthorizedManifest is §4.1's evidence for every install the settings add:
174// the build's signed record, and its canonical manifest hashing to the
175// record's digest. The manifest it returns is the one it checked; the caller
176// stages from it and never re-reads it.
177func AuthorizedManifest(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app, version string) (*Manifest, error) {
178 rec, err := ReadPromotionRecord(ctx, s, pub, acct, app, version)
179 if err != nil {
180 return nil, err
181 }
182 m, _, err := CheckManifestDigest(ctx, s, acct, app, version, rec.ManifestDigest)
183 return m, err
184}
185