diff --git a/.changeset/preserve-installed-driver-format.md b/.changeset/preserve-installed-driver-format.md new file mode 100644 index 000000000..cb29828c6 --- /dev/null +++ b/.changeset/preserve-installed-driver-format.md @@ -0,0 +1,10 @@ +--- +"ftw": patch +--- + +Keep each managed driver's metadata format through restarts and rollback. A +Device Support package now requires its verified runtime policy even when +control is not selected, its repository is removed, or its envelope is missing. +Verified legacy installs retain their normal autonomous default. Older installs +with no recorded format need matching verified metadata; restore their repository +or reinstall them if that metadata is unavailable. diff --git a/go/internal/driverrepo/installed_policy_format_test.go b/go/internal/driverrepo/installed_policy_format_test.go new file mode 100644 index 000000000..6c3ae5577 --- /dev/null +++ b/go/internal/driverrepo/installed_policy_format_test.go @@ -0,0 +1,488 @@ +package driverrepo + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "crypto/sha256" + "database/sql" + "encoding/base64" + "encoding/hex" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/srcfl/ftw/go/internal/config" + "github.com/srcfl/ftw/go/internal/drivers" + "github.com/srcfl/ftw/go/internal/state" + "github.com/srcfl/ftw/go/internal/telemetry" +) + +type installedPolicyFixture struct { + root string + statePath string + store *state.Store + repo config.DriverRepositorySource + installed state.DriverRepoInstall + driver config.Driver +} + +type installedPolicyModbus struct { + value uint16 + writes int +} + +func (m *installedPolicyModbus) Read(uint16, uint16, int32) ([]uint16, error) { + return []uint16{m.value}, nil +} + +func (m *installedPolicyModbus) WriteSingle(_ uint16, value uint16) error { + m.value = value + m.writes++ + return nil +} + +func (m *installedPolicyModbus) WriteMulti(_ uint16, values []uint16) error { + if len(values) > 0 { + m.value = values[0] + } + m.writes++ + return nil +} + +func (*installedPolicyModbus) Close() error { return nil } + +func makeSourcefulDefaultObservable(t *testing.T, fixture *sourcefulFixture, serverURL string) { + t.Helper() + fixture.artifact = []byte(`DRIVER = { + id = "sdm630", + name = "Eastron SDM630 meter", + version = "1.1.1", + host_api_min = 2, + host_api_max = 2, + protocols = { "modbus" }, + capabilities = { "meter" }, + read_only = false, +} +function driver_init(config) end +function driver_poll() return 1000 end +function driver_command_v2(command) + return {status="applied", code="ok", device_state="controlled", evidence={"write_ack", "readback"}} +end +function driver_default_mode_v2(context) + local write_err = host.modbus_write(10, 0) + if write_err then error(write_err) end + local value, read_err = host.modbus_read(10, 1, "holding") + if read_err then error(read_err) end + return {status="defaulted", code="default_restored", device_state="default", evidence={"write_ack", "readback"}} +end +`) + artifactSum := sha256.Sum256(fixture.artifact) + artifactHash := hex.EncodeToString(artifactSum[:]) + artifactFilename := "sdm630-1.1.1-ftw-core-ftw.lua51.source-" + artifactHash + ".lua" + fixture.artifactPath = "/" + artifactFilename + + var packageEnvelope sourcefulSignedEnvelope + if err := json.Unmarshal(fixture.packageEnvelope, &packageEnvelope); err != nil { + t.Fatal(err) + } + var pkg sourcefulPackage + if err := json.Unmarshal(packageEnvelope.Payload, &pkg); err != nil { + t.Fatal(err) + } + pkg.Artifacts[0].Filename = artifactFilename + pkg.Artifacts[0].URL = serverURL + fixture.artifactPath + pkg.Artifacts[0].SHA256 = artifactHash + pkg.Artifacts[0].SizeBytes = int64(len(fixture.artifact)) + pkg.Provenance.Materials[0].SHA256 = artifactHash + fixture.packageEnvelope = signSourcefulFixture( + t, fixture.private, sourcefulPackageEnvelopeSchema, sourcefulPackagePayloadType, pkg, + ) + packageSum := sha256.Sum256(fixture.packageEnvelope) + + var indexEnvelope sourcefulSignedEnvelope + if err := json.Unmarshal(fixture.indexEnvelope, &indexEnvelope); err != nil { + t.Fatal(err) + } + var index sourcefulDriverIndex + if err := json.Unmarshal(indexEnvelope.Payload, &index); err != nil { + t.Fatal(err) + } + index.Packages[0].EnvelopeSHA256 = hex.EncodeToString(packageSum[:]) + fixture.indexEnvelope = signSourcefulFixture( + t, fixture.private, sourcefulIndexEnvelopeSchema, sourcefulIndexPayloadType, index, + ) +} + +func newInstalledSourcefulControlFixture(t *testing.T) *installedPolicyFixture { + t.Helper() + public, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + fixture := &sourcefulFixture{private: private} + server := httptest.NewServer(http.HandlerFunc(fixture.serveHTTP)) + t.Cleanup(server.Close) + fixture.build(t, server.URL, false, true) + makeSourcefulDefaultObservable(t, fixture, server.URL) + + root := t.TempDir() + statePath := filepath.Join(root, "state.db") + store, err := state.Open(statePath) + if err != nil { + t.Fatal(err) + } + f := &installedPolicyFixture{ + root: root, + statePath: statePath, + store: store, + repo: config.DriverRepositorySource{ + ID: "sourceful", Format: config.DriverRepositoryFormatSourcefulIndexV1, + ManifestURL: server.URL + "/index.json", Enabled: true, AllowInsecure: true, + TrustedKeys: map[string]string{"sourceful-test-1": base64.StdEncoding.EncodeToString(public)}, + }, + } + t.Cleanup(func() { + if f.store != nil { + _ = f.store.Close() + } + }) + + manager := f.manager(t, []config.DriverRepositorySource{f.repo}, "1.7.0") + if err := manager.Refresh(context.Background(), f.repo.ID); err != nil { + t.Fatal(err) + } + catalog, err := manager.Catalog() + if err != nil || len(catalog) != 1 { + t.Fatalf("control v2 catalog = %+v, %v", catalog, err) + } + f.installed, err = manager.Install(context.Background(), f.repo.ID, "sdm630", "1.1.1") + if err != nil { + t.Fatal(err) + } + if f.installed.RepositoryFormat != config.DriverRepositoryFormatSourcefulIndexV1 { + t.Fatalf("installed repository format = %q", f.installed.RepositoryFormat) + } + f.driver = config.Driver{ + Name: "sdm630", Lua: filepath.Join(manager.ActiveDir(), "sdm630.lua"), + Modbus: &config.ModbusConfig{Host: "device", Port: 502}, + } + return f +} + +func (f *installedPolicyFixture) manager( + t *testing.T, + repositories []config.DriverRepositorySource, + hostVersion string, +) *Manager { + t.Helper() + if f.store == nil { + t.Fatal("manager requested while state store is closed") + } + cfg := &config.DeviceRepository{Enabled: true, Repositories: repositories} + return NewWithHostVersion(cfg, f.root, f.store, hostVersion) +} + +func (f *installedPolicyFixture) closeStore(t *testing.T) { + t.Helper() + if f.store == nil { + return + } + if err := f.store.Close(); err != nil { + t.Fatal(err) + } + f.store = nil +} + +func (f *installedPolicyFixture) reopenStore(t *testing.T) { + t.Helper() + if f.store != nil { + t.Fatal("state store is already open") + } + store, err := state.Open(f.statePath) + if err != nil { + t.Fatal(err) + } + f.store = store +} + +func (f *installedPolicyFixture) setHistoricalUnknownFormat(t *testing.T) { + t.Helper() + f.closeStore(t) + db, err := sql.Open("sqlite", f.statePath) + if err != nil { + t.Fatal(err) + } + result, execErr := db.Exec(`UPDATE driver_repo_installs SET repository_format = '' WHERE id = ?`, f.installed.ID) + closeErr := db.Close() + if execErr != nil { + t.Fatal(execErr) + } + if closeErr != nil { + t.Fatal(closeErr) + } + if rows, err := result.RowsAffected(); err != nil || rows != 1 { + t.Fatalf("rows changed = %d, %v", rows, err) + } + f.reopenStore(t) +} + +func (f *installedPolicyFixture) installRow(t *testing.T) state.DriverRepoInstall { + t.Helper() + installed, err := f.store.ActiveDriverRepoInstall(f.installed.LogicalPath) + if err != nil { + t.Fatal(err) + } + return installed +} + +func (f *installedPolicyFixture) invalidateLua(t *testing.T) { + t.Helper() + if err := os.WriteFile(f.installed.InstalledPath, []byte(`this is not Lua`), 0o600); err != nil { + t.Fatal(err) + } +} + +func assertRegistryRejectsBeforeLua(t *testing.T, manager *Manager, driver config.Driver) { + t.Helper() + registry := drivers.NewRegistry(telemetry.NewStore()) + registry.RuntimePolicyResolver = manager.RuntimePolicy + modbus := &installedPolicyModbus{} + registry.ModbusFactory = func(string, *config.ModbusConfig) (drivers.ModbusCap, error) { + return modbus, nil + } + defer registry.ShutdownAll() + err := registry.Add(context.Background(), driver) + if err == nil { + t.Fatal("Registry.Add accepted invalid installed policy state") + } + if !strings.Contains(err.Error(), "runtime policy:") { + t.Fatalf("Registry.Add reached Lua instead of stopping at runtime policy: %v", err) + } +} + +func TestInstalledSourcefulControlPolicyRunsDefaultWithoutSiteOptIn(t *testing.T) { + f := newInstalledSourcefulControlFixture(t) + manager := f.manager(t, []config.DriverRepositorySource{f.repo}, "1.7.0") + policy, err := manager.RuntimePolicy(f.driver) + if err != nil { + t.Fatal(err) + } + if policy == nil || !policy.IsControlV2() { + t.Fatalf("runtime policy = %+v", policy) + } + if policy.SiteEnabled { + t.Error("control v2 policy enabled site control without cfg.Control") + } + if policy.DefaultMode != "driver_default_mode_v2" || !policy.Permissions["modbus.write"] { + t.Fatalf("control v2 default policy = %+v", policy) + } + + registry := drivers.NewRegistry(telemetry.NewStore()) + registry.RuntimePolicyResolver = manager.RuntimePolicy + modbus := &installedPolicyModbus{} + registry.ModbusFactory = func(string, *config.ModbusConfig) (drivers.ModbusCap, error) { + return modbus, nil + } + defer registry.ShutdownAll() + if err := registry.Add(context.Background(), f.driver); err != nil { + t.Fatalf("add unselected control v2 driver and run startup default: %v", err) + } + if err := registry.SendDefault(context.Background(), f.driver.Name); err != nil { + t.Fatalf("run verified control v2 default: %v", err) + } + if modbus.writes != 2 || modbus.value != 0 { + t.Fatalf("default writes = %d, register = %d", modbus.writes, modbus.value) + } +} + +func TestInstalledSourcefulControlPolicyRejectsTrustChangesBeforeLua(t *testing.T) { + tests := []struct { + name string + repos func(*installedPolicyFixture) []config.DriverRepositorySource + host string + mutateDisk func(*testing.T, *installedPolicyFixture) + }{ + { + name: "repository removed", + repos: func(*installedPolicyFixture) []config.DriverRepositorySource { return nil }, + host: "1.7.0", + }, + { + name: "repository changed to direct manifest", + repos: func(f *installedPolicyFixture) []config.DriverRepositorySource { + repo := f.repo + repo.Format = config.DriverRepositoryFormatFTWManifestV1 + return []config.DriverRepositorySource{repo} + }, + host: "1.7.0", + }, + { + name: "repository alias now points to another source", + repos: func(f *installedPolicyFixture) []config.DriverRepositorySource { + repo := f.repo + repo.ManifestURL = "https://other.invalid/index.json" + return []config.DriverRepositorySource{repo} + }, + host: "1.7.0", + }, + { + name: "signed package envelope missing", + repos: func(f *installedPolicyFixture) []config.DriverRepositorySource { + return []config.DriverRepositorySource{f.repo} + }, + host: "1.7.0", + mutateDisk: func(t *testing.T, f *installedPolicyFixture) { + if err := os.Remove(filepath.Join(filepath.Dir(f.installed.InstalledPath), sourcefulInstalledPackageEnvelope)); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "signed package envelope corrupt", + repos: func(f *installedPolicyFixture) []config.DriverRepositorySource { + return []config.DriverRepositorySource{f.repo} + }, + host: "1.7.0", + mutateDisk: func(t *testing.T, f *installedPolicyFixture) { + path := filepath.Join(filepath.Dir(f.installed.InstalledPath), sourcefulInstalledPackageEnvelope) + if err := os.WriteFile(path, []byte(`{"broken":`), 0o600); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "host version incompatible", + repos: func(f *installedPolicyFixture) []config.DriverRepositorySource { + return []config.DriverRepositorySource{f.repo} + }, + host: "1.6.9", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := newInstalledSourcefulControlFixture(t) + if tt.mutateDisk != nil { + tt.mutateDisk(t, f) + } + manager := f.manager(t, tt.repos(f), tt.host) + f.invalidateLua(t) + assertRegistryRejectsBeforeLua(t, manager, f.driver) + }) + } +} + +func TestHistoricalInstalledSourcefulFormatRequiresMetadata(t *testing.T) { + f := newInstalledSourcefulControlFixture(t) + f.setHistoricalUnknownFormat(t) + if got := f.installRow(t).RepositoryFormat; got != "" { + t.Fatalf("historical repository format = %q", got) + } + if err := os.Remove(filepath.Join(filepath.Dir(f.installed.InstalledPath), sourcefulInstalledPackageEnvelope)); err != nil { + t.Fatal(err) + } + manager := f.manager(t, []config.DriverRepositorySource{f.repo}, "1.7.0") + f.invalidateLua(t) + assertRegistryRejectsBeforeLua(t, manager, f.driver) + if got := f.installRow(t).RepositoryFormat; got != "" { + t.Fatalf("unverified historical repository format was backfilled as %q", got) + } +} + +func TestHistoricalInstalledSourcefulFormatDoesNotInferLegacyAfterSourceRemoval(t *testing.T) { + for _, keepEnvelope := range []bool{true, false} { + t.Run(map[bool]string{true: "envelope retained", false: "envelope missing"}[keepEnvelope], func(t *testing.T) { + f := newInstalledSourcefulControlFixture(t) + f.setHistoricalUnknownFormat(t) + if !keepEnvelope { + if err := os.Remove(filepath.Join(filepath.Dir(f.installed.InstalledPath), sourcefulInstalledPackageEnvelope)); err != nil { + t.Fatal(err) + } + } + manager := f.manager(t, nil, "1.7.0") + f.invalidateLua(t) + assertRegistryRejectsBeforeLua(t, manager, f.driver) + if got := f.installRow(t).RepositoryFormat; got != "" { + t.Fatalf("unverified historical install became %q", got) + } + }) + } +} + +func TestHistoricalInstalledSourcefulFormatBackfillSurvivesRestart(t *testing.T) { + f := newInstalledSourcefulControlFixture(t) + f.setHistoricalUnknownFormat(t) + manager := f.manager(t, []config.DriverRepositorySource{f.repo}, "1.7.0") + + registry := drivers.NewRegistry(telemetry.NewStore()) + registry.RuntimePolicyResolver = manager.RuntimePolicy + registry.ModbusFactory = func(string, *config.ModbusConfig) (drivers.ModbusCap, error) { + return &installedPolicyModbus{}, nil + } + if err := registry.Add(context.Background(), f.driver); err != nil { + registry.ShutdownAll() + t.Fatalf("verify and backfill historical Sourceful install: %v", err) + } + registry.ShutdownAll() + if got := f.installRow(t).RepositoryFormat; got != config.DriverRepositoryFormatSourcefulIndexV1 { + t.Fatalf("backfilled repository format = %q", got) + } + + f.closeStore(t) + f.reopenStore(t) + if got := f.installRow(t).RepositoryFormat; got != config.DriverRepositoryFormatSourcefulIndexV1 { + t.Fatalf("repository format after restart = %q", got) + } + if err := os.Remove(filepath.Join(filepath.Dir(f.installed.InstalledPath), sourcefulInstalledPackageEnvelope)); err != nil { + t.Fatal(err) + } + restarted := f.manager(t, []config.DriverRepositorySource{f.repo}, "1.7.0") + f.invalidateLua(t) + assertRegistryRejectsBeforeLua(t, restarted, f.driver) + if got := f.installRow(t).RepositoryFormat; got != config.DriverRepositoryFormatSourcefulIndexV1 { + t.Fatalf("repository format after missing-envelope rejection = %q", got) + } +} + +func TestInactiveInstalledSourcefulControlPolicyStopsBeforeLua(t *testing.T) { + f := newInstalledSourcefulControlFixture(t) + manager := f.manager(t, []config.DriverRepositorySource{f.repo}, "1.7.0") + if err := f.store.DeactivateDriverRepoInstall(f.installed.LogicalPath); err != nil { + t.Fatal(err) + } + driver := f.driver + driver.Lua = f.installed.InstalledPath + f.invalidateLua(t) + assertRegistryRejectsBeforeLua(t, manager, driver) +} + +func TestInstallerCannotReclassifyRetainedSourcefulArtifact(t *testing.T) { + f := newInstalledSourcefulControlFixture(t) + manager := f.manager(t, []config.DriverRepositorySource{f.repo}, "1.7.0") + repo, manifest, entry, err := manager.find(f.repo.ID, "sdm630", "1.1.1") + if err != nil { + t.Fatal(err) + } + repo.Format = config.DriverRepositoryFormatFTWManifestV1 + envelopePath := filepath.Join(filepath.Dir(f.installed.InstalledPath), sourcefulInstalledPackageEnvelope) + sentinel := []byte("the retained envelope must not be rewritten") + if err := os.WriteFile(envelopePath, sentinel, 0o600); err != nil { + t.Fatal(err) + } + if _, err := manager.installResolved(context.Background(), repo, manifest, entry); err == nil || + !strings.Contains(err.Error(), "metadata format cannot change") { + t.Fatalf("reclassify retained Sourceful artifact error = %v", err) + } + got, err := os.ReadFile(envelopePath) + if err != nil { + t.Fatal(err) + } + if string(got) != string(sentinel) { + t.Fatalf("retained package envelope changed to %q", got) + } +} diff --git a/go/internal/driverrepo/legacy_format_upgrade_test.go b/go/internal/driverrepo/legacy_format_upgrade_test.go new file mode 100644 index 000000000..015488059 --- /dev/null +++ b/go/internal/driverrepo/legacy_format_upgrade_test.go @@ -0,0 +1,310 @@ +package driverrepo + +import ( + "context" + "crypto/ed25519" + "crypto/rand" + "database/sql" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/srcfl/ftw/go/internal/config" + "github.com/srcfl/ftw/go/internal/state" + _ "modernc.org/sqlite" +) + +type legacyDirectFixture struct { + dir string + dbPath string + repo config.DriverRepositorySource + installed state.DriverRepoInstall + signed *signedFixture +} + +func installLegacyDirectFixture(t *testing.T) legacyDirectFixture { + t.Helper() + public, private, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + signed := &signedFixture{private: private} + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/manifest.json": + _, _ = w.Write(signed.envelope(t)) + case "/demo.lua": + signed.mu.Lock() + defer signed.mu.Unlock() + _, _ = w.Write(signed.driver) + default: + http.NotFound(w, r) + } + })) + t.Cleanup(server.Close) + signed.setVersion(server.URL, "1.0.0") + signed.mu.Lock() + signed.manifest.Drivers[0].ReadOnly = true + signed.manifest.Drivers[0].Metadata.ReadOnly = true + signed.manifest.Drivers[0].Permissions = []string{"http.get"} + signed.mu.Unlock() + + dir := t.TempDir() + dbPath := filepath.Join(dir, "state.db") + store, err := state.Open(dbPath) + if err != nil { + t.Fatal(err) + } + repo := config.DriverRepositorySource{ + ID: "legacy-direct", ManifestURL: server.URL + "/manifest.json", Enabled: true, AllowInsecure: true, + TrustedKeys: map[string]string{"test": base64.StdEncoding.EncodeToString(public)}, + } + manager := New(&config.DeviceRepository{Enabled: true, Repositories: []config.DriverRepositorySource{repo}}, dir, store) + if err := manager.Refresh(context.Background(), repo.ID); err != nil { + t.Fatal(err) + } + installed, err := manager.Install(context.Background(), repo.ID, "demo", "1.0.0") + if err != nil { + t.Fatal(err) + } + if installed.RepositoryFormat != config.DriverRepositoryFormatFTWManifestV1 { + t.Fatalf("new direct install format = %q", installed.RepositoryFormat) + } + if err := store.Close(); err != nil { + t.Fatal(err) + } + return legacyDirectFixture{dir: dir, dbPath: dbPath, repo: repo, installed: installed, signed: signed} +} + +func (f legacyDirectFixture) legacyDriver() config.Driver { + return config.Driver{Name: "demo", Lua: filepath.Join(f.dir, "driver-repository", "active", "demo.lua")} +} + +func (f legacyDirectFixture) clearFormat(t *testing.T, mutate func(*sql.DB)) { + t.Helper() + db, err := sql.Open("sqlite", f.dbPath) + if err != nil { + t.Fatal(err) + } + if mutate != nil { + mutate(db) + } + if _, err := db.Exec(`UPDATE driver_repo_installs SET repository_format = '' WHERE installed_path = ?`, f.installed.InstalledPath); err != nil { + _ = db.Close() + t.Fatal(err) + } + if err := db.Close(); err != nil { + t.Fatal(err) + } +} + +func (f legacyDirectFixture) reopen(t *testing.T) (*state.Store, *Manager) { + t.Helper() + store, err := state.Open(f.dbPath) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = store.Close() }) + manager := New(&config.DeviceRepository{Enabled: true, Repositories: []config.DriverRepositorySource{f.repo}}, f.dir, store) + if _, err := manager.manifestFor(f.repo); err != nil { + t.Fatal(err) + } + return store, manager +} + +func assertUnrecordedFormat(t *testing.T, store *state.Store, path string) { + t.Helper() + installed, err := store.DriverRepoInstallByPath(path) + if err != nil { + t.Fatal(err) + } + if installed.RepositoryFormat != "" { + t.Fatalf("unverified legacy row recorded format %q", installed.RepositoryFormat) + } +} + +func TestLegacyDirectManifestFormatBackfillSurvivesRepositoryRemoval(t *testing.T) { + f := installLegacyDirectFixture(t) + f.clearFormat(t, nil) + store, manager := f.reopen(t) + + policy, err := manager.RuntimePolicy(f.legacyDriver()) + if err != nil || policy == nil || !policy.IsReadOnly() { + t.Fatalf("verified legacy direct policy = %+v, %v", policy, err) + } + installed, err := store.DriverRepoInstallByPath(f.installed.InstalledPath) + if err != nil || installed.RepositoryFormat != config.DriverRepositoryFormatFTWManifestV1 { + t.Fatalf("backfilled install = %+v, %v", installed, err) + } + if err := store.Close(); err != nil { + t.Fatal(err) + } + + withoutSource, err := state.Open(f.dbPath) + if err != nil { + t.Fatal(err) + } + defer withoutSource.Close() + policy, err = New(nil, f.dir, withoutSource).RuntimePolicy(f.legacyDriver()) + if err != nil || policy != nil { + t.Fatalf("recorded legacy driver after source removal = %+v, %v", policy, err) + } +} + +func TestLegacyDirectManifestFormatDoesNotTrustUnsignedCache(t *testing.T) { + f := installLegacyDirectFixture(t) + // New explicit installs may still use this setting. It must not turn an + // unknown historical row into a trusted direct-manifest install. + f.repo.AllowUnsigned = true + f.clearFormat(t, nil) + store, manager := f.reopen(t) + raw := f.signed.envelope(t) + var envelope ManifestEnvelope + if err := json.Unmarshal(raw, &envelope); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(manager.root, "cache", f.repo.ID+".json"), envelope.Payload, 0o600); err != nil { + t.Fatal(err) + } + if policy, err := manager.RuntimePolicy(f.legacyDriver()); err == nil || policy != nil || !strings.Contains(err.Error(), "verify older installed driver format") { + t.Fatalf("unsigned cached manifest backfilled legacy row: %+v, %v", policy, err) + } + assertUnrecordedFormat(t, store, f.installed.InstalledPath) +} + +func TestLegacyDirectManifestFormatDoesNotInferUnknownRows(t *testing.T) { + for _, tc := range []struct { + name string + want string + record func(t *testing.T, f legacyDirectFixture) + cache func(t *testing.T, f legacyDirectFixture, manager *Manager) + }{ + { + name: "different_driver", + want: "absent from its verified manifest", + record: func(t *testing.T, f legacyDirectFixture) { + f.clearFormat(t, func(db *sql.DB) { + if _, err := db.Exec(`UPDATE driver_repo_installs SET driver_id = 'other' WHERE installed_path = ?`, f.installed.InstalledPath); err != nil { + t.Fatal(err) + } + }) + }, + }, + { + name: "different_source", + want: "source does not match", + record: func(t *testing.T, f legacyDirectFixture) { + f.clearFormat(t, func(db *sql.DB) { + if _, err := db.Exec(`UPDATE driver_repo_installs SET repo_url = 'https://other.invalid/manifest.json' WHERE installed_path = ?`, f.installed.InstalledPath); err != nil { + t.Fatal(err) + } + }) + }, + }, + { + name: "cache_missing", + want: "verify older installed driver format", + record: func(t *testing.T, f legacyDirectFixture) { f.clearFormat(t, nil) }, + cache: func(t *testing.T, f legacyDirectFixture, manager *Manager) { + if err := os.Remove(filepath.Join(manager.root, "cache", f.repo.ID+".json")); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "malformed_cache", + want: "verify older installed driver format", + record: func(t *testing.T, f legacyDirectFixture) { f.clearFormat(t, nil) }, + cache: func(t *testing.T, f legacyDirectFixture, manager *Manager) { + if err := os.WriteFile(filepath.Join(manager.root, "cache", f.repo.ID+".json"), []byte("not a manifest"), 0o600); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "other_signer", + want: "verify older installed driver format", + record: func(t *testing.T, f legacyDirectFixture) { f.clearFormat(t, nil) }, + cache: func(t *testing.T, f legacyDirectFixture, manager *Manager) { + raw, err := os.ReadFile(filepath.Join(manager.root, "cache", f.repo.ID+".json")) + if err != nil { + t.Fatal(err) + } + var envelope ManifestEnvelope + if err := json.Unmarshal(raw, &envelope); err != nil { + t.Fatal(err) + } + _, other, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + envelope.Signature = base64.StdEncoding.EncodeToString(ed25519.Sign(other, envelope.Payload)) + raw, err = json.Marshal(envelope) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(manager.root, "cache", f.repo.ID+".json"), raw, 0o600); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "sourceful_package_entry", + want: "not a legacy direct-manifest artifact", + record: func(t *testing.T, f legacyDirectFixture) { f.clearFormat(t, nil) }, + cache: func(t *testing.T, f legacyDirectFixture, manager *Manager) { + f.signed.mu.Lock() + f.signed.manifest.Drivers[0].PackageID = "com.sourceful.driver.demo" + f.signed.mu.Unlock() + if err := os.WriteFile(filepath.Join(manager.root, "cache", f.repo.ID+".json"), f.signed.envelope(t), 0o600); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "v2_abi_entry", + want: "not a legacy direct-manifest artifact", + record: func(t *testing.T, f legacyDirectFixture) { f.clearFormat(t, nil) }, + cache: func(t *testing.T, f legacyDirectFixture, manager *Manager) { + f.signed.mu.Lock() + f.signed.manifest.Drivers[0].RuntimeABI = sourcefulFTWABIV2 + f.signed.manifest.Drivers[0].HostAPIProfile = sourcefulFTWHostAPIProfileV2 + f.signed.mu.Unlock() + if err := os.WriteFile(filepath.Join(manager.root, "cache", f.repo.ID+".json"), f.signed.envelope(t), 0o600); err != nil { + t.Fatal(err) + } + }, + }, + { + name: "memory_cache_cannot_override_bad_disk_cache", + want: "verify older installed driver format", + record: func(t *testing.T, f legacyDirectFixture) { f.clearFormat(t, nil) }, + cache: func(t *testing.T, f legacyDirectFixture, manager *Manager) { + if _, ok := manager.manifests[f.repo.ID]; !ok { + t.Fatal("test did not retain a positive in-memory manifest") + } + if err := os.WriteFile(filepath.Join(manager.root, "cache", f.repo.ID+".json"), []byte("bad saved bytes"), 0o600); err != nil { + t.Fatal(err) + } + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + f := installLegacyDirectFixture(t) + tc.record(t, f) + store, manager := f.reopen(t) + if tc.cache != nil { + tc.cache(t, f, manager) + } + if policy, err := manager.RuntimePolicy(f.legacyDriver()); err == nil || policy != nil || !strings.Contains(err.Error(), tc.want) { + t.Fatalf("unknown legacy row policy = %+v, %v; want %q", policy, err, tc.want) + } + assertUnrecordedFormat(t, store, f.installed.InstalledPath) + }) + } +} diff --git a/go/internal/driverrepo/manager.go b/go/internal/driverrepo/manager.go index 98036ebcc..feec2e01a 100644 --- a/go/internal/driverrepo/manager.go +++ b/go/internal/driverrepo/manager.go @@ -7,6 +7,7 @@ import ( "context" "crypto/ed25519" "crypto/sha256" + "database/sql" "encoding/base64" "encoding/hex" "encoding/json" @@ -586,6 +587,13 @@ func (m *Manager) installResolved(ctx context.Context, repo config.DriverReposit return state.DriverRepoInstall{}, fmt.Errorf("driver size %d, want %d", len(raw), entry.SizeBytes) } installPath := filepath.Join(m.root, "installed", safeSegment(repo.ID), safeSegment(entry.ID), entry.Version, strings.ToLower(entry.SHA256), entry.Filename) + retained, err := m.store.DriverRepoInstallByPath(installPath) + if err != nil && !errors.Is(err, sql.ErrNoRows) { + return state.DriverRepoInstall{}, err + } + if err == nil && retained.RepositoryFormat != "" && retained.RepositoryFormat != repositoryFormat(repo) { + return state.DriverRepoInstall{}, errors.New("retained driver metadata format cannot change") + } if err := atomicWrite(installPath, raw, 0o600); err != nil { return state.DriverRepoInstall{}, err } @@ -608,7 +616,8 @@ func (m *Manager) installResolved(ctx context.Context, repo config.DriverReposit logical := filepath.ToSlash(entry.Path) installed := state.DriverRepoInstall{ RepoURL: manifest.Repository, RepoID: repo.ID, DriverID: entry.ID, - LogicalPath: logical, Version: entry.Version, SHA256: strings.ToLower(entry.SHA256), InstalledPath: installPath, + RepositoryFormat: repositoryFormat(repo), + LogicalPath: logical, Version: entry.Version, SHA256: strings.ToLower(entry.SHA256), InstalledPath: installPath, // Recorded here and nowhere else, because here is where it happened. FTWSigned: ftwSigned(repo), } diff --git a/go/internal/driverrepo/sourceful.go b/go/internal/driverrepo/sourceful.go index c002097e0..7b5c268d3 100644 --- a/go/internal/driverrepo/sourceful.go +++ b/go/internal/driverrepo/sourceful.go @@ -578,10 +578,7 @@ func (m *Manager) RuntimePolicy(cfg config.Driver) (*drivers.RuntimePolicy, erro return nil, fmt.Errorf("resolve managed driver activation: %w", err) } if !installed.Active { - if cfg.Control != nil && cfg.Control.Enabled { - return nil, errors.New("control opt-in requires the active managed artifact") - } - return nil, nil + return nil, errors.New("managed driver runtime requires the active artifact") } var repo *config.DriverRepositorySource if installed.FTWSigned && installed.RepoURL == "https://github.com/srcfl/device-drivers" { @@ -603,23 +600,50 @@ func (m *Manager) RuntimePolicy(cfg config.Driver) (*drivers.RuntimePolicy, erro if repo == nil && m.betaRepo.ID != "" && installed.RepoID == m.betaRepo.ID { repo = &m.betaRepo } + packagePath := filepath.Join(filepath.Dir(resolved), sourcefulInstalledPackageEnvelope) + _, packageErr := os.Lstat(packagePath) + if packageErr != nil && !errors.Is(packageErr, os.ErrNotExist) { + return nil, fmt.Errorf("inspect installed signed package envelope: %w", packageErr) + } + // An envelope can rule out legacy operation even on an old install. Its + // presence never grants trust; only signed metadata can do that. + if packageErr == nil && (installed.RepositoryFormat == config.DriverRepositoryFormatFTWManifestV1 || + (repo != nil && repositoryFormat(*repo) != config.DriverRepositoryFormatSourcefulIndexV1)) { + return nil, errors.New("installed signed package requires its Device Support repository format") + } + switch installed.RepositoryFormat { + case "", config.DriverRepositoryFormatFTWManifestV1, config.DriverRepositoryFormatSourcefulIndexV1: + default: + return nil, errors.New("installed driver metadata format is unsupported") + } if repo == nil { if cfg.Control != nil && cfg.Control.Enabled { return nil, errors.New("control opt-in requires a configured Device Support trust root") } - // Preserve legacy v1 startup and its autonomous default after a source - // is removed. Official provenance already selected pinned trust above. + if installed.RepositoryFormat != config.DriverRepositoryFormatFTWManifestV1 || packageErr == nil { + return nil, errors.New("installed driver requires its repository to verify runtime metadata; restore the repository or reinstall the driver") + } + // Only a recorded direct-manifest install can retain legacy startup + // after its source is removed. An absent opt-in does not identify v1. return nil, nil } + if installed.RepositoryFormat != "" && installed.RepositoryFormat != repositoryFormat(*repo) { + return nil, errors.New("configured repository format does not match the installed driver") + } if repositoryFormat(*repo) != config.DriverRepositoryFormatSourcefulIndexV1 { + if installed.RepositoryFormat == "" { + if err := m.recordDirectManifestFormat(*repo, installed); err != nil { + return nil, err + } + } return m.directManifestRuntimePolicy(cfg, *repo, installed) } - packageRaw, err := readLimitedFile(filepath.Join(filepath.Dir(resolved), sourcefulInstalledPackageEnvelope), maxManifestBytes) + if installed.RepoURL != repo.ManifestURL { + return nil, errors.New("configured Device Support repository does not match the installed source") + } + packageRaw, err := readLimitedFile(packagePath, maxManifestBytes) if err != nil { - if cfg.Control != nil && cfg.Control.Enabled { - return nil, fmt.Errorf("read installed signed package envelope: %w", err) - } - return nil, nil + return nil, fmt.Errorf("read installed signed package envelope: %w", err) } payloadRaw, _, err := verifySourcefulEnvelope(packageRaw, *repo, sourcefulPackageEnvelopeSchema, sourcefulPackagePayloadType) if err != nil { @@ -640,14 +664,16 @@ func (m *Manager) RuntimePolicy(cfg config.Driver) (*drivers.RuntimePolicy, erro return nil, fmt.Errorf("validate installed signed package: %w", err) } if !compatible { - if cfg.Control != nil && cfg.Control.Enabled { - return nil, errors.New("control opt-in targets an artifact without an enabled FTW v2 control target") - } - return nil, nil + return nil, errors.New("installed signed package is incompatible with this FTW runtime") } if !strings.EqualFold(entry.SHA256, installed.SHA256) || entry.Version != installed.Version || entry.ID != installed.DriverID { return nil, errors.New("installed artifact does not match its signed package envelope") } + if installed.RepositoryFormat == "" { + if err := m.store.RecordDriverRepoInstallFormat(installed.ID, config.DriverRepositoryFormatSourcefulIndexV1); err != nil { + return nil, fmt.Errorf("record installed signed package format: %w", err) + } + } permissions := make(map[string]bool, len(entry.Permissions)) for _, permission := range entry.Permissions { permissions[permission] = true @@ -663,10 +689,7 @@ func (m *Manager) RuntimePolicy(cfg config.Driver) (*drivers.RuntimePolicy, erro }, nil } if !entry.ControlEnabled { - if cfg.Control != nil && cfg.Control.Enabled { - return nil, errors.New("control opt-in targets an artifact without an enabled FTW v2 control target") - } - return nil, nil + return nil, errors.New("installed signed package lacks an enabled FTW control target") } siteEnabled := false @@ -700,6 +723,43 @@ func (m *Manager) RuntimePolicy(cfg config.Driver) (*drivers.RuntimePolicy, erro }, nil } +func (m *Manager) recordDirectManifestFormat(repo config.DriverRepositorySource, installed state.DriverRepoInstall) error { + if repositoryFormat(repo) != config.DriverRepositoryFormatFTWManifestV1 { + return errors.New("installed driver metadata format is unsupported") + } + // Do not infer an older install's format from a config alias or an + // in-memory manifest. Reverify the saved bytes against the resolved trust + // source and match the recorded origin and exact artifact. + raw, err := readLimitedFile(filepath.Join(m.root, "cache", safeSegment(installed.RepoID)+".json"), maxManifestBytes) + if err != nil { + return fmt.Errorf("verify older installed driver format: %w", err) + } + // An unsigned source can still perform an explicit new install. It + // cannot supply missing historical provenance during startup. + repo.AllowUnsigned = false + manifest, _, err := verifyManifest(raw, repo) + if err != nil { + return fmt.Errorf("verify older installed driver format: %w", err) + } + if err := validateManifest(manifest, repo.AllowInsecure); err != nil { + return fmt.Errorf("validate older installed driver format: %w", err) + } + if manifest.Repository != installed.RepoURL { + return errors.New("older installed driver source does not match its verified manifest") + } + for _, entry := range append(append([]ManifestDriver{}, manifest.Drivers...), manifest.History...) { + if entry.ID != installed.DriverID || entry.Version != installed.Version || !strings.EqualFold(entry.SHA256, installed.SHA256) { + continue + } + if entry.PackageID != "" || (entry.RuntimeABI != "" && entry.RuntimeABI != sourcefulFTWABIV1) || + (entry.HostAPIProfile != "" && entry.HostAPIProfile != sourcefulFTWHostAPIProfileV1) { + return errors.New("older installed driver is not a legacy direct-manifest artifact") + } + return m.store.RecordDriverRepoInstallFormat(installed.ID, config.DriverRepositoryFormatFTWManifestV1) + } + return errors.New("older installed driver is absent from its verified manifest; reinstall the driver") +} + func (m *Manager) directManifestRuntimePolicy( cfg config.Driver, repo config.DriverRepositorySource, diff --git a/go/internal/state/driver_repository.go b/go/internal/state/driver_repository.go index 0616767f7..4f8b39f13 100644 --- a/go/internal/state/driver_repository.go +++ b/go/internal/state/driver_repository.go @@ -1,6 +1,9 @@ package state -import "time" +import ( + "errors" + "time" +) // DriverRepoInstall is durable activation metadata. Lua contents are stored in // the repository manager's content-addressed directory, never in SQLite. @@ -16,6 +19,9 @@ type DriverRepoInstall struct { PreviousInstalledPath string `json:"previous_installed_path,omitempty"` InstalledAtMS int64 `json:"installed_at_ms"` Active bool `json:"active"` + // RepositoryFormat records the verified metadata format at install. + // Empty means an older install whose format still needs verification. + RepositoryFormat string `json:"repository_format,omitempty"` // FTWSigned records what happened at install: the manifest that named this // artifact verified against FTW's own signing key, the one compiled into @@ -41,19 +47,27 @@ func (s *Store) ActivateDriverRepoInstall(in DriverRepoInstall) (DriverRepoInsta } in.PreviousInstalledPath = previous in.InstalledAtMS = time.Now().UnixMilli() - if _, err := tx.Exec(`INSERT INTO driver_repo_installs - (repo_url, repo_id, driver_id, logical_path, version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?) + result, err := tx.Exec(`INSERT INTO driver_repo_installs + (repo_url, repo_id, driver_id, logical_path, version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed, repository_format) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?) ON CONFLICT(repo_id, driver_id, version, sha256) DO UPDATE SET repo_url=excluded.repo_url, logical_path=excluded.logical_path, installed_path=excluded.installed_path, previous_installed_path=excluded.previous_installed_path, installed_at_ms=excluded.installed_at_ms, active=1, - ftw_signed=excluded.ftw_signed`, + ftw_signed=excluded.ftw_signed, + repository_format=CASE WHEN excluded.repository_format = '' + THEN driver_repo_installs.repository_format ELSE excluded.repository_format END + WHERE driver_repo_installs.repository_format = '' OR excluded.repository_format = '' + OR driver_repo_installs.repository_format = excluded.repository_format`, in.RepoURL, in.RepoID, in.DriverID, in.LogicalPath, in.Version, in.SHA256, - in.InstalledPath, in.PreviousInstalledPath, in.InstalledAtMS, boolToInt(in.FTWSigned)); err != nil { + in.InstalledPath, in.PreviousInstalledPath, in.InstalledAtMS, boolToInt(in.FTWSigned), in.RepositoryFormat) + if err != nil { return DriverRepoInstall{}, err } + if n, err := result.RowsAffected(); err != nil || n != 1 { + return DriverRepoInstall{}, errors.New("installed driver metadata format cannot change") + } if err := tx.Commit(); err != nil { return DriverRepoInstall{}, err } @@ -62,19 +76,19 @@ func (s *Store) ActivateDriverRepoInstall(in DriverRepoInstall) (DriverRepoInsta func (s *Store) ActiveDriverRepoInstall(logicalPath string) (DriverRepoInstall, error) { return scanDriverRepoInstall(s.db.QueryRow(`SELECT id, repo_url, repo_id, driver_id, logical_path, - version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed + version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed, repository_format FROM driver_repo_installs WHERE logical_path = ? AND active = 1`, logicalPath)) } func (s *Store) DriverRepoInstallByPath(installedPath string) (DriverRepoInstall, error) { return scanDriverRepoInstall(s.db.QueryRow(`SELECT id, repo_url, repo_id, driver_id, logical_path, - version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed + version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed, repository_format FROM driver_repo_installs WHERE installed_path = ? ORDER BY installed_at_ms DESC LIMIT 1`, installedPath)) } func (s *Store) ActiveDriverRepoInstalls() ([]DriverRepoInstall, error) { rows, err := s.db.Query(`SELECT id, repo_url, repo_id, driver_id, logical_path, - version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed + version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed, repository_format FROM driver_repo_installs WHERE active = 1 ORDER BY logical_path`) if err != nil { return nil, err @@ -96,7 +110,7 @@ func (s *Store) ActiveDriverRepoInstalls() ([]DriverRepoInstall, error) { // the version history that can be reactivated without relying on the network. func (s *Store) DriverRepoInstallsByDriver(driverID string) ([]DriverRepoInstall, error) { rows, err := s.db.Query(`SELECT id, repo_url, repo_id, driver_id, logical_path, - version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed + version, sha256, installed_path, previous_installed_path, installed_at_ms, active, ftw_signed, repository_format FROM driver_repo_installs WHERE driver_id = ? ORDER BY installed_at_ms DESC, id DESC`, driverID) if err != nil { return nil, err @@ -118,6 +132,23 @@ func (s *Store) DeactivateDriverRepoInstall(logicalPath string) error { return err } +// RecordDriverRepoInstallFormat fills an old install's unknown format after +// its metadata has been verified again. It cannot replace a known format. +func (s *Store) RecordDriverRepoInstallFormat(id int64, format string) error { + if format == "" { + return errors.New("installed driver metadata format is required") + } + result, err := s.db.Exec(`UPDATE driver_repo_installs SET repository_format = ? + WHERE id = ? AND (repository_format = '' OR repository_format = ?)`, format, id, format) + if err != nil { + return err + } + if n, err := result.RowsAffected(); err != nil || n != 1 { + return errors.New("installed driver metadata format cannot change") + } + return nil +} + type driverRepoScanner interface{ Scan(...any) error } func scanDriverRepoInstall(row driverRepoScanner) (DriverRepoInstall, error) { @@ -125,7 +156,7 @@ func scanDriverRepoInstall(row driverRepoScanner) (DriverRepoInstall, error) { var active, ftwSigned int err := row.Scan(&out.ID, &out.RepoURL, &out.RepoID, &out.DriverID, &out.LogicalPath, &out.Version, &out.SHA256, &out.InstalledPath, &out.PreviousInstalledPath, - &out.InstalledAtMS, &active, &ftwSigned) + &out.InstalledAtMS, &active, &ftwSigned, &out.RepositoryFormat) out.Active = active == 1 out.FTWSigned = ftwSigned == 1 return out, err diff --git a/go/internal/state/driver_repository_test.go b/go/internal/state/driver_repository_test.go index a68df7299..d9301a41f 100644 --- a/go/internal/state/driver_repository_test.go +++ b/go/internal/state/driver_repository_test.go @@ -61,6 +61,9 @@ func TestUpgradingABoxAddsTheProvenanceColumn(t *testing.T) { if installs[0].FTWSigned { t.Error("a row written before FTW recorded provenance claims FTW signed it") } + if installs[0].RepositoryFormat != "" { + t.Error("an older install claims a metadata format that was not recorded") + } // And a second open must not trip over the column it added the first time. store.Close() @@ -73,3 +76,67 @@ func TestUpgradingABoxAddsTheProvenanceColumn(t *testing.T) { t.Fatalf("read after a second open: %v", err) } } + +func TestDriverRepoInstallFormatSurvivesReactivation(t *testing.T) { + path := filepath.Join(t.TempDir(), "state.db") + store, err := Open(path) + if err != nil { + t.Fatal(err) + } + defer func() { store.Close() }() + old, err := store.ActivateDriverRepoInstall(DriverRepoInstall{ + RepoURL: "https://example.invalid", RepoID: "custom", DriverID: "demo", + LogicalPath: "drivers/demo.lua", Version: "1.0.0", SHA256: "ab", InstalledPath: "/installed/v1.lua", + }) + if err != nil { + t.Fatal(err) + } + if err := store.RecordDriverRepoInstallFormat(old.ID, "verified-format"); err != nil { + t.Fatal(err) + } + if err := store.RecordDriverRepoInstallFormat(old.ID, "different-format"); err == nil { + t.Fatal("backfill changed a known format") + } + if err := store.RecordDriverRepoInstallFormat(old.ID, ""); err == nil { + t.Fatal("backfill erased a known format") + } + // A rollback may still hold the struct read before backfill. Activating + // it must retain the recorded format rather than copy its empty value. + if _, err := store.ActivateDriverRepoInstall(old); err != nil { + t.Fatal(err) + } + if err := store.Close(); err != nil { + t.Fatal(err) + } + store, err = Open(path) + if err != nil { + t.Fatal(err) + } + got, err := store.DriverRepoInstallByPath(old.InstalledPath) + if err != nil || got.RepositoryFormat != "verified-format" || !got.Active { + t.Fatalf("reopened activation = %+v, %v", got, err) + } + newer := old + newer.Version, newer.SHA256, newer.InstalledPath = "2.0.0", "cd", "/installed/v2.lua" + newer.RepositoryFormat = "verified-format" + if _, err := store.ActivateDriverRepoInstall(newer); err != nil { + t.Fatal(err) + } + old.RepositoryFormat = "different-format" + if _, err := store.ActivateDriverRepoInstall(old); err == nil { + t.Fatal("reactivation changed a known format") + } + active, err := store.ActiveDriverRepoInstall(old.LogicalPath) + if err != nil || active.Version != newer.Version || active.RepositoryFormat != "verified-format" { + t.Fatalf("failed reactivation changed the active artifact: %+v, %v", active, err) + } + history, err := store.DriverRepoInstallsByDriver(old.DriverID) + if err != nil || len(history) != 2 { + t.Fatalf("retained history = %+v, %v", history, err) + } + for _, entry := range history { + if entry.RepositoryFormat != "verified-format" { + t.Errorf("retained artifact lost format: %+v", entry) + } + } +} diff --git a/go/internal/state/store.go b/go/internal/state/store.go index 3a053eb9b..d96951c80 100644 --- a/go/internal/state/store.go +++ b/go/internal/state/store.go @@ -869,7 +869,8 @@ func (s *Store) migrate() error { previous_installed_path TEXT NOT NULL DEFAULT '', installed_at_ms INTEGER NOT NULL, active INTEGER NOT NULL DEFAULT 0, - ftw_signed INTEGER NOT NULL DEFAULT 0 + ftw_signed INTEGER NOT NULL DEFAULT 0, + repository_format TEXT NOT NULL DEFAULT '' ) STRICT`, `CREATE UNIQUE INDEX IF NOT EXISTS idx_driver_repo_artifact ON driver_repo_installs(repo_id, driver_id, version, sha256)`, @@ -1016,6 +1017,10 @@ func (s *Store) migrate() error { "INTEGER NOT NULL DEFAULT 0"); err != nil { return err } + if err := s.addColumn("driver_repo_installs", "repository_format", + "TEXT NOT NULL DEFAULT ''"); err != nil { + return err + } if err := s.ensureEnergyLedgerVersion(); err != nil { return err }