Skip to content

Commit

Permalink
feat: Add new status command (#342)
Browse files Browse the repository at this point in the history
This command can be used to help automation tooling decide when
metadata expirations will be reached.

 # See if timestamp metadata is expiring in the next hour:
 $ tuf status --valid-at "$(date -d '+1 hour')" timestamp

Signed-off-by: Andy Doan <andy@foundries.io>
  • Loading branch information
doanac committed Jul 19, 2022
1 parent 9334b3f commit af3c7d6
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <date> <role>`

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
Expand Down
1 change: 1 addition & 0 deletions cmd/tuf/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions cmd/tuf/status.go
Original file line number Diff line number Diff line change
@@ -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=<date> <role>
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=<date> 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["<role>"]
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")
}
36 changes: 36 additions & 0 deletions repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
23 changes: 23 additions & 0 deletions repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit af3c7d6

Please sign in to comment.