diff --git a/README.md b/README.md index b0f69daa..b1a4b6ea 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,10 @@ 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 status --valid-at ` + +Check if the role's metadata will be expired on the given date. + #### 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 83885ced..f2b73972 100644 --- a/cmd/tuf/main.go +++ b/cmd/tuf/main.go @@ -40,6 +40,7 @@ Commands: add-signatures Adds signatures generated offline sign Sign a role's metadata file sign-payload Sign a file from the "payload" command. + status Check if a role's metadata has expired commit Commit staged files to the repository regenerate Recreate the targets metadata file [Not supported yet] set-threshold Sets the threshold for a role diff --git a/cmd/tuf/status.go b/cmd/tuf/status.go new file mode 100644 index 00000000..97568e78 --- /dev/null +++ b/cmd/tuf/status.go @@ -0,0 +1,47 @@ +package main + +import ( + "fmt" + "time" + + "github.com/flynn/go-docopt" + "github.com/theupdateframework/go-tuf" +) + +func init() { + register("status", cmdStatus, ` +usage: tuf status --valid-at= + +Check if the role's metadata will be expired on the given date. + +The command's exit status will be 1 if the role has expired, 0 otherwise. + +Example: + # See if timestamp metadata is expiring in the next hour: + tuf status --valid-at "$(date -d '+1 hour')" timestamp || echo "Time to refresh" + +Options: + --valid-at= Must be in one of the formats: + * RFC3339 - 2006-01-02T15:04:05Z07:00 + * RFC822 - 02 Jan 06 15:04 MST + * UnixDate - Mon Jan _2 15:04:05 MST 2006 +`) +} + +func cmdStatus(args *docopt.Args, repo *tuf.Repo) error { + role := args.String[""] + validAtStr := args.String["--valid-at"] + + formats := []string{ + time.RFC3339, + time.RFC822, + time.UnixDate, + } + for _, fmt := range formats { + validAt, err := time.Parse(fmt, validAtStr) + if err == nil { + return repo.CheckRoleUnexpired(role, validAt) + } + } + return fmt.Errorf("failed to parse --valid-at arg") +} diff --git a/repo.go b/repo.go index 2354ef4f..887a04ad 100644 --- a/repo.go +++ b/repo.go @@ -1555,3 +1555,39 @@ func (r *Repo) Payload(roleFilename string) ([]byte, error) { return p, nil } + +func (r *Repo) CheckRoleUnexpired(role string, validAt time.Time) error { + var expires time.Time + switch role { + case "root": + root, err := r.root() + if err != nil { + return err + } + expires = root.Expires + case "snapshot": + snapshot, err := r.snapshot() + if err != nil { + return err + } + expires = snapshot.Expires + case "timestamp": + timestamp, err := r.timestamp() + if err != nil { + return err + } + expires = timestamp.Expires + case "targets": + targets, err := r.topLevelTargets() + if err != nil { + return err + } + expires = targets.Expires + default: + return fmt.Errorf("invalid role: %s", role) + } + if expires.Before(validAt) || expires.Equal(validAt) { + return fmt.Errorf("role expired on: %s", expires) + } + return nil +} diff --git a/repo_test.go b/repo_test.go index 4f27b368..a8f44052 100644 --- a/repo_test.go +++ b/repo_test.go @@ -688,6 +688,29 @@ func (rs *RepoSuite) TestSign(c *C) { c.Assert(r.Sign("targets.json"), Equals, ErrMissingMetadata{"targets.json"}) } +func (rs *RepoSuite) TestStatus(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") + + 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) + + expires := time.Now().Add(2 * time.Hour) + c.Assert(r.CheckRoleUnexpired("timestamp", expires), ErrorMatches, "role expired on.*") + c.Assert(r.CheckRoleUnexpired("snapshot", expires), IsNil) + c.Assert(r.CheckRoleUnexpired("targets", expires), IsNil) + c.Assert(r.CheckRoleUnexpired("root", expires), IsNil) +} + func (rs *RepoSuite) TestCommit(c *C) { files := map[string][]byte{"foo.txt": []byte("foo"), "bar.txt": []byte("bar")} local := MemoryStore(make(map[string]json.RawMessage), files)