carlos / platform Public

moves: the HMAC launch report and T3's comparison; activator: LaunchEnvironment (§7.3)

ReportKeyFromSecrets rejects malformed base64 and any key that is not 32 bytes.
LaunchEnvironment composes the pre-spawn environment and reports names the
instance's own .env shadows; instanceEnv is unchanged (now over instanceEnvParts).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 32509bba8725d7fc9f62e24e74a48ce98352673d parent 58e6c49
4 files changed, +439 −5
  • internal/activator/backend_exec.go +60 −5
  • internal/activator/backend_exec_test.go +55 −0
  • internal/moves/envreport.go +173 −0
  • internal/moves/envreport_test.go +151 −0
diff --git a/internal/activator/backend_exec.go b/internal/activator/backend_exec.go
index 60fbaa181..d1f8008b7 100644
--- a/internal/activator/backend_exec.go
+++ b/internal/activator/backend_exec.go
@@ -1162,6 +1162,18 @@ func (b *ExecBackend) nestedGate(rt registry.Route) error {
// instance's CARLOS_ADMIN_TOKEN into <host>.env first, so "the same
// environment" includes the same secret.)
func (b *ExecBackend) instanceEnv(rt registry.Route) (map[string]string, error) {
+ cfgEnv, ownEnv, err := b.instanceEnvParts(rt)
+ if err != nil {
+ return nil, err
+ }
+ return mergeEnv(cfgEnv, ownEnv), nil
+}
+
+// instanceEnvParts is instanceEnv's two reads, before they are merged:
+// the sync-owned config layer and the instance's own file. Extracted so
+// LaunchEnvironment can see which names the own file shadows; instanceEnv
+// is exactly mergeEnv over these two, as it was.
+func (b *ExecBackend) instanceEnvParts(rt registry.Route) (cfgEnv, ownEnv map[string]string, err error) {
paths := b.pathsFor(rt)
// A nested host's files live under a tenant-writable directory (design
// §3.2): read them without following a symlink. A flat host's files sit
@@ -1172,15 +1184,58 @@ func (b *ExecBackend) instanceEnv(rt registry.Route) (map[string]string, error)
return readEnvFileBeneath(filepath.Dir(path), filepath.Base(path))
}
}
- cfgEnv, err := read(paths.configEnv)
+ cfgEnv, err = read(paths.configEnv)
if err != nil && !os.IsNotExist(err) {
- return nil, fmt.Errorf("read config env file: %w", err)
+ return nil, nil, fmt.Errorf("read config env file: %w", err)
}
- ownEnv, err := read(paths.envFile)
+ ownEnv, err = read(paths.envFile)
if err != nil && !os.IsNotExist(err) {
- return nil, fmt.Errorf("read env file: %w", err)
+ return nil, nil, fmt.Errorf("read env file: %w", err)
}
- return mergeEnv(cfgEnv, ownEnv), nil
+ return cfgEnv, ownEnv, nil
+}
+
+// LaunchEnvironment is the composed environment the next Start of rt
+// would spawn with: childEnv over agentOwnedVars over instanceEnv, the
+// exact composition Start uses (agentOwnedVars(rt, fileEnv, haveControl)),
+// decoded back to a map. agentOwnedVars REMOVES tenant-provided
+// CARLOS_CONTROL_SOCKET, CARLOS_PURGE and CARLOS_REGION when the agent
+// does not provide them, so a report over mergeEnv alone would misstate
+// the environment. haveControl is true: startChild calls
+// edge.ListenControl for EVERY exec child, and b.Control is the nested
+// root-control client, which says nothing about the flat channel, so
+// before the spawn the honest composition is "the agent will bind it"; a
+// bind that fails at spawn is logged there and the probe's spawn-time
+// check compares the actual outcome. CARLOS_PURGE follows b.Cache != nil
+// exactly as agentOwnedVars does.
+//
+// shadowed is the sorted names present in both the rendered configuration
+// file and the instance's own <host>.env: the override mergeEnv applies
+// (moves spec §7.3 T3 reports it), collected here because this is the only
+// layer that sees both files. The move's launch report (§7.3, codex R2-8)
+// is computed over exactly this env, so a target compares what would run,
+// not a reconstruction of it. The values are raw: the caller HMACs them
+// and must never log or return them.
+func (b *ExecBackend) LaunchEnvironment(rt registry.Route) (map[string]string, []string, error) {
+ cfgEnv, ownEnv, err := b.instanceEnvParts(rt)
+ if err != nil {
+ return nil, nil, err
+ }
+ var shadowed []string
+ for k := range ownEnv {
+ if _, rendered := cfgEnv[k]; rendered {
+ shadowed = append(shadowed, k)
+ }
+ }
+ sort.Strings(shadowed)
+ fileEnv := mergeEnv(cfgEnv, ownEnv)
+ out := map[string]string{}
+ for _, kv := range childEnv(b.agentOwnedVars(rt, fileEnv, true)) {
+ if i := strings.IndexByte(kv, '='); i > 0 {
+ out[kv[:i]] = kv[i+1:]
+ }
+ }
+ return out, shadowed, nil
}
// childEnv is the child's complete environment: the agent's own, minus
diff --git a/internal/activator/backend_exec_test.go b/internal/activator/backend_exec_test.go
index 8b4c41ceb..a3602a38c 100644
--- a/internal/activator/backend_exec_test.go
+++ b/internal/activator/backend_exec_test.go
@@ -13,6 +13,7 @@ import (
_ "modernc.org/sqlite"
"github.com/carlosframework/platform/internal/cgroup"
+ "github.com/carlosframework/platform/internal/edge"
"github.com/carlosframework/platform/internal/lease"
"github.com/carlosframework/platform/internal/registry"
)
@@ -315,3 +316,57 @@ func writeMinimalSQLite(dbPath string) error {
}
return db.Close()
}
+
+func TestLaunchEnvironmentComposesTheSpawnEnvironment(t *testing.T) {
+ b := &ExecBackend{DataDir: t.TempDir(), Region: "ie"} // Cache nil: no purge channel on this box
+ rt := registry.Route{Host: "a.go.example.com", Socket: filepath.Join(b.DataDir, "a.sock")}
+ if err := os.WriteFile(filepath.Join(b.DataDir, rt.Host+".env"), []byte("MINE=1\nCARLOS_ADMIN_TOKEN=t\nCARLOS_PURGE=header\nCARLOS_REGION=fake\n"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ t.Setenv("CARLOS_SECRETS_KEY", "never")
+ t.Setenv("UNRELATED", "yes")
+ // The rendered configuration file (the path instanceEnv reads first)
+ // renders MINE=0; the instance's own .env overrides it, so MINE is
+ // reported as shadowed (spec §7.3 T3).
+ if err := os.WriteFile(filepath.Join(b.DataDir, rt.Host+".config.env"), []byte("MINE=0\nFROM_CONFIG=c\n"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ env, shadowed, err := b.LaunchEnvironment(rt)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if len(shadowed) != 1 || shadowed[0] != "MINE" || env["FROM_CONFIG"] != "c" {
+ t.Fatalf("shadowed %v (env values withheld: the agent's own environment is in it)", shadowed)
+ }
+ if env["MINE"] != "1" || env["UNRELATED"] != "yes" || env["CARLOS_SECRETS_KEY"] != "" || env["CARLOS_ADMIN_TOKEN"] != "t" {
+ t.Fatalf("launch env: MINE=%q SECRETS_KEY set=%v UNRELATED=%q", env["MINE"], env["CARLOS_SECRETS_KEY"] != "", env["UNRELATED"])
+ }
+ // The agent-owned layer, exactly as agentOwnedVars composes it: the
+ // tenant's region and purge values are overridden or removed.
+ if env["CARLOS_REGION"] != "ie" || env["CARLOS_CONTROL_SOCKET"] != rt.Socket+".control" {
+ t.Fatalf("agent-owned vars: region %q control %q", env["CARLOS_REGION"], env["CARLOS_CONTROL_SOCKET"])
+ }
+ if _, ok := env["CARLOS_PURGE"]; ok {
+ t.Fatal("CARLOS_PURGE survived on a box without a cache")
+ }
+ b.Cache = edge.NewCache(16, 1<<20)
+ if env, _, _ = b.LaunchEnvironment(rt); env["CARLOS_PURGE"] != "header" {
+ t.Fatalf("CARLOS_PURGE on a box with a cache: %q", env["CARLOS_PURGE"])
+ }
+ // A box with no region strips the tenant's value rather than keeping it.
+ b.Region = ""
+ if env, _, _ = b.LaunchEnvironment(rt); env["CARLOS_REGION"] != "" {
+ t.Fatalf("CARLOS_REGION survived on a box with no region: %q", env["CARLOS_REGION"])
+ }
+}
+
+// With neither file present the composition is the agent-owned layer over
+// the agent's own environment, no error and nothing shadowed.
+func TestLaunchEnvironmentNoFiles(t *testing.T) {
+ b := &ExecBackend{DataDir: t.TempDir(), Region: "ie"}
+ rt := registry.Route{Host: "b.go.example.com", Socket: filepath.Join(b.DataDir, "b.sock")}
+ env, shadowed, err := b.LaunchEnvironment(rt)
+ if err != nil || len(shadowed) != 0 || env["CARLOS_REGION"] != "ie" {
+ t.Fatalf("region %q shadowed %v err %v", env["CARLOS_REGION"], shadowed, err)
+ }
+}
diff --git a/internal/moves/envreport.go b/internal/moves/envreport.go
new file mode 100644
index 000000000..6837fb41c
--- /dev/null
+++ b/internal/moves/envreport.go
@@ -0,0 +1,173 @@
+package moves
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "io"
+ "sort"
+ "strings"
+
+ "golang.org/x/crypto/hkdf"
+)
+
+const RegionVar = "CARLOS_REGION"
+
+// secretsKeyLen is the length of CARLOS_SECRETS_KEY's raw bytes: a P-256
+// private scalar (internal/appconfig GenerateKey).
+const secretsKeyLen = 32
+
+var (
+ // LayoutVars name a channel the flat layout always provides: present
+ // on both sides of a flat move, values not compared (they are paths
+ // on each box).
+ LayoutVars = map[string]bool{"CARLOS_CONTROL_SOCKET": true}
+ // AdvisoryVars follow a box fact (CARLOS_PURGE is set iff the box has
+ // a response cache, ExecBackend.agentOwnedVars): a presence difference
+ // is reported as a note, never a failure (round-4 finding 10). The
+ // spec's T3 table puts CARLOS_PURGE in the layout class; the plan
+ // deliberately makes it advisory, and the spec ledger records that.
+ AdvisoryVars = map[string]bool{"CARLOS_PURGE": true}
+ // BoxVars are the per-process variables of a box's own shell or
+ // systemd unit, never compared. systemd may inject others
+ // (SYSTEMD_EXEC_PID, MEMORY_PRESSURE_WATCH); the rehearsal checks the
+ // real box.
+ BoxVars = map[string]bool{"INVOCATION_ID": true, "JOURNAL_STREAM": true, "HOSTNAME": true, "PWD": true, "OLDPWD": true, "SHLVL": true, "_": true}
+)
+
+// ReportKeyFromSecrets derives the report's HMAC key from
+// CARLOS_SECRETS_KEY (HKDF-SHA256), so the report reveals names and
+// equality only and only to boxes holding the deployment's key (§7.3).
+// The input must be the base64 of exactly 32 raw bytes; anything else is
+// an error, and the error never carries the input.
+func ReportKeyFromSecrets(secretsKeyB64 string) ([]byte, error) {
+ raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(secretsKeyB64))
+ if err != nil {
+ return nil, fmt.Errorf("moves: secrets key: %w", err)
+ }
+ if len(raw) != secretsKeyLen {
+ return nil, errors.New("moves: secrets key: not a 32-byte key")
+ }
+ key := make([]byte, 32)
+ if _, err := io.ReadFull(hkdf.New(sha256.New, raw, nil, []byte("carlos-move-env-report-v1")), key); err != nil {
+ return nil, err
+ }
+ return key, nil
+}
+
+type EnvReport struct {
+ V int `json:"v"`
+ Vars map[string]string `json:"vars"`
+ Digest string `json:"digest"`
+}
+
+// ReportEnv HMACs every value (bound to its name); Digest identifies the
+// whole report. Raw values never appear in the result.
+func ReportEnv(key []byte, env map[string]string) EnvReport {
+ r := EnvReport{V: 1, Vars: make(map[string]string, len(env))}
+ names := make([]string, 0, len(env))
+ for k, v := range env {
+ mac := hmac.New(sha256.New, key)
+ mac.Write([]byte(k))
+ mac.Write([]byte{0})
+ mac.Write([]byte(v))
+ r.Vars[k] = hex.EncodeToString(mac.Sum(nil))
+ names = append(names, k)
+ }
+ sort.Strings(names)
+ h := sha256.New()
+ for _, k := range names {
+ fmt.Fprintf(h, "%s=%s\n", k, r.Vars[k])
+ }
+ r.Digest = "sha256:" + hex.EncodeToString(h.Sum(nil))
+ return r
+}
+
+// Class is T3's variable class.
+func Class(name string) string {
+ switch {
+ case name == RegionVar:
+ return "region"
+ case LayoutVars[name]:
+ return "layout"
+ case AdvisoryVars[name]:
+ return "advisory"
+ case BoxVars[name]:
+ return "box"
+ default:
+ return "equal"
+ }
+}
+
+type EnvDiff struct {
+ Name, Kind string
+}
+
+// Note reports a diff T3 records without failing.
+func (d EnvDiff) Note() bool { return d.Kind == "advisory-differs" }
+
+// CompareEnv is T3: region must differ, layout variables must be present
+// iff the flat layout provides them (value not compared), advisory
+// variables report a presence difference as a note (EnvDiff.Note), box
+// variables are not compared, everything else must be equal by HMAC. T3
+// fails on any diff that is not a note.
+func CompareEnv(source, target EnvReport, flatLayout bool) []EnvDiff {
+ var diffs []EnvDiff
+ seen := map[string]bool{}
+ for name, s := range source.Vars {
+ seen[name] = true
+ tv, ok := target.Vars[name]
+ switch Class(name) {
+ case "region":
+ if !ok {
+ diffs = append(diffs, EnvDiff{name, "region-missing"}) // the target must set its own region
+ } else if tv == s {
+ diffs = append(diffs, EnvDiff{name, "region-same"})
+ }
+ case "layout":
+ if flatLayout && !ok {
+ diffs = append(diffs, EnvDiff{name, "layout-missing"})
+ }
+ if !flatLayout && ok {
+ diffs = append(diffs, EnvDiff{name, "layout-present"})
+ }
+ case "advisory":
+ if !ok {
+ diffs = append(diffs, EnvDiff{name, "advisory-differs"})
+ }
+ case "box":
+ default:
+ if !ok {
+ diffs = append(diffs, EnvDiff{name, "missing"})
+ } else if tv != s {
+ diffs = append(diffs, EnvDiff{name, "differs"})
+ }
+ }
+ }
+ for name := range target.Vars {
+ if seen[name] {
+ continue
+ }
+ switch Class(name) {
+ case "equal":
+ diffs = append(diffs, EnvDiff{name, "extra"})
+ case "advisory":
+ diffs = append(diffs, EnvDiff{name, "advisory-differs"})
+ }
+ }
+ // layout names absent from BOTH reports are still checked: a flat
+ // landing must provide every layout variable the flat layout provides
+ for name := range LayoutVars {
+ if _, inSrc := source.Vars[name]; inSrc {
+ continue // handled above
+ }
+ if _, inTgt := target.Vars[name]; flatLayout && !inTgt {
+ diffs = append(diffs, EnvDiff{name, "layout-missing"})
+ }
+ }
+ sort.Slice(diffs, func(i, j int) bool { return diffs[i].Name < diffs[j].Name })
+ return diffs
+}
diff --git a/internal/moves/envreport_test.go b/internal/moves/envreport_test.go
new file mode 100644
index 000000000..bf6ffd09a
--- /dev/null
+++ b/internal/moves/envreport_test.go
@@ -0,0 +1,151 @@
+package moves
+
+import (
+ "encoding/base64"
+ "strings"
+ "testing"
+)
+
+func TestEnvReportRevealsNamesAndEqualityOnly(t *testing.T) {
+ key := []byte("0123456789abcdef0123456789abcdef")
+ src := ReportEnv(key, map[string]string{"DATABASE_URL": "sqlite:a", "CARLOS_REGION": "ie", "CARLOS_CONTROL_SOCKET": "/run/x", "INVOCATION_ID": "1", "SECRET": "s3"})
+ if src.Vars["SECRET"] == "s3" || len(src.Vars["SECRET"]) != 64 {
+ t.Fatal("a value leaked or the mac is not hex sha256")
+ }
+ same := ReportEnv(key, map[string]string{"DATABASE_URL": "sqlite:a", "CARLOS_REGION": "uk", "CARLOS_CONTROL_SOCKET": "/run/y", "INVOCATION_ID": "2", "SECRET": "s3"})
+ if diffs := CompareEnv(src, same, true); len(diffs) != 0 {
+ t.Fatalf("a matching target reported %v", diffs)
+ }
+ // Region must differ; layout vars are present iff flat provides them;
+ // box vars are never compared; everything else equal.
+ sameRegion := ReportEnv(key, map[string]string{"DATABASE_URL": "sqlite:a", "CARLOS_REGION": "ie", "CARLOS_CONTROL_SOCKET": "/run/y", "SECRET": "s3"})
+ diffs := CompareEnv(src, sameRegion, true)
+ if len(diffs) != 1 || diffs[0].Name != "CARLOS_REGION" || diffs[0].Kind != "region-same" {
+ t.Fatalf("same region: %v", diffs)
+ }
+ noRegion := ReportEnv(key, map[string]string{"DATABASE_URL": "sqlite:a", "CARLOS_CONTROL_SOCKET": "/run/y", "SECRET": "s3"})
+ if diffs := CompareEnv(src, noRegion, true); len(diffs) != 1 || diffs[0].Kind != "region-missing" {
+ t.Fatalf("missing region: %v", diffs)
+ }
+ // the layout variable absent from both: still required of a flat landing
+ srcNoLayout := ReportEnv(key, map[string]string{"DATABASE_URL": "sqlite:a", "CARLOS_REGION": "ie"})
+ if diffs := CompareEnv(srcNoLayout, ReportEnv(key, map[string]string{"DATABASE_URL": "sqlite:a", "CARLOS_REGION": "uk"}), true); len(diffs) != 1 || diffs[0].Name != "CARLOS_CONTROL_SOCKET" || diffs[0].Kind != "layout-missing" {
+ t.Fatalf("layout name absent from both: %v", diffs)
+ }
+ // CARLOS_PURGE is advisory (set iff the box has a cache): a presence
+ // difference is a note, never a failure; main omits the variable on a
+ // box without a cache (backend_exec.go agentOwnedVars), so neither
+ // example above carries it and they compare clean.
+ purge := ReportEnv(key, map[string]string{"DATABASE_URL": "sqlite:a", "CARLOS_REGION": "uk", "CARLOS_CONTROL_SOCKET": "/run/y", "SECRET": "s3", "CARLOS_PURGE": "header"})
+ if diffs := CompareEnv(src, purge, true); len(diffs) != 1 || diffs[0].Name != "CARLOS_PURGE" || diffs[0].Kind != "advisory-differs" || !diffs[0].Note() {
+ t.Fatalf("purge presence: %v", diffs)
+ }
+ drift := ReportEnv(key, map[string]string{"DATABASE_URL": "sqlite:b", "CARLOS_REGION": "uk", "EXTRA": "x"})
+ kinds := map[string]string{}
+ for _, d := range CompareEnv(src, drift, false) {
+ kinds[d.Name] = d.Kind
+ }
+ if kinds["DATABASE_URL"] != "differs" || kinds["SECRET"] != "missing" || kinds["EXTRA"] != "extra" || kinds["CARLOS_CONTROL_SOCKET"] != "" {
+ t.Fatalf("drift: %v", kinds)
+ }
+ if ReportEnv([]byte("another-key-another-key-another!"), map[string]string{"SECRET": "s3"}).Vars["SECRET"] == src.Vars["SECRET"] {
+ t.Fatal("the mac does not depend on the key")
+ }
+ if src.Digest == "" || src.Digest == same.Digest {
+ t.Fatal("digest")
+ }
+}
+
+func TestEnvReportClassesAndNotes(t *testing.T) {
+ for name, want := range map[string]string{
+ "CARLOS_REGION": "region", "CARLOS_CONTROL_SOCKET": "layout", "CARLOS_PURGE": "advisory",
+ "INVOCATION_ID": "box", "JOURNAL_STREAM": "box", "HOSTNAME": "box", "PWD": "box", "OLDPWD": "box", "SHLVL": "box", "_": "box",
+ "DATABASE_URL": "equal", "CARLOS_ADMIN_TOKEN": "equal",
+ } {
+ if got := Class(name); got != want {
+ t.Errorf("Class(%q) = %q, want %q", name, got, want)
+ }
+ }
+ if len(LayoutVars) != 1 || !LayoutVars["CARLOS_CONTROL_SOCKET"] || len(AdvisoryVars) != 1 || !AdvisoryVars["CARLOS_PURGE"] {
+ t.Fatalf("layout %v advisory %v", LayoutVars, AdvisoryVars)
+ }
+ for _, k := range []string{"missing", "extra", "differs", "region-missing", "region-same", "layout-missing", "layout-present"} {
+ if (EnvDiff{"X", k}).Note() {
+ t.Errorf("%s is a note", k)
+ }
+ }
+ if !(EnvDiff{"X", "advisory-differs"}).Note() {
+ t.Error("advisory-differs is not a note")
+ }
+}
+
+func TestCompareEnvNestedLayout(t *testing.T) {
+ key := []byte("0123456789abcdef0123456789abcdef")
+ src := ReportEnv(key, map[string]string{"A": "1", "CARLOS_REGION": "ie"})
+ tgt := ReportEnv(key, map[string]string{"A": "1", "CARLOS_REGION": "uk", "CARLOS_CONTROL_SOCKET": "/run/y"})
+ diffs := CompareEnv(src, tgt, false)
+ // a layout name only the target carries is not "extra": the not-flat
+ // rule (layout-present) fires only for a name in the source report, so
+ // the target-only case is pinned here as the current behaviour.
+ if len(diffs) != 0 {
+ t.Fatalf("target-only layout var, non-flat: %v", diffs)
+ }
+ src2 := ReportEnv(key, map[string]string{"A": "1", "CARLOS_REGION": "ie", "CARLOS_CONTROL_SOCKET": "/run/x"})
+ if diffs := CompareEnv(src2, tgt, false); len(diffs) != 1 || diffs[0].Kind != "layout-present" {
+ t.Fatalf("layout-present: %v", diffs)
+ }
+}
+
+func TestReportEnvIsDeterministicAndDigestCoversNamesAndValues(t *testing.T) {
+ key := []byte("0123456789abcdef0123456789abcdef")
+ a := ReportEnv(key, map[string]string{"A": "1", "B": "2"})
+ b := ReportEnv(key, map[string]string{"B": "2", "A": "1"})
+ if a.Digest != b.Digest || a.V != 1 || !strings.HasPrefix(a.Digest, "sha256:") {
+ t.Fatalf("%v %v", a, b)
+ }
+ // the mac binds the name: the same value under another name differs
+ c := ReportEnv(key, map[string]string{"A": "2", "B": "1"})
+ if c.Digest == a.Digest || c.Vars["A"] == a.Vars["B"] {
+ t.Fatal("the mac does not bind the name")
+ }
+ // name/value boundary: "AB"+"" is not "A"+"B"
+ if ReportEnv(key, map[string]string{"AB": ""}).Vars["AB"] == ReportEnv(key, map[string]string{"A": "B"}).Vars["A"] {
+ t.Fatal("name and value run together")
+ }
+}
+
+func TestReportKeyFromSecrets(t *testing.T) {
+ raw := make([]byte, 32)
+ for i := range raw {
+ raw[i] = byte(i + 1)
+ }
+ good := base64.StdEncoding.EncodeToString(raw)
+ k1, err := ReportKeyFromSecrets(good)
+ if err != nil || len(k1) != 32 {
+ t.Fatalf("%v %d", err, len(k1))
+ }
+ k2, _ := ReportKeyFromSecrets(good)
+ if string(k1) != string(k2) {
+ t.Fatal("not deterministic")
+ }
+ if string(k1) == string(raw) {
+ t.Fatal("the report key is the raw secret")
+ }
+ raw[0] ^= 1
+ k3, err := ReportKeyFromSecrets(base64.StdEncoding.EncodeToString(raw))
+ if err != nil || string(k3) == string(k1) {
+ t.Fatalf("key does not depend on the secret: %v", err)
+ }
+ for name, in := range map[string]string{
+ "malformed base64": "not base64!!",
+ "empty": "",
+ "short": base64.StdEncoding.EncodeToString(raw[:31]),
+ "long": base64.StdEncoding.EncodeToString(append(append([]byte{}, raw...), 0)),
+ } {
+ if k, err := ReportKeyFromSecrets(in); err == nil || k != nil {
+ t.Errorf("%s: key %v err %v", name, k, err)
+ } else if strings.Contains(err.Error(), in) && in != "" {
+ t.Errorf("%s: the error echoes the input: %v", name, err)
+ }
+ }
+}