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 "errors"
6 "fmt"
7 "time"
8)
9
10// ErrResignChanged is a resign whose channel moved after its plan was shown.
11var ErrResignChanged = errors.New("release: the channel moved since the resign plan was made")
12
13// ResignPlan is what `carlos channels resign` shows the operator, and the
14// only thing Resign signs (spec §4.6). Computed[0] is the head, Computed[i+1]
15// History[i]: true where the digest was read from the bucket now rather than
16// copied from the signed pointer, which is the part the operator confirms.
17type ResignPlan struct {
18 Account, App, Channel string
19 Version string
20 Digest string
21 History []string
22 HistoryDigests []string
23 Computed []bool
24 etag string
25}
26
27// PlanResign reads the channel's verified pointer and fills in a digest for
28// every entry it can: a digest the pointer already signs is copied, and any
29// other is the bucket's current manifest digest. A history entry whose
30// manifest is gone stays "", so rolling back onto it stays refused. A head
31// whose manifest is gone cannot be resigned.
32func (sg Signer) PlanResign(ctx context.Context, s Store, acct, app, channel string) (*ResignPlan, error) {
33 if !ValidAccount(acct) || !ValidApp(app) || !ValidChannel(channel) {
34 return nil, fmt.Errorf("resign: invalid %s/%s/%s", acct, app, channel)
35 }
36 if err := sg.allows(channel, time.Now().UTC()); err != nil {
37 return nil, fmt.Errorf("resign: %w", err)
38 }
39 if err := resignMarker(ctx, s, sg, acct); err != nil {
40 return nil, err
41 }
42 key := ChannelKey(acct, app, channel)
43 cur, etag, err := GetQualified(ctx, s, key)
44 if err != nil {
45 return nil, fmt.Errorf("resign: %w", err)
46 }
47 old, err := verifyPointer(sg.verifyPub(), cur, acct, app, channel)
48 if err != nil {
49 return nil, fmt.Errorf("resign: existing pointer at %s: %w", key, err)
50 }
51 plan := &ResignPlan{Account: acct, App: app, Channel: channel, Version: old.Version,
52 History: old.History, HistoryDigests: alignedHistoryDigests(old),
53 Computed: make([]bool, len(old.History)+1), etag: etag}
54 plan.Digest = old.Digest
55 if plan.Digest == "" {
56 _, raw, err := GetManifestRaw(ctx, s, acct, app, old.Version)
57 if err != nil {
58 return nil, fmt.Errorf(TextResignUnreadable, old.Version)
59 }
60 plan.Digest, plan.Computed[0] = ManifestDigest(raw), true
61 }
62 for i, v := range old.History {
63 if plan.HistoryDigests[i] != "" {
64 continue
65 }
66 if _, raw, err := GetManifestRaw(ctx, s, acct, app, v); err == nil {
67 plan.HistoryDigests[i], plan.Computed[i+1] = ManifestDigest(raw), true
68 }
69 }
70 return plan, nil
71}
72
73// claimRawResign signs the plan's pointer: the same version, history, audit
74// trail and fast-track waiver, with the plan's digests, conditional on the
75// pointer being exactly the one the plan was made from. Resign and
76// ResignAuthorized are this under the app mutation.
77//
78// authorize, when non-nil, is the caller's release authority over the
79// channel, asked of the pipeline read after the pointer, inside the same
80// admitted mutation, exactly as claimRawRollbackAuthorized asks it: resign
81// binds bytes to a version name, so it is a release decision (Astra r1 #2).
82// A refusal comes back as an AuthorizationError and nothing is written.
83func (sg Signer) claimRawResign(ctx context.Context, s Store, plan *ResignPlan, actor string, authorize func(*Pipeline) error) error {
84 if plan == nil {
85 return errors.New("resign: no plan to sign")
86 }
87 if !ValidAccount(plan.Account) || !ValidApp(plan.App) || !ValidChannel(plan.Channel) {
88 return fmt.Errorf("resign: invalid %s/%s/%s", plan.Account, plan.App, plan.Channel)
89 }
90 // Checked again at signing: the marker may have been turned off since
91 // the plan was shown.
92 if err := resignMarker(ctx, s, sg, plan.Account); err != nil {
93 return err
94 }
95 key := ChannelKey(plan.Account, plan.App, plan.Channel)
96 cur, etag, err := GetQualified(ctx, s, key)
97 if err != nil {
98 return fmt.Errorf("resign: %w", err)
99 }
100 if etag != plan.etag {
101 return fmt.Errorf("%w: %s", ErrResignChanged, fmt.Sprintf(TextResignChanged, plan.Channel))
102 }
103 old, err := verifyPointer(sg.verifyPub(), cur, plan.Account, plan.App, plan.Channel)
104 if err != nil {
105 return fmt.Errorf("resign: existing pointer at %s: %w", key, err)
106 }
107 if authorize != nil {
108 pipe, perr := GetPipeline(ctx, s, sg.verifyPub(), plan.Account, plan.App)
109 if perr != nil {
110 return fmt.Errorf("resign: cannot read the pipeline for %s/%s: %w", plan.Account, plan.App, perr)
111 }
112 if aerr := authorize(pipe); aerr != nil {
113 return &AuthorizationError{Err: aerr}
114 }
115 }
116 next := *old
117 // A pointer from before the account binding carries no account, which the
118 // verifier accepts and the signer refuses. A resigned pointer is a new
119 // signed statement, so it is bound to the account it was verified under.
120 next.Account = plan.Account
121 next.Digest, next.HistoryDigests = plan.Digest, plan.HistoryDigests
122 if len(next.HistoryDigests) == 0 {
123 next.HistoryDigests = nil
124 }
125 if old.FastTrack && sg.scope != nil {
126 return fmt.Errorf("resign: %s carries a fast-track waiver, which only the root key may sign", plan.Channel)
127 }
128 wrapper, err := signPointer(sg, &next)
129 if err != nil {
130 return err
131 }
132 if _, err := s.Put(ctx, key, wrapper, etag); err != nil {
133 if errors.Is(err, ErrCASConflict) {
134 return fmt.Errorf("%w: %s", ErrResignChanged, fmt.Sprintf(TextResignChanged, plan.Channel))
135 }
136 return err
137 }
138 return nil
139}
140
141// resignMarker is resign's gate (Astra I3 #4, spec §4.6 and §8): the
142// account's emission marker must exist and verify against the root, the
143// operator's statement that every writer is upgraded. Without it a resigned
144// pointer would arm the boxes' ratchets while old writers still write
145// pointers without digests, and every such write would then be refused.
146func resignMarker(ctx context.Context, s Store, sg Signer, acct string) error {
147 m, err := ReadDigestMarker(ctx, s, sg.verifyPub(), acct)
148 if err != nil {
149 return fmt.Errorf(TextMarkerUnreadable, "resign", acct, err)
150 }
151 if m == nil {
152 return fmt.Errorf(TextResignNoMarker, acct, acct)
153 }
154 return nil
155}
156