Skip to content

Commit

Permalink
Added an unsafe method for loading the tuf metadata on disk (theupdat…
Browse files Browse the repository at this point in the history
…eframework#87)

* Added an unsafe method for loading the tuf metadata on disk

Signed-off-by: Fredrik Skogman <kommendorkapten@github.com>

* Feedback from review. Added a config parameter instead of a separate method.

Signed-off-by: Fredrik Skogman <kommendorkapten@github.com>

* Added unit tests for unsafe local mode

Signed-off-by: Fredrik Skogman <kommendorkapten@github.com>

* DEBUG: remove added tests

Signed-off-by: Fredrik Skogman <kommendorkapten@github.com>

* comment out correct test

Signed-off-by: Fredrik Skogman <kommendorkapten@github.com>

* Uncommented tests cases and disabled go caching

Signed-off-by: Fredrik Skogman <kommendorkapten@github.com>

---------

Signed-off-by: Fredrik Skogman <kommendorkapten@github.com>
  • Loading branch information
kommendorkapten authored and rdimitrov committed Jan 25, 2024
1 parent 0c0a360 commit 4f5046b
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 3 deletions.
4 changes: 4 additions & 0 deletions metadata/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ type UpdaterConfig struct {
RemoteTargetsURL string
DisableLocalCache bool
PrefixTargetsWithHash bool
// UnsafeLocalMode only uses the metadata as written on disk
// if the metadata is incomplete, calling updater.Refresh will fail
UnsafeLocalMode bool
}

// New creates a new UpdaterConfig instance used by the Updater to
Expand All @@ -61,6 +64,7 @@ func New(remoteURL string, rootBytes []byte) (*UpdaterConfig, error) {
RemoteTargetsURL: targetsURL, // URL of where the target files should be downloaded from
DisableLocalCache: false, // enable local caching of trusted metadata
PrefixTargetsWithHash: true, // use hash-prefixed target files with consistent snapshots
UnsafeLocalMode: false,
}, nil
}

Expand Down
59 changes: 58 additions & 1 deletion metadata/updater/updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func New(config *config.UpdaterConfig) (*Updater, error) {
return updater, nil
}

// Refresh refreshes top-level metadata.
// Refresh loads and possibly refreshes top-level metadata.
// Downloads, verifies, and loads metadata for the top-level roles in the
// specified order (root -> timestamp -> snapshot -> targets) implementing
// all the checks required in the TUF client workflow.
Expand All @@ -102,7 +102,20 @@ func New(config *config.UpdaterConfig) (*Updater, error) {
// that happens on demand during GetTargetInfo(). However, if the
// repository uses consistent snapshots (ref. https://theupdateframework.github.io/specification/latest/#consistent-snapshots),
// then all metadata downloaded by the Updater will use the same consistent repository state.
//
// If UnsafeLocalMode is set, no network interaction is performed, only
// the cached files on disk are used. If the cached data is not complete,
// this call will fail.
func (update *Updater) Refresh() error {
if update.cfg.UnsafeLocalMode {
return update.unsafeLocalRefresh()
}
return update.onlineRefresh()
}

// onlineRefresh implements the TUF client workflow as described for
// the Refresh function.
func (update *Updater) onlineRefresh() error {
err := update.loadRoot()
if err != nil {
return err
Expand All @@ -122,6 +135,50 @@ func (update *Updater) Refresh() error {
return nil
}

// unsafeLoadRefresh tries to load the persisted metadata already cached
// on disk. Note that this is an usafe function, and does deviate from the
// TUF specification section 5.3 to 5.7 (update phases).
// The metadata on disk are verified against the provided root though,
// and expiration dates are verified.
func (update *Updater) unsafeLocalRefresh() error {
// Root is already loaded
// load timestamp
var p = filepath.Join(update.cfg.LocalMetadataDir, metadata.TIMESTAMP)
data, err := update.loadLocalMetadata(p)
if err != nil {
return err
}
_, err = update.trusted.UpdateTimestamp(data)
if err != nil {
return err
}

// load snapshot
p = filepath.Join(update.cfg.LocalMetadataDir, metadata.SNAPSHOT)
data, err = update.loadLocalMetadata(p)
if err != nil {
return err
}
_, err = update.trusted.UpdateSnapshot(data, false)
if err != nil {
return err
}

// targets
p = filepath.Join(update.cfg.LocalMetadataDir, metadata.TARGETS)
data, err = update.loadLocalMetadata(p)
if err != nil {
return err
}
// verify and load the new target metadata
_, err = update.trusted.UpdateDelegatedTargets(data, metadata.TARGETS, metadata.ROOT)
if err != nil {
return err
}

return nil
}

// GetTargetInfo returns metadata.TargetFiles instance with information
// for targetPath. The return value can be used as an argument to
// DownloadTarget() and FindCachedTarget().
Expand Down
109 changes: 107 additions & 2 deletions metadata/updater/updater_top_level_update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,17 @@ func loadUpdaterConfig() (*config.UpdaterConfig, error) {
return updaterConfig, err
}

// runRefresh creates new Updater instance and
// runs Refresh
func loadUnsafeUpdaterConfig() (*config.UpdaterConfig, error) {
updaterConfig, err := loadUpdaterConfig()
if err != nil {
return nil, err
}
updaterConfig.UnsafeLocalMode = true

return updaterConfig, nil
}

// runRefresh creates new Updater instance and runs Refresh
func runRefresh(updaterConfig *config.UpdaterConfig, moveInTime time.Time) (Updater, error) {
if len(simulator.Sim.DumpDir) > 0 {
simulator.Sim.Write()
Expand Down Expand Up @@ -116,6 +125,23 @@ func assertFilesExist(t *testing.T, roles []string) {
}
}

func assertFilesExact(t *testing.T, roles []string) {
expectedFiles := []string{}

for _, role := range roles {
expectedFiles = append(expectedFiles, fmt.Sprintf("%s.json", role))
}
localMetadataFiles, err := os.ReadDir(simulator.MetadataDir)
assert.NoError(t, err)

actual := []string{}
for _, file := range localMetadataFiles {
actual = append(actual, file.Name())
}

assert.ElementsMatch(t, actual, expectedFiles)
}

// Asserts that local file content is the expected
func assertContentEquals(t *testing.T, role string, version *int) {
expectedContent, err := simulator.Sim.FetchMetadata(role, version)
Expand Down Expand Up @@ -169,6 +195,27 @@ func TestLoadTrustedRootMetadata(t *testing.T) {
}
}

func TestUnsafeLoadTrustedRootMetadata(t *testing.T) {
err := loadOrResetTrustedRootMetadata()
assert.NoError(t, err)

updaterConfig, err := loadUnsafeUpdaterConfig()
assert.NoError(t, err)
updater, err := New(updaterConfig)
assert.NoError(t, err)

assert.Nil(t, err)
if assert.NotNil(t, updater) {
assert.Equal(t, metadata.ROOT, updater.trusted.Root.Signed.Type)
assert.Equal(t, metadata.SPECIFICATION_VERSION, updater.trusted.Root.Signed.SpecVersion)
assert.True(t, updater.trusted.Root.Signed.ConsistentSnapshot)
assert.Equal(t, int64(1), updater.trusted.Root.Signed.Version)
assert.Nil(t, updater.trusted.Snapshot)
assert.Nil(t, updater.trusted.Timestamp)
assert.Empty(t, updater.trusted.Targets)
}
}

func TestFirstTimeRefresh(t *testing.T) {
err := loadOrResetTrustedRootMetadata()
assert.NoError(t, err)
Expand All @@ -192,6 +239,64 @@ func TestFirstTimeRefresh(t *testing.T) {
}
}

func TestFirstUnsafeTimeRefresh(t *testing.T) {
err := loadOrResetTrustedRootMetadata()
assert.NoError(t, err)

assertFilesExist(t, []string{metadata.ROOT})
simulator.Sim.MDRoot.Signed.Version += 1
simulator.Sim.PublishRoot()

updaterConfig, err := loadUnsafeUpdaterConfig()
assert.NoError(t, err)
_, err = runRefresh(updaterConfig, time.Now())
assert.Error(t, err)
// As no update was made only the root file should be present
assertFilesExact(t, []string{metadata.ROOT})
}

func TestUnsafeRefresh(t *testing.T) {
// First run a "real" refresh
err := loadOrResetTrustedRootMetadata()
assert.NoError(t, err)

assertFilesExist(t, []string{metadata.ROOT})
simulator.Sim.MDRoot.Signed.Version += 1
simulator.Sim.PublishRoot()

updaterConfig, err := loadUpdaterConfig()
assert.NoError(t, err)
_, err = runRefresh(updaterConfig, time.Now())
assert.NoError(t, err)
assertFilesExist(t, metadata.TOP_LEVEL_ROLE_NAMES[:])

// Create a new unsafe updater, verify content is still valid
updaterConfig, err = loadUnsafeUpdaterConfig()
assert.NoError(t, err)
updater, err := runRefresh(updaterConfig, time.Now())
assert.NotNil(t, updater)
assert.NoError(t, err)
assertFilesExist(t, metadata.TOP_LEVEL_ROLE_NAMES[:])

for _, role := range metadata.TOP_LEVEL_ROLE_NAMES {
var version int
if role == metadata.ROOT {
// The root file is written when the updater is
// created, so the version is reset.
version = 1
}
assertContentEquals(t, role, &version)
}

assert.Equal(t, metadata.ROOT, updater.trusted.Root.Signed.Type)
assert.Equal(t, metadata.SPECIFICATION_VERSION, updater.trusted.Root.Signed.SpecVersion)
assert.True(t, updater.trusted.Root.Signed.ConsistentSnapshot)
assert.Equal(t, int64(1), updater.trusted.Root.Signed.Version)
assert.NotNil(t, updater.trusted.Snapshot)
assert.NotNil(t, updater.trusted.Timestamp)
assert.Equal(t, 1, len(updater.trusted.Targets))
}

func TestTrustedRootMissing(t *testing.T) {
err := loadOrResetTrustedRootMetadata()
assert.NoError(t, err)
Expand Down

0 comments on commit 4f5046b

Please sign in to comment.