package host import ( "bytes" "context" "crypto/ed25519" "encoding/json" "errors" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/carlosframework/platform/internal/hostfloor" "github.com/carlosframework/platform/internal/release" ) // install-channel --policy --identity (claim trial, untagged-pointer ruling // R1). The channel pointer `carlos system update` writes before a host floor // is published carries no release tag and no moves protocol: the console's // door resolves the release, mirrors its artifact and calls // release.PromoteSystem with {Version, Source "release:", Artifacts} // (reserveHostUpdate tags a release only once a floor is published, which in // the trial is step 7). install-channel then finds the release identity // through --identity, the file claims run publish-identity wrote, and never // through the pointer's tag. The console's own test, // TestSystemUpdateWritesAnUntaggedPointerBeforeTheFloor, pins the pointer's // shape these fixtures promote. // promoteAsSystemUpdate moves stable to version with the pointer shape `carlos // system update` writes before a floor is published. It does not run the // door: it calls f.store.Put for the artifact (scaffolding) and // release.PromoteSystem in-process with the untagged release {Version, // Source "release:", Artifacts}. The shape is tied to the console's // door only through TestSystemUpdateWritesAnUntaggedPointerBeforeTheFloor. func (f *channelFixture) promoteAsSystemUpdate(t *testing.T, version string, body []byte) { t.Helper() ctx := context.Background() key := release.SystemArtifactKey(version, "linux-arm64") if _, err := f.store.Put(ctx, key, body, ""); err != nil && !errors.Is(err, release.ErrCASConflict) { t.Fatal(err) } rel := release.SystemRelease{Version: version, Source: "release:" + version, Artifacts: map[string]release.SystemArtifact{ "linux-arm64": {Key: key, SHA256: hostfloor.Digest(body)}}} if _, err := release.PromoteSystem(ctx, f.store, release.RootSigner(f.priv), release.SystemComponentRouter, "stable", rel, release.SystemPromoteOptions{Actor: "operator@example.test"}); err != nil { t.Fatal(err) } p, _, err := release.GetSystemPointer(ctx, f.store, f.pub, release.SystemComponentRouter, "stable") if err != nil || p.Current.ReleaseTag != "" || p.Current.MovesProtocol != 0 { t.Fatalf("the promote is not the untagged pointer the trial found: %+v %v", p, err) } } // identityRelease is the release claims run publish-identity signs for tag // (claimactivation's identityRelease): version and tag both the tag, moves // protocol 1, the router artifact from the signed sums and a console // artifact. func identityRelease(tag string, router []byte) release.SystemRelease { return release.SystemRelease{Version: tag, Source: "release:" + tag, ReleaseTag: tag, MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{"linux-arm64": {Key: release.SystemArtifactKey(tag, "linux-arm64"), SHA256: hostfloor.Digest(router)}}, ConsoleArtifacts: map[string]release.SystemArtifact{"linux-arm64": {Key: "bootstrap/console/" + tag + "/carlos-linux-arm64", SHA256: hostfloor.Digest([]byte("console " + tag))}}} } // publishIdentity publishes rel as publish-identity does (SignSystemIdentity, // then PublishSystemIdentity create-only) and returns the published bytes, // which are what its --out file holds. func (f *channelFixture) publishIdentity(t *testing.T, rel release.SystemRelease) []byte { t.Helper() ctx := context.Background() wire, err := release.SignSystemIdentity(release.RootSigner(f.priv), rel) if err != nil { t.Fatal(err) } if err := release.PublishSystemIdentity(ctx, f.store, f.pub, wire); err != nil { t.Fatal(err) } key, _ := release.IdentityKey(rel.ReleaseTag) stored, _, err := f.store.Get(ctx, key) if err != nil { t.Fatal(err) } return stored } // untaggedUpdatedBox is the trial's leo on v0.28.3: installed at v1.3.0, // updated by the real apply to v1.4.0 from the untagged pointer `carlos // system update` wrote, so applied.json names v1.4.0, carlos is its artifact // and carlos.prev v1.3.0, with no channel identity. v1.4.0's identity is // published as publish-identity publishes it, and returned. func untaggedUpdatedBox(t *testing.T) (f *channelFixture, old, identity []byte) { t.Helper() f = &channelFixture{applyFixture: newApplyFixture(t, "stable", "v1.3.0")} old, err := os.ReadFile(systemBinaryPath(f.a.optDir)) if err != nil { t.Fatal(err) } f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n") applyChannelUpdateWith(t, f, "v1.4.0", f.body, func() { f.promoteAsSystemUpdate(t, "v1.4.0", f.body) }) if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, old) { t.Fatalf("the update left %q and %q", cur, prev) } identity = f.publishIdentity(t, identityRelease("v1.4.0", f.body)) f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { f.fetches++ return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) }, own: func(path string) error { f.owned = append(f.owned, path) return nil }} return f, old, identity } // The trial's 6.1a on an untagged pointer: refused before R1 for want of a // release tag, armed with --identity. func TestInstallChannelPolicyIdentityArmsAnUntaggedUpdatedBox(t *testing.T) { f, old, identity := untaggedUpdatedBox(t) ctx := context.Background() // Without --policy, exactly as before (ruling 6): no floor, the loop. before := boxFiles(t, f) if _, err := f.in.run(ctx); err == nil || err.Error() != "no host floor is published, so this box could not arm its guard and nothing was installed" { t.Fatalf("without --policy: %v", err) } if after := boxFiles(t, f); !equalFiles(before, after) { t.Fatalf("a refused run changed the box: %v", diffFiles(before, after)) } applied, _ := os.ReadFile(f.a.file("applied.json")) seen, _ := os.ReadFile(f.a.file("seen.json")) f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = identity got, err := f.in.run(ctx) if err != nil { t.Fatal(err) } // The tag the run names is the identity's: the pointer has none. if !got.Replaced || got.Already || got.Version != "v1.4.0" || got.Tag != "v1.4.0" { t.Fatalf("installed %+v", got) } if line := installedLine(got); line != "replaced the rollback copy carlos.prev with v1.4.0 (v1.4.0) from the stable channel, the release carlos already runs. the release carlos.prev held before is no longer on this box" { t.Fatalf("line %q", line) } if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || bytes.Equal(prev, old) { t.Fatalf("slots hold %q and %q", cur, prev) } if f.fetches != 0 || !prevRecorded(f.a.optDir, "v1.4.0") { t.Fatalf("%d fetches, record %v", f.fetches, prevRecorded(f.a.optDir, "v1.4.0")) } if a, _ := os.ReadFile(f.a.file("applied.json")); !bytes.Equal(a, applied) { t.Fatalf("applied.json changed: %s", a) } if s, _ := os.ReadFile(f.a.file("seen.json")); !bytes.Equal(s, seen) { t.Fatalf("seen.json changed: %s", s) } // What is kept for guard-arm is the --identity file, byte for byte, and // guard-arm's checks pass with it: its inputs, and both slots. held, err := os.ReadFile(channelIdentityPath(f.a.optDir)) if err != nil || !bytes.Equal(held, identity) { t.Fatalf("kept identity %v", err) } _, floor, err := guardArmInputs(f.in.policy, held, f.pub, "bucket", "v1.4.0", "linux-arm64", hostfloor.Digest(f.body)) if err != nil { t.Fatalf("guard-arm's inputs: %v", err) } for _, slot := range []string{systemBinaryPath(f.a.optDir), systemPrevPath(f.a.optDir)} { b, _ := os.ReadFile(slot) if err := hostfloor.CheckImage(f.pub, floor, held, "v1.4.0", "linux-arm64", hostfloor.Digest(b)); err != nil { t.Fatalf("guard-arm would refuse %s: %v", slot, err) } } again, err := f.in.run(ctx) if err != nil || !again.Already || again.Replaced || f.fetches != 0 { t.Fatalf("second run %+v %v", again, err) } if line := installedLine(again); line != "v1.4.0 (v1.4.0) from the stable channel is already installed" { t.Fatalf("again %q", line) } // Once the floor is published from the same plan, the run still passes. if err := hostfloor.Publish(ctx, f.store, release.RootSigner(f.priv), "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}); err != nil { t.Fatal(err) } if got, err := f.in.run(ctx); err != nil || !got.Already { t.Fatalf("published: %+v %v", got, err) } } // A fresh box on an untagged pointer: --identity names the release, and the // install fetches the pointer's artifact into both slots. func TestInstallChannelPolicyIdentityInstallsAFreshBoxOnAnUntaggedPointer(t *testing.T) { f := &channelFixture{applyFixture: newApplyFixture(t, "stable", "bootstrap")} if err := os.Remove(systemBinaryPath(f.a.optDir)); err != nil { t.Fatal(err) } f.body = []byte("#!/bin/sh\necho carlos v1.4.0\n") f.promoteAsSystemUpdate(t, "v1.4.0", f.body) identity := f.publishIdentity(t, identityRelease("v1.4.0", f.body)) f.in = channelInstall{a: f.a, bucket: "bucket", fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error { f.fetches++ return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha) }, own: func(string) error { return nil }} f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = identity got, err := f.in.run(context.Background()) if err != nil || got.Already || got.Replaced || got.Tag != "v1.4.0" { t.Fatalf("%+v %v", got, err) } if line := installedLine(got); line != "installed v1.4.0 (v1.4.0) from the stable channel as carlos and carlos.prev" { t.Fatalf("line %q", line) } if cur, prev := f.slots(t); !bytes.Equal(cur, f.body) || !bytes.Equal(prev, f.body) || f.fetches != 1 { t.Fatalf("slots %q %q after %d fetches", cur, prev, f.fetches) } if held, _ := os.ReadFile(channelIdentityPath(f.a.optDir)); !bytes.Equal(held, identity) { t.Fatal("the kept identity is not the --identity file") } } // Every refusal of --identity leaves the box as it was, fetches nothing and // keeps no identity. func TestInstallChannelPolicyIdentityRefusals(t *testing.T) { t.Parallel() _, otherKey, _ := ed25519.GenerateKey(nil) const notSigned = "the --identity file is not a release identity signed for this deployment, so nothing was installed" const notPublished = "the --identity file is not the identity published for v1.4.0, so nothing was installed" for _, c := range []struct { name string setup func(t *testing.T, f *channelFixture, identity []byte) []byte want string }{ {"no --identity", func(t *testing.T, f *channelFixture, identity []byte) []byte { return nil }, "--policy and --identity go together, so nothing was installed"}, {"not an identity", func(t *testing.T, f *channelFixture, identity []byte) []byte { return f.in.policy }, notSigned}, {"signed by another key", func(t *testing.T, f *channelFixture, identity []byte) []byte { wire, err := release.SignSystemIdentity(release.RootSigner(otherKey), identityRelease("v1.4.0", f.body)) if err != nil { t.Fatal(err) } return wire }, notSigned}, {"a channel pointer, not an identity", func(t *testing.T, f *channelFixture, identity []byte) []byte { wire, _, err := f.store.Get(context.Background(), release.SystemPointerKey("router", "stable")) if err != nil { t.Fatal(err) } return wire }, notSigned}, {"another identity under its tag", func(t *testing.T, f *channelFixture, identity []byte) []byte { rel := identityRelease("v1.4.0", f.body) rel.ConsoleArtifacts["linux-arm64"] = release.SystemArtifact{Key: rel.ConsoleArtifacts["linux-arm64"].Key, SHA256: hostfloor.Digest([]byte("another console"))} wire, err := release.SignSystemIdentity(release.RootSigner(f.priv), rel) if err != nil { t.Fatal(err) } return wire }, notPublished}, {"re-formatted", func(t *testing.T, f *channelFixture, identity []byte) []byte { var b bytes.Buffer if err := json.Compact(&b, identity); err != nil { t.Fatal(err) } reformatted := append([]byte(" "), b.Bytes()...) if _, err := release.VerifySystemIdentity(f.pub, reformatted); err != nil || bytes.Equal(reformatted, identity) { t.Fatalf("the re-formatted identity: %v", err) } return reformatted }, notPublished}, {"no identity published under its tag", func(t *testing.T, f *channelFixture, identity []byte) []byte { key, _ := release.IdentityKey("v1.4.0") if err := f.store.Delete(context.Background(), key); err != nil { t.Fatal(err) } return identity }, "no release identity is published for v1.4.0, so nothing was installed"}, {"signed for a tag never published", func(t *testing.T, f *channelFixture, identity []byte) []byte { wire, err := release.SignSystemIdentity(release.RootSigner(f.priv), identityRelease("v1.4.9", f.body)) if err != nil { t.Fatal(err) } return wire }, "no release identity is published for v1.4.9, so nothing was installed"}, {"another release's identity", func(t *testing.T, f *channelFixture, identity []byte) []byte { return f.publishIdentity(t, identityRelease("v1.5.0", []byte("#!/bin/sh\necho carlos v1.5.0\n"))) }, "the --identity file names v1.5.0 (v1.5.0), not the release the stable channel names, so nothing was installed"}, {"the same artifact under another version", func(t *testing.T, f *channelFixture, identity []byte) []byte { return f.publishIdentity(t, identityRelease("v1.4.1", f.body)) }, "the --identity file names v1.4.1 (v1.4.1), not the release the stable channel names, so nothing was installed"}, {"another artifact under the release", func(t *testing.T, f *channelFixture, identity []byte) []byte { key, _ := release.IdentityKey("v1.4.0") if err := f.store.Delete(context.Background(), key); err != nil { t.Fatal(err) } return f.publishIdentity(t, identityRelease("v1.4.0", []byte("#!/bin/sh\necho carlos v1.4.0 rebuilt\n"))) }, "the --identity file names v1.4.0 (v1.4.0), not the release the stable channel names, so nothing was installed"}, {"no artifact for this platform", func(t *testing.T, f *channelFixture, identity []byte) []byte { key, _ := release.IdentityKey("v1.4.0") if err := f.store.Delete(context.Background(), key); err != nil { t.Fatal(err) } rel := identityRelease("v1.4.0", f.body) rel.Artifacts = map[string]release.SystemArtifact{"linux-amd64": {Key: release.SystemArtifactKey("v1.4.0", "linux-amd64"), SHA256: hostfloor.Digest(f.body)}} return f.publishIdentity(t, rel) }, "the --identity file names v1.4.0 (v1.4.0), not the release the stable channel names, so nothing was installed"}, {"a pointer tagged with another release", func(t *testing.T, f *channelFixture, identity []byte) []byte { // The same version and artifact, but the pointer names another tag. rel := release.SystemRelease{Version: "v1.4.0", Source: "release:v1.4.0", ReleaseTag: "v1.4.1", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{ "linux-arm64": {Key: release.SystemArtifactKey("v1.4.0", "linux-arm64"), SHA256: hostfloor.Digest(f.body)}}} if _, err := release.PromoteSystem(context.Background(), f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } return identity }, "the --identity file names v1.4.0 (v1.4.0), not the release the stable channel names, so nothing was installed"}, {"an unreadable identity object", func(t *testing.T, f *channelFixture, identity []byte) []byte { open := f.a.openStore f.a.openStore = func(ctx context.Context) (release.Store, ed25519.PublicKey, error) { s, k, err := open(ctx) return identityUnreadable{s}, k, err } return identity }, "the release identity for v1.4.0 could not be read, so nothing was installed: door restarting"}, {"below the policy's floor", func(t *testing.T, f *channelFixture, identity []byte) []byte { f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v2.0.0"}) return identity }, "v1.4.0 (v1.4.0) is below the host floor v2.0.0 or is not the release its identity names, so nothing was installed"}, } { t.Run(c.name, func(t *testing.T) { f, _, identity := untaggedUpdatedBox(t) f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = c.setup(t, f, identity) before := boxFiles(t, f) _, err := f.in.run(context.Background()) if err == nil || err.Error() != c.want { t.Fatalf("err %v, want %q", err, c.want) } if after := boxFiles(t, f); !equalFiles(before, after) { t.Fatalf("a refused run changed the box: %v", diffFiles(before, after)) } if f.fetches != 0 { t.Fatalf("%d fetches", f.fetches) } if _, err := os.Stat(channelIdentityPath(f.a.optDir)); !os.IsNotExist(err) { t.Fatalf("a refused run kept an identity: %v", err) } }) } } // A pointer that does carry a release tag (as the console writes it once a // floor is published) passes when the tag names the --identity's release. func TestInstallChannelPolicyIdentityAcceptsAPointerTaggedWithIt(t *testing.T) { f, _, identity := untaggedUpdatedBox(t) ctx := context.Background() rel := release.SystemRelease{Version: "v1.4.0", Source: "release:v1.4.0", ReleaseTag: "v1.4.0", MovesProtocol: 1, Artifacts: map[string]release.SystemArtifact{ "linux-arm64": {Key: release.SystemArtifactKey("v1.4.0", "linux-arm64"), SHA256: hostfloor.Digest(f.body)}}} if _, err := release.PromoteSystem(ctx, f.store, release.RootSigner(f.priv), "router", "stable", rel, release.SystemPromoteOptions{}); err != nil { t.Fatal(err) } f.in.policy = signedPolicy(t, f.priv, "bucket", hostfloor.Floor{Min: 1, MinRelease: "v1.4.0"}) f.in.identity = identity if got, err := f.in.run(ctx); err != nil || !got.Replaced || got.Tag != "v1.4.0" { t.Fatalf("%+v %v", got, err) } if held, _ := os.ReadFile(channelIdentityPath(f.a.optDir)); !bytes.Equal(held, identity) { t.Fatal("the kept identity is not the --identity file") } } // The flags: --policy and --identity together or not at all, both read up // front through the hardened open; without either nothing is read. func TestInstallChannelInputsAreReadUpFront(t *testing.T) { dir := t.TempDir() policyPath, identityPath := filepath.Join(dir, "policy.json"), filepath.Join(dir, "identity.json") if p, i, err := readChannelInputs("", ""); p != nil || i != nil || err != nil { t.Fatalf("no flags: %v %v %v", p, i, err) } for _, c := range [][2]string{{policyPath, ""}, {"", identityPath}} { if _, _, err := readChannelInputs(c[0], c[1]); err == nil || err.Error() != "--policy and --identity go together, so nothing was installed" { t.Fatalf("%q: %v", c, err) } } raw := signedPolicyBytes(t) if err := os.WriteFile(policyPath, raw, 0o600); err != nil { t.Fatal(err) } if _, _, err := readChannelInputs(policyPath, identityPath); err == nil || !strings.HasPrefix(err.Error(), "the identity "+identityPath+" could not be read, so nothing was installed: ") { t.Fatalf("missing identity: %v", err) } if err := os.WriteFile(identityPath, []byte("identity bytes"), 0o600); err != nil { t.Fatal(err) } p, i, err := readChannelInputs(policyPath, identityPath) if err != nil || !bytes.Equal(p, raw) || string(i) != "identity bytes" { t.Fatalf("both: %v", err) } // The policy is still read and checked first. if err := os.WriteFile(policyPath, []byte(`{"mode":"unpublished"}`), 0o600); err != nil { t.Fatal(err) } if _, _, err := readChannelInputs(policyPath, filepath.Join(dir, "missing.json")); err == nil || err.Error() != "the --policy file is not an armed policy signed for this deployment, so nothing was installed" { t.Fatalf("a bad policy: %v", err) } } // identityUnreadable fails every read of a release identity object. type identityUnreadable struct{ release.Store } func (s identityUnreadable) Get(ctx context.Context, key string) ([]byte, string, error) { if strings.HasPrefix(key, "control/system/router/releases/") { return nil, "", errors.New("door restarting") } return s.Store.Get(ctx, key) } // identityOf is the release identity published for tag: what claims run // publish-identity's --out file holds. func (f *channelFixture) identityOf(t *testing.T, tag string) []byte { t.Helper() key, err := release.IdentityKey(tag) if err != nil { t.Fatal(err) } raw, _, err := f.store.Get(context.Background(), key) if err != nil { t.Fatal(err) } return raw } // installChannelCLIEnv makes this test binary run RunSystemInstallChannel // with the arguments it holds (one per line), so its exits can be observed. const installChannelCLIEnv = "CARLOS_TEST_INSTALL_CHANNEL_ARGS" // TestInstallChannelCLIChild is the child runInstallChannelCLI starts; run // without installChannelCLIEnv it does nothing. func TestInstallChannelCLIChild(t *testing.T) { raw, ok := os.LookupEnv(installChannelCLIEnv) if !ok { t.Skip("only as runInstallChannelCLI's child") } var args []string if raw != "" { args = strings.Split(raw, "\n") } RunSystemInstallChannel(args) os.Exit(0) } // runInstallChannelCLI runs `carlos system install-channel args...` in a // child of this test binary, not as root, and returns its combined output // and exit code. func runInstallChannelCLI(t *testing.T, args ...string) (string, int) { t.Helper() cmd := exec.Command(os.Args[0], "-test.run=^TestInstallChannelCLIChild$") cmd.Env = append(os.Environ(), installChannelCLIEnv+"="+strings.Join(args, "\n"), "CARLOS_SYSTEM_CHANNEL=") cmd.Dir = t.TempDir() out, err := cmd.CombinedOutput() code := 0 if ee, ok := err.(*exec.ExitError); ok { code = ee.ExitCode() } else if err != nil { t.Fatal(err) } return string(out), code } // The command's both-or-neither check (untagged-pointer R1, platform-00): // --policy alone and --identity alone are refused with exit 2 and the pair // line, before the root check and before any file is read; -h lists both // flags. func TestRunSystemInstallChannelTakesPolicyAndIdentityTogether(t *testing.T) { missing := filepath.Join(t.TempDir(), "absent.json") for _, args := range [][]string{{"--policy", missing}, {"--identity", missing}} { out, code := runInstallChannelCLI(t, args...) if code != 2 || strings.TrimSpace(out) != "--policy and --identity go together, so nothing was installed" { t.Fatalf("%v: exit %d, %q", args, code, out) } } out, code := runInstallChannelCLI(t, "-h") if code != 0 || !strings.HasPrefix(out, installChannelUsage) || !strings.Contains(out, " --identity "+installChannelIdentityFlag+"\n") || !strings.Contains(out, " --policy "+installChannelPolicyFlag+"\n") { t.Fatalf("-h: exit %d\n%s", code, out) } if os.Geteuid() != 0 { // With neither flag, unchanged: the root check is the first refusal. if out, code := runInstallChannelCLI(t); code != 1 || strings.TrimSpace(out) != installChannelNotRoot { t.Fatalf("no flags: exit %d, %q", code, out) } } }