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 "strings"
6 "testing"
7 "time"
8)
9
10// trapStore fails the test on ANY access. The spec requires an out-of-scope
11// promotion to fail locally before any store WRITE; the code manages it
12// before any store access at all, and that is what is pinned here — a refusal
13// that costs a bucket round trip is a refusal that behaves differently when
14// the bucket is unreachable.
15type trapStore struct{ t *testing.T }
16
17func (s trapStore) Get(_ context.Context, key string) ([]byte, string, error) {
18 s.t.Fatalf("out-of-scope promote read %s — it must refuse before touching the store", key)
19 return nil, "", nil
20}
21
22func (s trapStore) Put(_ context.Context, key string, _ []byte, _ string) (string, error) {
23 s.t.Fatalf("out-of-scope promote WROTE %s — the scope check is not a gate", key)
24 return "", nil
25}
26
27func (s trapStore) List(_ context.Context, prefix string) ([]string, error) {
28 s.t.Fatalf("out-of-scope promote listed %s — it must refuse before touching the store", prefix)
29 return nil, nil
30}
31
32func (s trapStore) ListDirs(_ context.Context, prefix string) ([]string, error) {
33 s.t.Fatalf("out-of-scope promote listed %s — it must refuse before touching the store", prefix)
34 return nil, nil
35}
36
37func (s trapStore) Delete(_ context.Context, key string) error {
38 s.t.Fatalf("out-of-scope promote DELETED %s — the scope check is not a gate", key)
39 return nil
40}
41
42// An out-of-scope channel is refused locally, by every write verb, before the
43// store is touched — and the refusal names the scope, so the operator can see
44// which key they are holding.
45//
46// Mutation-mindedness: move the allows() call below the GetManifest read and
47// trapStore fails the test on the read; delete it and Put is reached and the
48// test fails on the write. Either way this is red.
49func TestScopedPromoteRefusesOutOfScopeBeforeTouchingTheStore(t *testing.T) {
50 _, _, sg := scopedFixture(t, []string{RungCanary, RungEdge}, 90*24*time.Hour)
51 ctx := context.Background()
52 s := trapStore{t}
53
54 for _, ch := range []string{"stable", "beta"} {
55 _, err := sg.PromoteOnLadder(ctx, s, "bab", "kass", ch, "9f3c1ad", PromoteOptions{})
56 if err == nil {
57 t.Fatalf("PromoteOnLadder to %s with a canary/edge key succeeded", ch)
58 }
59 assertNamesTheScope(t, err, ch)
60
61 if err := sg.Promote(ctx, s, "bab", "kass", ch, "9f3c1ad"); err == nil {
62 t.Fatalf("Promote to %s with a canary/edge key succeeded", ch)
63 } else {
64 assertNamesTheScope(t, err, ch)
65 }
66
67 if err := sg.Rollback(ctx, s, "bab", "kass", ch); err == nil {
68 t.Fatalf("Rollback of %s with a canary/edge key succeeded", ch)
69 } else {
70 assertNamesTheScope(t, err, ch)
71 }
72 }
73}
74
75// assertNamesTheScope checks a refusal is legible: it says which grant, what
76// the grant covers, and which channel was asked for.
77func assertNamesTheScope(t *testing.T, err error, channel string) {
78 t.Helper()
79 got := err.Error()
80 for _, want := range []string{"edge-builder", "canary, edge", channel} {
81 if !strings.Contains(got, want) {
82 t.Errorf("refusal %q does not name %q", got, want)
83 }
84 }
85}
86
87// -hotfix bypasses the LADDER. It can never bypass a grant: the two gates
88// answer different questions, and a flag an operator can type is not a way to
89// widen a delegation.
90//
91// Mutation-mindedness: put the allows() check behind `if !opt.Hotfix` and
92// this test is the only thing that notices — which would make the whole
93// feature bypassable by a documented flag.
94func TestHotfixDoesNotWidenAGrant(t *testing.T) {
95 _, _, sg := scopedFixture(t, []string{RungCanary, RungEdge}, 90*24*time.Hour)
96 _, err := sg.PromoteOnLadder(context.Background(), trapStore{t}, "bab", "kass", "stable", "9f3c1ad",
97 PromoteOptions{Hotfix: true})
98 if err == nil {
99 t.Fatal("--hotfix let a canary/edge key promote to stable")
100 }
101 assertNamesTheScope(t, err, "stable")
102}
103
104// A scoped key promotes inside its scope through the real store, and the
105// pointer it wrote verifies for a fleet configured with the ROOT public key
106// and nothing else. That is the whole scheme in one assertion: one anchor,
107// two kinds of signer.
108func TestScopedPromoteVerifiesAgainstTheRootPubkey(t *testing.T) {
109 root, rootPub, sg := scopedFixture(t, []string{RungCanary, RungEdge}, 90*24*time.Hour)
110 ctx := context.Background()
111 s := NewDirStore(t.TempDir())
112 shipRelease(t, s, "bab", "kass", "9f3c1ad", "linux-arm64", []byte("build"))
113
114 if err := sg.Promote(ctx, s, "bab", "kass", "edge", "9f3c1ad"); err != nil {
115 t.Fatalf("scoped promote to edge: %v", err)
116 }
117 p, err := Channel(ctx, s, rootPub, "bab", "kass", "edge")
118 if err != nil {
119 t.Fatalf("Channel with the root pubkey: %v", err)
120 }
121 if p.Version != "9f3c1ad" {
122 t.Fatalf("pointer = %+v", p)
123 }
124 // The root key still reaches stable, from beta, as it always did.
125 shipRelease(t, s, "bab", "kass", "aa11bb2", "linux-arm64", []byte("build two"))
126 if err := Promote(ctx, s, root, "bab", "kass", "beta", "aa11bb2"); err != nil {
127 t.Fatalf("root promote to beta: %v", err)
128 }
129 if err := Promote(ctx, s, root, "bab", "kass", "stable", "aa11bb2"); err != nil {
130 t.Fatalf("root promote to stable: %v", err)
131 }
132}
133
134// The pointer a scoped key replaces was signed by something else — the root
135// key, in every real deployment, because that is what promoted the channel
136// before the builder existed. Verification of the OUTGOING pointer therefore
137// has to be anchored at the root, not at the signing key's own public half,
138// and the proof is that history comes forward.
139//
140// Mutation-mindedness: change Signer.verifyPub to return the signing key's
141// public half and this fails at the second promote with "existing pointer …
142// signature verification failed", which is exactly what a scoped key would
143// hit on its first run against a real channel.
144func TestScopedKeyReplacesARootSignedPointerAndCarriesHistory(t *testing.T) {
145 root, rootPub, sg := scopedFixture(t, []string{RungCanary, RungEdge}, 90*24*time.Hour)
146 ctx := context.Background()
147 s := NewDirStore(t.TempDir())
148 shipRelease(t, s, "bab", "kass", "9f3c1ad", "linux-arm64", []byte("one"))
149 shipRelease(t, s, "bab", "kass", "aa11bb2", "linux-arm64", []byte("two"))
150
151 if err := Promote(ctx, s, root, "bab", "kass", "edge", "9f3c1ad"); err != nil {
152 t.Fatalf("root promote: %v", err)
153 }
154 if err := sg.Promote(ctx, s, "bab", "kass", "edge", "aa11bb2"); err != nil {
155 t.Fatalf("scoped promote over a root-signed pointer: %v", err)
156 }
157 p, err := Channel(ctx, s, rootPub, "bab", "kass", "edge")
158 if err != nil {
159 t.Fatalf("Channel: %v", err)
160 }
161 if p.Version != "aa11bb2" || len(p.History) != 1 || p.History[0] != "9f3c1ad" {
162 t.Fatalf("pointer = %+v, want aa11bb2 with 9f3c1ad in history", p)
163 }
164
165 // And back the other way: the root key replaces a scope-signed pointer,
166 // carrying its history. Neither direction may be a dead end, or a
167 // deployment could not stop using a scoped key once it started.
168 shipRelease(t, s, "bab", "kass", "cc33dd4", "linux-arm64", []byte("three"))
169 if err := Promote(ctx, s, root, "bab", "kass", "edge", "cc33dd4"); err != nil {
170 t.Fatalf("root promote over a scope-signed pointer: %v", err)
171 }
172 p, err = Channel(ctx, s, rootPub, "bab", "kass", "edge")
173 if err != nil {
174 t.Fatalf("Channel: %v", err)
175 }
176 if p.Version != "cc33dd4" || len(p.History) != 2 {
177 t.Fatalf("pointer = %+v, want cc33dd4 with two history entries", p)
178 }
179 // Rollback with the scoped key returns to the scope-signed version.
180 if err := sg.Rollback(ctx, s, "bab", "kass", "edge"); err != nil {
181 t.Fatalf("scoped rollback: %v", err)
182 }
183 p, err = Channel(ctx, s, rootPub, "bab", "kass", "edge")
184 if err != nil {
185 t.Fatalf("Channel: %v", err)
186 }
187 if p.Version != "aa11bb2" {
188 t.Fatalf("after rollback pointer = %+v, want aa11bb2", p)
189 }
190}
191
192// End to end, in the shape the feature exists for: mint a scoped edge key,
193// promote to edge with it, and let an ordinary adoption pass — which knows
194// only CARLOS_RELEASE_PUBKEY — install the build. Then the same key is
195// refused stable locally, and a hand-crafted stable grant is refused at
196// verification.
197func TestScopedEdgeKeyEndToEnd(t *testing.T) {
198 root, rootPub, sg := scopedFixture(t, []string{RungCanary, RungEdge}, 90*24*time.Hour)
199 ctx := context.Background()
200 s := NewDirStore(t.TempDir())
201 opt := t.TempDir()
202 reg := testRegistry(t)
203
204 shipRelease(t, s, "bab", "myapp", "v1", adoptPlatform, []byte("build one"))
205 if err := sg.Promote(ctx, s, "bab", "myapp", "canary/x", "v1"); err != nil {
206 t.Fatalf("scoped promote: %v", err)
207 }
208 addRoute(t, reg, "awake.example.com", "myapp", "canary/x", false)
209 if err := Pass(ctx, reg, s, rootPub, opt, func(string) error { return nil }); err != nil {
210 t.Fatalf("adoption pass over a scope-signed pointer: %v", err)
211 }
212 if v, _ := LiveVersion(opt, "awake.example.com"); v != "v1" {
213 t.Fatalf("live version = %q, want v1 — adoption did not accept the scope-signed pointer", v)
214 }
215
216 // stable is refused locally, without a write.
217 if err := sg.Promote(ctx, trapStore{t}, "bab", "myapp", "stable", "v1"); err == nil {
218 t.Fatal("the edge key promoted to stable")
219 }
220
221 // And a stable grant hand-crafted with the ROOT key — the strongest
222 // attacker this scheme contemplates, since it is the deployment's own
223 // signing authority — is refused at verification.
224 _, scopedPub := testKeys(t)
225 g := goodGrant(scopedPub)
226 g.Rungs = []string{RungEdge, RungStable}
227 sub, err := parseGrantBlob(forgeGrant(t, root, g), "test")
228 if err != nil {
229 t.Fatal(err)
230 }
231 if _, err := verifyGrant(rootPub, sub); err == nil {
232 t.Fatal("a root-signed stable grant was honoured")
233 }
234}
235