carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package appconfig
2
3import (
4 "bytes"
5 "context"
6 "crypto/ecdh"
7 "encoding/base64"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12
13 "github.com/carlosframework/platform/internal/registry"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17func bindKeys(t *testing.T) (*ecdh.PrivateKey, *ecdh.PublicKey) {
18 t.Helper()
19 priv, err := ecdh.P256().NewPrivateKey(fixedScalar(0x11))
20 if err != nil {
21 t.Fatal(err)
22 }
23 return priv, priv.PublicKey()
24}
25
26// One CARLOS_SECRETS_KEY opens every envelope in a deployment, so before
27// the binding a secret sealed into one tenant's config, copied into
28// another's, was decrypted into the second tenant's environment
29// (tenant-isolation audit, discussion #220). A bound envelope opens only for
30// the (account, app) it was sealed for.
31//
32// Mutation-mindedness: seal or open with nil associated data and the
33// cross-account and cross-app opens below succeed.
34func TestBoundEnvelopeOpensOnlyForItsBinding(t *testing.T) {
35 priv, pub := bindKeys(t)
36 env, err := SealBound(pub, Binding{Account: "aaa", App: "blog"}, []byte("s3kr1t"))
37 if err != nil {
38 t.Fatal(err)
39 }
40 got, err := Open(priv, Binding{Account: "aaa", App: "blog"}, env)
41 if err != nil || string(got) != "s3kr1t" {
42 t.Fatalf("own binding: %q, %v", got, err)
43 }
44 for _, b := range []Binding{
45 {Account: "bbb", App: "blog"}, // same app name, another tenant
46 {Account: "aaa", App: "shop"}, // same tenant, another app
47 } {
48 if got, err := Open(priv, b, env); err == nil || got != nil {
49 t.Fatalf("envelope for aaa/blog opened as %s/%s", b.Account, b.App)
50 }
51 }
52}
53
54// Downgrade: cutting the v2 prefix off leaves bytes shaped exactly like a
55// v1 envelope, which opens without a binding. It must not open — the v2 key
56// derivation differs (boundInfo), so the GCM tag fails.
57//
58// Mutation-mindedness: set boundInfo to hkdfInfo and this opens.
59func TestBoundEnvelopeCannotBeDowngradedToUnbound(t *testing.T) {
60 priv, pub := bindKeys(t)
61 env, err := SealBound(pub, Binding{Account: "aaa", App: "blog"}, []byte("s3kr1t"))
62 if err != nil {
63 t.Fatal(err)
64 }
65 raw, _ := base64.StdEncoding.DecodeString(env)
66 if !bytes.HasPrefix(raw, []byte(boundPrefix)) {
67 t.Fatalf("Seal wrote no %s prefix", boundPrefix)
68 }
69 stripped := base64.StdEncoding.EncodeToString(raw[len(boundPrefix):])
70 if err := CheckEnvelope(stripped); err != nil {
71 t.Fatalf("precondition: stripped envelope should look like v1: %v", err)
72 }
73 if got, err := Open(priv, Binding{Account: "bbb", App: "blog"}, stripped); err == nil || got != nil {
74 t.Fatal("a v2 envelope stripped to v1 opened without its binding")
75 }
76}
77
78// The v2 wire shape, pinned like v1's golden vector: a drift here means
79// envelopes already sealed stop opening.
80func TestBoundEnvelopeGoldenVector(t *testing.T) {
81 priv, pub := bindKeys(t)
82 eph, err := ecdh.P256().NewPrivateKey(fixedScalar(0x22))
83 if err != nil {
84 t.Fatal(err)
85 }
86 nonce := make([]byte, 12)
87 for i := range nonce {
88 nonce[i] = byte(i + 1)
89 }
90 b := Binding{Account: "bab", App: "keymail"}
91 aad, err := b.aad()
92 if err != nil {
93 t.Fatal(err)
94 }
95 got, err := sealWith(eph, nonce, pub, boundPrefix, boundInfo, aad, []byte("s3kr1t-value"))
96 if err != nil {
97 t.Fatal(err)
98 }
99 const want = "Q1NCMgSrTT1cMAUIiVUDWxuplu2yTDxWX6TUcFNW0TXikHFdvT876aH9TjYb0calxCZ5P+PqnTz0YNalrQpdLIBLE2lIAQIDBAUGBwgJCgsMxRFVmrtd/T04JgIGwUoHD+0wmlJIwGOB+IO4KQ=="
100 if got != want {
101 t.Fatalf("bound envelope drifted from the pinned wire format:\n got: %s\n want: %s", got, want)
102 }
103 if plain, err := Open(priv, b, got); err != nil || string(plain) != "s3kr1t-value" {
104 t.Fatalf("Open: %q, %v", plain, err)
105 }
106}
107
108func TestSealRefusesAnInvalidBinding(t *testing.T) {
109 _, pub := bindKeys(t)
110 for _, b := range []Binding{{}, {Account: "bab"}, {App: "keymail"}} {
111 if _, err := SealBound(pub, b, []byte("x")); err == nil || !strings.Contains(err.Error(), "binding") {
112 t.Fatalf("Seal with binding %+v: %v", b, err)
113 }
114 }
115}
116
117// The binding as the box applies it: a sync pass renders a route's config
118// as (route account, route app), so another tenant's envelope copied into
119// this route's bundle fails the render and the env file is never written.
120func TestSyncRefusesAnEnvelopeSealedForAnotherAccount(t *testing.T) {
121 ctx := context.Background()
122 priv, pub := bindKeys(t)
123 for _, tc := range []struct {
124 name string
125 sealed Binding
126 render bool
127 }{
128 {"own account", Binding{Account: "bbb", App: "blog"}, true},
129 {"another account's blog", Binding{Account: "aaa", App: "blog"}, false},
130 } {
131 t.Run(tc.name, func(t *testing.T) {
132 s := release.NewDirStore(t.TempDir())
133 reg := testRegistry(t)
134 envDir := t.TempDir()
135 env, err := SealBound(pub, tc.sealed, []byte("shh"))
136 if err != nil {
137 t.Fatal(err)
138 }
139 if err := SetSecret(ctx, s, "bbb", "blog", map[string]string{"TOKEN": env}); err != nil {
140 t.Fatal(err)
141 }
142 if err := reg.Upsert(registry.Route{
143 Host: "blog.example.com", Kind: "instance", App: "blog", Account: "bbb", Status: "ready",
144 Backing: registry.BackingUnit,
145 }); err != nil {
146 t.Fatal(err)
147 }
148 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
149 t.Fatalf("Sync: %v", err)
150 }
151 got, err := os.ReadFile(filepath.Join(envDir, "blog.example.com.env"))
152 if tc.render {
153 if err != nil || !strings.Contains(string(got), "TOKEN=shh\n") {
154 t.Fatalf("own secret not rendered: %q, %v", got, err)
155 }
156 return
157 }
158 if err == nil {
159 t.Fatalf("another account's secret was rendered:\n%s", got)
160 }
161 })
162 }
163}
164
165// The rollout guard: until a deployment turns bound sealing on, Seal writes
166// v1, which every box can open — a box on a binary from before v2 would
167// stall the app's whole config render on a v2 envelope. It still validates
168// the binding, so a wrong caller fails before the switch ever flips.
169//
170// Mutation-mindedness: default boundSealing to true, or have Seal ignore it,
171// and the first assertion fails.
172func TestSealWritesV1UntilBoundSealingIsOn(t *testing.T) {
173 priv, pub := bindKeys(t)
174 t.Cleanup(func() { SetBoundSealing(false) })
175 b := Binding{Account: "aaa", App: "blog"}
176
177 SetBoundSealing(false)
178 env, err := Seal(pub, b, []byte("shh"))
179 if err != nil {
180 t.Fatal(err)
181 }
182 raw, _ := base64.StdEncoding.DecodeString(env)
183 if raw[0] != 0x04 {
184 t.Fatalf("with bound sealing off Seal wrote a non-v1 envelope (first byte %#x)", raw[0])
185 }
186 if _, err := Seal(pub, Binding{}, []byte("shh")); err == nil {
187 t.Fatal("Seal with bound sealing off accepted an empty binding")
188 }
189
190 SetBoundSealing(true)
191 env, err = Seal(pub, b, []byte("shh"))
192 if err != nil {
193 t.Fatal(err)
194 }
195 raw, _ = base64.StdEncoding.DecodeString(env)
196 if !bytes.HasPrefix(raw, []byte(boundPrefix)) {
197 t.Fatal("with bound sealing on Seal did not write v2")
198 }
199 if _, err := Open(priv, Binding{Account: "bbb", App: "blog"}, env); err == nil {
200 t.Fatal("v2 envelope from Seal opened under another account")
201 }
202}
203