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 "strings"
9 "testing"
10)
11
12// A pointer signed for one account's app must not verify at another
13// account's key for an app of the same name (tenant-isolation audit,
14// discussion #220). App names are unique only WITHIN an account, so
15// app+channel alone named "blog/edge" in every account at once: a byte copy
16// of A's pointer into B's prefix verified there.
17//
18// Mutation-mindedness: drop the account comparison from validatePointer and
19// the cross-account read below verifies again.
20func TestPointerDoesNotVerifyUnderAnotherAccount(t *testing.T) {
21 s := NewDirStore(t.TempDir())
22 ctx := context.Background()
23 priv, pub := testKeys(t)
24 shipRelease(t, s, "aaa", "blog", "9f3c1ad", "linux-arm64", []byte("bytes"))
25 if err := Promote(ctx, s, priv, "aaa", "blog", "edge", "9f3c1ad"); err != nil {
26 t.Fatalf("Promote: %v", err)
27 }
28 raw, _, err := s.Get(ctx, ChannelKey("aaa", "blog", "edge"))
29 if err != nil {
30 t.Fatal(err)
31 }
32 if _, err := Channel(ctx, s, pub, "aaa", "blog", "edge"); err != nil {
33 t.Fatalf("own account: %v", err)
34 }
35 if _, err := s.Put(ctx, ChannelKey("bbb", "blog", "edge"), raw, ""); err != nil {
36 t.Fatal(err)
37 }
38 _, err = Channel(ctx, s, pub, "bbb", "blog", "edge")
39 if err == nil {
40 t.Fatal("account aaa's pointer verified under account bbb")
41 }
42 if !strings.Contains(err.Error(), "aaa") {
43 t.Fatalf("refusal should name the account the pointer was signed for: %v", err)
44 }
45}
46
47// Every pointer written from now on carries its account under the
48// signature, so the binding cannot be removed without breaking it.
49func TestFreshPointerPayloadNamesItsAccount(t *testing.T) {
50 var sw signedWrapper
51 if err := json.Unmarshal(freshRootWrapper(t), &sw); err != nil {
52 t.Fatal(err)
53 }
54 payload, err := base64.StdEncoding.DecodeString(sw.Payload)
55 if err != nil {
56 t.Fatal(err)
57 }
58 var p ChannelPointer
59 if err := json.Unmarshal(payload, &p); err != nil {
60 t.Fatal(err)
61 }
62 if p.Account != "bab" {
63 t.Fatalf("signed payload account = %q, want bab", p.Account)
64 }
65}
66
67// A pointer written before the binding existed has no account and must keep
68// verifying wherever it sits: that is most of every production bucket. This
69// is the residual the binding cannot close by itself — see verifyPointer.
70func TestAccountlessPointerStillVerifiesAnywhere(t *testing.T) {
71 pub, err := ParsePublicKey(goldenRootPub, "golden")
72 if err != nil {
73 t.Fatal(err)
74 }
75 for _, acct := range []string{"bab", "zzz"} {
76 if _, err := verifyPointer(pub, []byte(goldenRootSignedWrapper), acct, "kass", "canary/platform-adoption"); err != nil {
77 t.Fatalf("golden account-less pointer under %s: %v", acct, err)
78 }
79 }
80}
81
82// A signer cannot produce an unbound pointer: the account is required at
83// signing, so no code path quietly writes the old shape.
84func TestSignPointerRequiresAccount(t *testing.T) {
85 priv, _ := testKeys(t)
86 if _, err := signPointer(RootSigner(priv), &ChannelPointer{App: "blog", Channel: "edge", Version: "9f3c1ad"}); err == nil {
87 t.Fatal("signed a pointer with no account")
88 }
89}
90
91// Moving an app between accounts: a bound pointer is re-signed for the new
92// account (deterministically, so a re-run compares equal), an unbound one
93// travels as bytes, and the result verifies at the destination only.
94func TestRebindPointerMovesTheBinding(t *testing.T) {
95 s := NewDirStore(t.TempDir())
96 ctx := context.Background()
97 priv, pub := testKeys(t)
98 shipRelease(t, s, "aaa", "blog", "9f3c1ad", "linux-arm64", []byte("bytes"))
99 if err := Promote(ctx, s, priv, "aaa", "blog", "edge", "9f3c1ad"); err != nil {
100 t.Fatal(err)
101 }
102 raw, _, err := s.Get(ctx, ChannelKey("aaa", "blog", "edge"))
103 if err != nil {
104 t.Fatal(err)
105 }
106 if acct, err := PointerAccount(raw); err != nil || acct != "aaa" {
107 t.Fatalf("PointerAccount = %q, %v", acct, err)
108 }
109 moved, err := RebindPointer(priv, raw, "aaa", "bbb", "blog", "edge")
110 if err != nil {
111 t.Fatal(err)
112 }
113 again, err := RebindPointer(priv, raw, "aaa", "bbb", "blog", "edge")
114 if err != nil || string(again) != string(moved) {
115 t.Fatalf("rebind is not deterministic (%v)", err)
116 }
117 got, err := verifyPointer(pub, moved, "bbb", "blog", "edge")
118 if err != nil {
119 t.Fatalf("rebound pointer does not verify at the destination: %v", err)
120 }
121 orig, _ := verifyPointer(pub, raw, "aaa", "blog", "edge")
122 if got.Version != orig.Version || !got.PromotedAt.Equal(orig.PromotedAt) || got.Account != "bbb" {
123 t.Fatalf("rebind changed more than the account: %+v vs %+v", got, orig)
124 }
125 if _, err := verifyPointer(pub, moved, "aaa", "blog", "edge"); err == nil {
126 t.Fatal("rebound pointer still verifies at the source account")
127 }
128 // A pointer from before the binding travels unchanged.
129 golden := []byte(goldenRootSignedWrapper)
130 goldPriv := ed25519.NewKeyFromSeed(goldenSeed())
131 same, err := RebindPointer(goldPriv, golden, "aaa", "bbb", "kass", "canary/platform-adoption")
132 if err != nil || string(same) != string(golden) {
133 t.Fatalf("account-less pointer was rewritten (%v)", err)
134 }
135 // Rebinding needs the root key the pointer verifies against.
136 wrong := ed25519.NewKeyFromSeed(make([]byte, ed25519.SeedSize))
137 if _, err := RebindPointer(wrong, raw, "aaa", "bbb", "blog", "edge"); err == nil {
138 t.Fatal("rebound a pointer with a key it does not verify against")
139 }
140}
141
142// goldenSeed is the deterministic seed golden_test.go's fixture was signed
143// with (bytes 1..32).
144func goldenSeed() []byte {
145 seed := make([]byte, ed25519.SeedSize)
146 for i := range seed {
147 seed[i] = byte(i + 1)
148 }
149 return seed
150}
151