package appconfig import ( "bytes" "context" "crypto/ecdh" "encoding/base64" "os" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) func bindKeys(t *testing.T) (*ecdh.PrivateKey, *ecdh.PublicKey) { t.Helper() priv, err := ecdh.P256().NewPrivateKey(fixedScalar(0x11)) if err != nil { t.Fatal(err) } return priv, priv.PublicKey() } // One CARLOS_SECRETS_KEY opens every envelope in a deployment, so before // the binding a secret sealed into one tenant's config, copied into // another's, was decrypted into the second tenant's environment // (tenant-isolation audit, discussion #220). A bound envelope opens only for // the (account, app) it was sealed for. // // Mutation-mindedness: seal or open with nil associated data and the // cross-account and cross-app opens below succeed. func TestBoundEnvelopeOpensOnlyForItsBinding(t *testing.T) { priv, pub := bindKeys(t) env, err := SealBound(pub, Binding{Account: "aaa", App: "blog"}, []byte("s3kr1t")) if err != nil { t.Fatal(err) } got, err := Open(priv, Binding{Account: "aaa", App: "blog"}, env) if err != nil || string(got) != "s3kr1t" { t.Fatalf("own binding: %q, %v", got, err) } for _, b := range []Binding{ {Account: "bbb", App: "blog"}, // same app name, another tenant {Account: "aaa", App: "shop"}, // same tenant, another app } { if got, err := Open(priv, b, env); err == nil || got != nil { t.Fatalf("envelope for aaa/blog opened as %s/%s", b.Account, b.App) } } } // Downgrade: cutting the v2 prefix off leaves bytes shaped exactly like a // v1 envelope, which opens without a binding. It must not open — the v2 key // derivation differs (boundInfo), so the GCM tag fails. // // Mutation-mindedness: set boundInfo to hkdfInfo and this opens. func TestBoundEnvelopeCannotBeDowngradedToUnbound(t *testing.T) { priv, pub := bindKeys(t) env, err := SealBound(pub, Binding{Account: "aaa", App: "blog"}, []byte("s3kr1t")) if err != nil { t.Fatal(err) } raw, _ := base64.StdEncoding.DecodeString(env) if !bytes.HasPrefix(raw, []byte(boundPrefix)) { t.Fatalf("Seal wrote no %s prefix", boundPrefix) } stripped := base64.StdEncoding.EncodeToString(raw[len(boundPrefix):]) if err := CheckEnvelope(stripped); err != nil { t.Fatalf("precondition: stripped envelope should look like v1: %v", err) } if got, err := Open(priv, Binding{Account: "bbb", App: "blog"}, stripped); err == nil || got != nil { t.Fatal("a v2 envelope stripped to v1 opened without its binding") } } // The v2 wire shape, pinned like v1's golden vector: a drift here means // envelopes already sealed stop opening. func TestBoundEnvelopeGoldenVector(t *testing.T) { priv, pub := bindKeys(t) eph, err := ecdh.P256().NewPrivateKey(fixedScalar(0x22)) if err != nil { t.Fatal(err) } nonce := make([]byte, 12) for i := range nonce { nonce[i] = byte(i + 1) } b := Binding{Account: "bab", App: "keymail"} aad, err := b.aad() if err != nil { t.Fatal(err) } got, err := sealWith(eph, nonce, pub, boundPrefix, boundInfo, aad, []byte("s3kr1t-value")) if err != nil { t.Fatal(err) } const want = "Q1NCMgSrTT1cMAUIiVUDWxuplu2yTDxWX6TUcFNW0TXikHFdvT876aH9TjYb0calxCZ5P+PqnTz0YNalrQpdLIBLE2lIAQIDBAUGBwgJCgsMxRFVmrtd/T04JgIGwUoHD+0wmlJIwGOB+IO4KQ==" if got != want { t.Fatalf("bound envelope drifted from the pinned wire format:\n got: %s\n want: %s", got, want) } if plain, err := Open(priv, b, got); err != nil || string(plain) != "s3kr1t-value" { t.Fatalf("Open: %q, %v", plain, err) } } func TestSealRefusesAnInvalidBinding(t *testing.T) { _, pub := bindKeys(t) for _, b := range []Binding{{}, {Account: "bab"}, {App: "keymail"}} { if _, err := SealBound(pub, b, []byte("x")); err == nil || !strings.Contains(err.Error(), "binding") { t.Fatalf("Seal with binding %+v: %v", b, err) } } } // The binding as the box applies it: a sync pass renders a route's config // as (route account, route app), so another tenant's envelope copied into // this route's bundle fails the render and the env file is never written. func TestSyncRefusesAnEnvelopeSealedForAnotherAccount(t *testing.T) { ctx := context.Background() priv, pub := bindKeys(t) for _, tc := range []struct { name string sealed Binding render bool }{ {"own account", Binding{Account: "bbb", App: "blog"}, true}, {"another account's blog", Binding{Account: "aaa", App: "blog"}, false}, } { t.Run(tc.name, func(t *testing.T) { s := release.NewDirStore(t.TempDir()) reg := testRegistry(t) envDir := t.TempDir() env, err := SealBound(pub, tc.sealed, []byte("shh")) if err != nil { t.Fatal(err) } if err := SetSecret(ctx, s, "bbb", "blog", map[string]string{"TOKEN": env}); err != nil { t.Fatal(err) } if err := reg.Upsert(registry.Route{ Host: "blog.example.com", Kind: "instance", App: "blog", Account: "bbb", Status: "ready", Backing: registry.BackingUnit, }); err != nil { t.Fatal(err) } if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil { t.Fatalf("Sync: %v", err) } got, err := os.ReadFile(filepath.Join(envDir, "blog.example.com.env")) if tc.render { if err != nil || !strings.Contains(string(got), "TOKEN=shh\n") { t.Fatalf("own secret not rendered: %q, %v", got, err) } return } if err == nil { t.Fatalf("another account's secret was rendered:\n%s", got) } }) } } // The rollout guard: until a deployment turns bound sealing on, Seal writes // v1, which every box can open — a box on a binary from before v2 would // stall the app's whole config render on a v2 envelope. It still validates // the binding, so a wrong caller fails before the switch ever flips. // // Mutation-mindedness: default boundSealing to true, or have Seal ignore it, // and the first assertion fails. func TestSealWritesV1UntilBoundSealingIsOn(t *testing.T) { priv, pub := bindKeys(t) t.Cleanup(func() { SetBoundSealing(false) }) b := Binding{Account: "aaa", App: "blog"} SetBoundSealing(false) env, err := Seal(pub, b, []byte("shh")) if err != nil { t.Fatal(err) } raw, _ := base64.StdEncoding.DecodeString(env) if raw[0] != 0x04 { t.Fatalf("with bound sealing off Seal wrote a non-v1 envelope (first byte %#x)", raw[0]) } if _, err := Seal(pub, Binding{}, []byte("shh")); err == nil { t.Fatal("Seal with bound sealing off accepted an empty binding") } SetBoundSealing(true) env, err = Seal(pub, b, []byte("shh")) if err != nil { t.Fatal(err) } raw, _ = base64.StdEncoding.DecodeString(env) if !bytes.HasPrefix(raw, []byte(boundPrefix)) { t.Fatal("with bound sealing on Seal did not write v2") } if _, err := Open(priv, Binding{Account: "bbb", App: "blog"}, env); err == nil { t.Fatal("v2 envelope from Seal opened under another account") } }