Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/preserve-beta-driver-trust.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": patch
---

Keep official driver signature checks and read-only limits after repository names change. Use the trust recorded at installation and verify the saved manifest again; stop a managed driver if its trusted source cannot be found.
165 changes: 165 additions & 0 deletions go/internal/driverrepo/beta_runtime_policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package driverrepo

import (
"context"
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"github.com/srcfl/ftw/go/internal/config"
"github.com/srcfl/ftw/go/internal/state"
)

type betaPolicyFixture struct {
manager *Manager
installed state.DriverRepoInstall
driver config.Driver
newManager func([]string) *Manager
}

func installBetaPolicyFixture(t *testing.T, repositoryIDs []string) betaPolicyFixture {
t.Helper()
public, private, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
fixture := &signedFixture{private: private}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/manifest.json":
_, _ = w.Write(fixture.envelope(t))
case "/demo.lua":
_, _ = w.Write(fixture.driver)
default:
http.NotFound(w, r)
}
}))
t.Cleanup(server.Close)
fixture.setVersion(server.URL, "1.2.2")
fixture.manifest.Repository = "https://github.com/srcfl/device-drivers"
entry := &fixture.manifest.Drivers[0]
entry.ReadOnly, entry.Metadata.ReadOnly = true, true
entry.Permissions = []string{"http.get", "http.post"}
entry.Metadata.AuthPostPath = "/oauth/token"
entry.Metadata.ConfigSecrets = []string{"refresh_token"}
dir := t.TempDir()
st, err := state.Open(filepath.Join(dir, "state.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
newManager := func(ids []string) *Manager {
cfg := &config.DeviceRepository{}
for _, id := range ids {
cfg.Repositories = append(cfg.Repositories, config.DriverRepositorySource{
ID: id, ManifestURL: "https://unrelated.invalid/manifest.json", AllowUnsigned: true,
})
}
m := New(cfg, dir, st)
// Keep the production alias calculation, replacing only the fixture
// transport and signing key. No production private key is available.
m.betaRepo.ManifestURL = server.URL + "/manifest.json"
m.betaRepo.AllowInsecure = true
m.betaRepo.TrustedKeys = map[string]string{"test": base64.StdEncoding.EncodeToString(public)}
return m
}
m := newManager(repositoryIDs)
installed, err := m.InstallChannel(context.Background(), "beta", "demo", "1.2.2")
if err != nil {
t.Fatal(err)
}
if installed.FTWSigned {
t.Fatal("fixture key was incorrectly recorded as FTW's production key")
}
// Model the durable provenance that the production installer records for
// its pinned key. Signature verification still runs with the fixture key.
installed.FTWSigned = true
installed, err = st.ActivateDriverRepoInstall(installed)
if err != nil {
t.Fatal(err)
}
return betaPolicyFixture{m, installed, config.Driver{Name: "demo", Lua: filepath.Join(m.ActiveDir(), "demo.lua")}, newManager}
}

func TestOfficialBetaPolicySurvivesRepositoryAliasChanges(t *testing.T) {
beta := config.DefaultDriverRepositoryBetaID
for _, tc := range []struct {
name string
before, after []string
}{
{"collision_removed", []string{beta}, nil},
{"collision_added", nil, []string{beta}},
{"multiple_collisions_removed", []string{beta, beta + "-channel"}, nil},
} {
t.Run(tc.name, func(t *testing.T) {
f := installBetaPolicyFixture(t, tc.before)
reloaded := f.newManager(tc.after)
if reloaded.betaRepo.ID == f.installed.RepoID {
t.Fatal("test did not change the generated beta alias")
}
// The current repository owning this alias may already have loaded
// different, even unsigned metadata into the shared memory cache.
unrelated := f.manager.manifests[f.installed.RepoID]
unrelated.Drivers = append([]ManifestDriver(nil), unrelated.Drivers...)
unrelated.Drivers[0].ReadOnly = false
unrelated.Drivers[0].Metadata.ReadOnly = false
unrelated.Drivers[0].ControlEnabled = true
reloaded.manifests[f.installed.RepoID] = unrelated
policy, err := reloaded.RuntimePolicy(f.driver)
if err != nil || policy == nil || !policy.IsReadOnly() || policy.ArtifactSHA256 != f.installed.SHA256 ||
policy.AuthPostPath != "/oauth/token" || len(policy.ConfigSecrets) != 1 || policy.ConfigSecrets[0] != "refresh_token" {
t.Fatalf("beta policy after alias change = %+v, %v", policy, err)
}
})
}
}

func TestOfficialBetaPolicyRejectsReplacedOrMissingManifest(t *testing.T) {
for _, mode := range []string{"missing", "other_signer", "changed_payload"} {
t.Run(mode, func(t *testing.T) {
f := installBetaPolicyFixture(t, nil)
cache := filepath.Join(f.manager.root, "cache", f.installed.RepoID+".json")
if mode == "missing" {
if err := os.Remove(cache); err != nil {
t.Fatal(err)
}
} else {
raw, err := os.ReadFile(cache)
if err != nil {
t.Fatal(err)
}
var envelope ManifestEnvelope
if err := json.Unmarshal(raw, &envelope); err != nil {
t.Fatal(err)
}
if mode == "other_signer" {
_, other, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatal(err)
}
envelope.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(other, envelope.Payload))
} else {
envelope.Payload = json.RawMessage(`{"schema_version":1}`)
}
raw, err = json.Marshal(envelope)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(cache, raw, 0600); err != nil {
t.Fatal(err)
}
}
// Even an earlier valid in-memory manifest cannot bypass a fresh
// check against the recorded official trust identity.
if policy, err := f.manager.RuntimePolicy(f.driver); err == nil || policy != nil {
t.Fatalf("%s manifest yielded runtime policy %+v, %v", mode, policy, err)
}
})
}
}
2 changes: 1 addition & 1 deletion go/internal/driverrepo/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ func TestOfficialBetaChannelInstallsOneSignedDriver(t *testing.T) {
unknown := New(configured, dir, store)
unknown.betaRepo = manager.betaRepo
unknown.betaRepo.ID = "other-beta-source"
if policy, err := unknown.RuntimePolicy(driverCfg); err != nil || policy != nil {
if policy, err := unknown.RuntimePolicy(driverCfg); err == nil || policy != nil {
t.Fatalf("unknown installed repository gained a policy: %+v, %v", policy, err)
}
})
Expand Down
39 changes: 30 additions & 9 deletions go/internal/driverrepo/sourceful.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,10 +584,18 @@ func (m *Manager) RuntimePolicy(cfg config.Driver) (*drivers.RuntimePolicy, erro
return nil, nil
}
var repo *config.DriverRepositorySource
for i := range m.cfg.Repositories {
if m.cfg.Repositories[i].ID == installed.RepoID {
repo = &m.cfg.Repositories[i]
break
if installed.FTWSigned && installed.RepoURL == "https://github.com/srcfl/device-drivers" {
// The installer recorded this trust identity before config aliases
// could change. Keep the historical ID only to locate its cache.
pinned := m.betaRepo
pinned.ID = installed.RepoID
repo = &pinned
} else {
for i := range m.cfg.Repositories {
if m.cfg.Repositories[i].ID == installed.RepoID {
repo = &m.cfg.Repositories[i]
break
}
}
}
// InstallChannel uses this pinned trust source without adding it to the
Expand All @@ -596,10 +604,7 @@ func (m *Manager) RuntimePolicy(cfg config.Driver) (*drivers.RuntimePolicy, erro
repo = &m.betaRepo
}
if repo == nil {
if cfg.Control != nil && cfg.Control.Enabled {
return nil, errors.New("control opt-in requires a configured Device Support trust root")
}
return nil, nil
return nil, errors.New("active managed driver has no trusted repository source")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Invoke autonomous default before rejecting missing trust

When an active control-capable managed driver’s repository is removed from configuration, this new error propagates through drivers.Registry.add before the driver is constructed, while the startup loop in go/cmd/ftw/main.go only logs the failure. Consequently, driver_default_mode is never invoked and hardware may remain in the forced mode issued before the restart; reject unrestricted operation only after arranging a default-mode transition or safe fallback.

AGENTS.md reference: AGENTS.md:L36-L36

Useful? React with 👍 / 👎.

}
if repositoryFormat(*repo) != config.DriverRepositoryFormatSourcefulIndexV1 {
return m.directManifestRuntimePolicy(cfg, *repo, installed)
Expand Down Expand Up @@ -698,7 +703,23 @@ func (m *Manager) directManifestRuntimePolicy(
if cfg.Control != nil && cfg.Control.Enabled {
return nil, errors.New("control opt-in requires a signed Device Support control package")
}
manifest, err := m.manifestFor(repo)
var manifest Manifest
var err error
if installed.FTWSigned && installed.RepoURL == "https://github.com/srcfl/device-drivers" {
// The in-memory cache is keyed only by a configurable repository ID.
// Another source may now own that ID. Reverify the saved envelope
// against the pinned official key before applying its runtime policy.
var raw []byte
raw, err = readLimitedFile(filepath.Join(m.root, "cache", safeSegment(installed.RepoID)+".json"), maxManifestBytes)
if err == nil {
manifest, _, err = verifyManifest(raw, repo)
}
if err == nil {
err = validateManifest(manifest, repo.AllowInsecure)
}
} else {
manifest, err = m.manifestFor(repo)
}
if err != nil {
if installed.RepoURL != "https://github.com/srcfl/device-drivers" {
return nil, nil
Expand Down