From 28513310c39c32ae0a5147f8625583ccdf117f2b Mon Sep 17 00:00:00 2001 From: Andy Doan Date: Sun, 10 Jul 2022 22:49:28 -0500 Subject: [PATCH] feat: Add a `refresh` command This command is helpful for users that would like to have automation tooling keep the timestamp and snapshot metadata fresh. Signed-off-by: Andy Doan --- README.md | 7 +++++++ cmd/tuf/main.go | 1 + cmd/tuf/refresh.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++ repo.go | 32 +++++++++++++++++++++++++++++ repo_test.go | 48 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+) create mode 100644 cmd/tuf/refresh.go diff --git a/README.md b/README.md index 220980f42..0f8f0d1e9 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,13 @@ Adds signatures (the output of `tuf sign-payload`) to the given role metadata fi If the signature does not verify, it will not be added. +#### `tuf refresh --snapshot-threshold=2 --timestamp-threshold=1` + +Update the timestamp and/or snapshop expiration time if either is set to expire +within the given number of hours. + +This command is usual for automation tools that try to keep the metadata fresh. + #### Usage of environment variables The `tuf` CLI supports receiving passphrases via environment variables in diff --git a/cmd/tuf/main.go b/cmd/tuf/main.go index 137420f12..e414f8e77 100644 --- a/cmd/tuf/main.go +++ b/cmd/tuf/main.go @@ -41,6 +41,7 @@ Commands: sign Sign a role's metadata file sign-payload Sign a file from the "payload" command. commit Commit staged files to the repository + refresh Refresh timestamp and snapshot metadata if needed regenerate Recreate the targets metadata file [Not supported yet] set-threshold Sets the threshold for a role get-threshold Outputs the threshold for a role diff --git a/cmd/tuf/refresh.go b/cmd/tuf/refresh.go new file mode 100644 index 000000000..8b0bfae40 --- /dev/null +++ b/cmd/tuf/refresh.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "strconv" + "time" + + "github.com/flynn/go-docopt" + "github.com/theupdateframework/go-tuf" +) + +func init() { + register("refresh", cmdRefresh, ` +usage: tuf refresh [--snapshot-threshold= --timestamp-threshold=] + +Resign the snapshot and/or timestamp metadata if needed. If either metadata +is expiring in less than the number of hours given by the threshold, its +metadata will expiration will be refreshed and the change(s) will be committed. + +Alternatively, passphrases can be set via environment variables in the +form of TUF_{{ROLE}}_PASSPHRASE + +Options: + --snapshot-threshold= Set the threshold for when to resign expiring + snapshot metadata. + --timestamp-threshold= Set the threshold for when to resign expiring + timestamp metadata +`) +} + +func cmdRefresh(args *docopt.Args, repo *tuf.Repo) error { + ssThreshold := time.Now().Add(time.Hour * -1) + tsThreshold := time.Now().Add(time.Hour * -1) + + if arg := args.String["--snapshot-threshold"]; arg != "" { + hours, err := strconv.Atoi(arg) + if err != nil { + return fmt.Errorf("failed to parse --snapshot-threshold arg: %s", err) + } + ssThreshold = time.Now().Add(time.Hour * time.Duration(hours) * -1) + } + if arg := args.String["--timestamp-threshold"]; arg != "" { + hours, err := strconv.Atoi(arg) + if err != nil { + return fmt.Errorf("failed to parse --timestamp-threshold arg: %s", err) + } + tsThreshold = time.Now().Add(time.Hour * time.Duration(hours) * -1) + } + return repo.RefreshExpires(ssThreshold, tsThreshold) +} diff --git a/repo.go b/repo.go index 2354ef4f9..f199c3d3a 100644 --- a/repo.go +++ b/repo.go @@ -1555,3 +1555,35 @@ func (r *Repo) Payload(roleFilename string) ([]byte, error) { return p, nil } + +func (r *Repo) RefreshExpires(snapshotExpires, timestampExpires time.Time) error { + ss, err := r.snapshot() + if err != nil { + return err + } + ts, err := r.timestamp() + if err != nil { + return err + } + + changed := false + + if ss.Expires.Before(snapshotExpires) { + if err = r.Snapshot(); err != nil { + return err + } + changed = true + } + + if changed || ts.Expires.Before(timestampExpires) { + if err = r.Timestamp(); err != nil { + return err + } + changed = true + } + + if changed { + err = r.Commit() + } + return err +} diff --git a/repo_test.go b/repo_test.go index 4f27b3682..09700a2c8 100644 --- a/repo_test.go +++ b/repo_test.go @@ -754,6 +754,54 @@ func (rs *RepoSuite) TestCommit(c *C) { c.Assert(r.Commit(), DeepEquals, ErrNotEnoughKeys{"timestamp", 0, 1}) } +func (rs *RepoSuite) TestRefreshExpires(c *C) { + files := map[string][]byte{"foo.txt": []byte("foo")} + local := MemoryStore(make(map[string]json.RawMessage), files) + r, err := NewRepo(local) + c.Assert(err, IsNil) + + genKey(c, r, "root") + genKey(c, r, "targets") + genKey(c, r, "snapshot") + genKey(c, r, "timestamp") + + // Set up a scenario where only the timestamp needs to be refreshed + c.Assert(r.AddTarget("foo.txt", nil), IsNil) + c.Assert(r.SnapshotWithExpires(time.Now().Add(24*time.Hour)), IsNil) + c.Assert(r.TimestampWithExpires(time.Now().Add(1*time.Hour)), IsNil) + c.Assert(r.Commit(), IsNil) + + ss, _ := r.snapshot() + ssExpiresOrig := ss.Expires + ts, _ := r.timestamp() + tsExpiresOrig := ts.Expires + + thresh := time.Now().Add(2 * time.Hour) + c.Assert(r.RefreshExpires(thresh, thresh), IsNil) + + ss, _ = r.snapshot() + ts, _ = r.timestamp() + c.Assert(ss.Expires, Equals, ssExpiresOrig) + c.Assert(ts.Expires, Not(Equals), tsExpiresOrig) + + // Now have the snapshot need refreshing (it will cause a timestamp change) + c.Assert(r.SnapshotWithExpires(time.Now().Add(1*time.Hour)), IsNil) + c.Assert(r.TimestampWithExpires(time.Now().Add(4*time.Hour)), IsNil) + + ss, _ = r.snapshot() + ssExpiresOrig = ss.Expires + ts, _ = r.timestamp() + tsExpiresOrig = ts.Expires + + thresh = time.Now().Add(2 * time.Hour) + c.Assert(r.RefreshExpires(thresh, thresh), IsNil) + + ss, _ = r.snapshot() + ts, _ = r.timestamp() + c.Assert(ss.Expires, Not(Equals), ssExpiresOrig) + c.Assert(ts.Expires, Not(Equals), tsExpiresOrig) +} + func (rs *RepoSuite) TestCommitVersions(c *C) { files := map[string][]byte{"foo.txt": []byte("foo")} local := MemoryStore(make(map[string]json.RawMessage), files)