bastion 0.53.2
ci / personal-data (push) Successful in 5s
ci / test (push) Successful in 3m4s
ci / fuzz (push) Successful in 3m45s

This commit is contained in:
Privatae
2026-10-20 04:42:14 +00:00
commit e4c9b69b46
471 changed files with 159794 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
//go:build !windows
package main
// Bastion updates (0.44.0): on an anchored box a move to a NEWER version needs
// no approval, because the release signature is the approval for it. A move to
// an older version, or to one this binary cannot compare with its own, still
// needs the person's approval bound to that version. Tagged !windows for the
// anchored-box fixture (setupBox, anchor_test.go).
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"runtime"
"strings"
"sync/atomic"
"testing"
)
// updateBeat is a heartbeat response directing a self-update and carrying no
// approval.
func updateBeat(version string) []byte {
return []byte(`{"update":{"version":"` + version + `"}}`)
}
// A newer version reaches the signed manifest with no approval. The rest of the
// gate still holds: a member's directive and the local switch both refuse it.
func TestANewerSignedReleaseNeedsNoApproval(t *testing.T) {
setupBox(t)
freshVersionState(t)
t.Cleanup(func() { clearDirectiveRefusal("self-update") })
clientVersion = "0.44.0"
var fetches atomic.Int64
man := httptest.NewServer(signedStub(releaseKindBastion, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fetches.Add(1)
_, _ = w.Write([]byte(`{"version":"0.45.0","binaries":{}}`))
})))
defer man.Close()
manifestURL = man.URL + "/version.json"
maybeSelfUpdate(tMember, "", updateBeat("0.45.0"))
if fetches.Load() != 0 {
t.Fatal("a member's directive must be refused before any fetch, newer version or not")
}
maybeSelfUpdate(tAnchor, "", updateBeat("0.45.0"))
if fetches.Load() != 1 {
t.Fatalf("the anchor's directive to a newer version must reach the signed manifest with no approval, got %d fetches", fetches.Load())
}
if r := directiveRefusalReport(); r != nil {
t.Fatalf("the update passed, so the member's refusal is no longer reported: %v", r)
}
if err := localDenySet(tAnchor, localFamilyDirectives, true); err != nil {
t.Fatal(err)
}
maybeSelfUpdate(tAnchor, "", updateBeat("0.45.0"))
if fetches.Load() != 1 {
t.Fatal("switched off here, a newer version must not fetch either")
}
}
// The host learns the rule from the box itself, on the enforcing report every
// beat carries, so it never has to infer it from a version number. A build
// that cannot read its own version gates every update, and says so.
func TestTheBoxTellsTheHostAForwardUpdateNeedsNoApproval(t *testing.T) {
setupBox(t)
freshVersionState(t)
clientVersion = "0.44.0"
if v, _ := buildEnforcingReport("", nil)["update_forward_needs_no_approval"].(bool); !v {
t.Fatal("the enforcing report must say a forward update needs no approval")
}
for _, own := range []string{"dev", "0.44.0-rc1", ""} {
clientVersion = own
if v, _ := buildEnforcingReport("", nil)["update_forward_needs_no_approval"].(bool); v {
t.Fatalf("a build running %q can compare nothing, so it must not say so", own)
}
}
}
// Older, unstamped and suffixed versions are refused without an approval, and
// the host is told an approval is what is missing. With one, an older version
// proceeds.
func TestAnOlderOrUncomparableVersionStillNeedsAnApproval(t *testing.T) {
setupBox(t)
freshVersionState(t)
t.Cleanup(func() { clearDirectiveRefusal("self-update") })
var fetches atomic.Int64
man := httptest.NewServer(signedStub(releaseKindBastion, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fetches.Add(1)
_, _ = w.Write([]byte(`{"version":"0.43.1","binaries":{}}`))
})))
defer man.Close()
manifestURL = man.URL + "/version.json"
cases := []struct{ running, target string }{
{"0.44.0", "0.43.1"}, // older
{"0.44.0", "0.9.0"}, // older, and above 0.44.0 as a string
{"0.44.0", "0.44"}, // the running release, spelled shorter
{"dev", "0.45.0"}, // an unstamped build cannot say what is newer
{"0.44.0", "0.45.0-rc1"}, // nor can a suffixed target
}
for _, c := range cases {
clientVersion = c.running
clearDirectiveRefusal("self-update")
maybeSelfUpdate(tAnchor, "", updateBeat(c.target))
if fetches.Load() != 0 {
t.Fatalf("%s on a box running %s, with no approval, must be refused before any fetch", c.target, c.running)
}
if r := directiveRefusalReport(); r == nil || !strings.Contains(fmt.Sprint(r), "approval") {
t.Fatalf("%s on a box running %s: the host must be told an approval is missing: %v", c.target, c.running, r)
}
}
clientVersion = "0.44.0"
maybeSelfUpdate(tAnchor, "", testDirective(t, directiveSelfUpdate, []string{"0.43.1"}, string(updateBeat("0.43.1"))))
if fetches.Load() != 1 {
t.Fatal("approved, a move to an older version proceeds")
}
}
// Without an approval, the only thing that installs is a newer SIGNED release
// that is exactly the target. A signed manifest for another version (an old one
// served again by a download server, say) is not downloaded, and neither is an
// unsigned manifest for the target itself. The binary's digest never matches
// the payload here, so even a broken gate could not swap the test binary.
func TestTheForwardPathInstallsNothingButTheSignedTarget(t *testing.T) {
setupBox(t)
freshVersionState(t)
t.Cleanup(func() { clearDirectiveRefusal("self-update") })
clientVersion = "0.44.0"
key := runtime.GOOS + "-" + runtime.GOARCH
elsewhere := sha256.Sum256([]byte("some other build"))
var served atomic.Value
var downloads atomic.Int64
mux := http.NewServeMux()
mux.HandleFunc("/version.json", func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"version": served.Load().(string),
"binaries": map[string]any{key: map[string]any{
"url": "http://" + r.Host + "/dl", "sha256": hex.EncodeToString(elsewhere[:])}},
})
})
mux.HandleFunc("/dl", func(w http.ResponseWriter, r *http.Request) {
downloads.Add(1)
_, _ = w.Write([]byte("not the bastion"))
})
signed := httptest.NewServer(signedStub(releaseKindBastion, mux))
defer signed.Close()
unsigned := httptest.NewServer(mux)
defer unsigned.Close()
manifestURL = signed.URL + "/version.json"
served.Store("0.43.1")
maybeSelfUpdate(tAnchor, "", updateBeat("0.45.0"))
if downloads.Load() != 0 {
t.Fatal("a signed manifest for another version must not be downloaded from")
}
manifestURL = unsigned.URL + "/version.json"
served.Store("0.45.0")
maybeSelfUpdate(tAnchor, "", updateBeat("0.45.0"))
if downloads.Load() != 0 {
t.Fatal("an unsigned manifest must not be downloaded from, even for the target")
}
// The control: signed and naming the target, the download does happen (and
// the digest mismatch then stops the swap).
manifestURL = signed.URL + "/version.json"
maybeSelfUpdate(tAnchor, "", updateBeat("0.45.0"))
if downloads.Load() != 1 {
t.Fatalf("a signed manifest naming the target must be downloaded from with no approval, got %d", downloads.Load())
}
}