Skip to content

Commit

Permalink
feat: add payload and add-signature commands. (#214)
Browse files Browse the repository at this point in the history
* feat: Add `payload` and `add-signature` commands.

Fixes #205.

* docs: Clarify `payload` and `add-signature` args.

Specifically, they expect a metadata file name, *not* a role name.

Added a test for each.

* feat: Add `sign-payload` command.

This completes the offline flow:

```shell
tuf payload root.json > /tmp/root.json.payload
tuf sign-payload --role=root /tmp/root.json.payload > /tmp/root.json.sigs
tuf add-signatures --signatures /tmp/root.json.sigs root.json
```

Additional changes:
- rename `add-signature` to `add-signatures`
- `add-signatures` expects JSON (from `sign-payload`) rather than hex bytes

* docs: Beef up documentation for offline signature flow.

- move CLI commands to matching file names
- add examples to README.md
- more details for `repo.SignPayload` docs

* docs: Point out where keys are stored in `sign-payload` docs

* fix: ensure that output is canonicalized

* style: rename ErrInsufficientKeys to ErrNoKeys

* doc: minor `tuf sign-payload` clarifiation

* test: add client test for offline flow

* test: fix tests after rebase
  • Loading branch information
znewman01 authored May 9, 2022
1 parent ed6788e commit 4bf58eb
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 19 deletions.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,28 @@ Changes the passphrase for given role keys file. The CLI supports reading
both the existing and the new passphrase via the following environment
variables - `TUF_{{ROLE}}_PASSPHRASE` and respectively `TUF_NEW_{{ROLE}}_PASSPHRASE`

#### `tuf payload <metadata>`

Outputs the metadata file for a role in a ready-to-sign (canonicalized) format.

See also `tuf sign-payload` and `tuf add-signatures`.

#### `tuf sign-payload --role=<role> <path>`

Sign a file (outside of the TUF repo) using keys (in the TUF keys database,
typically produced by `tuf gen-key`) for the given `role` (from the TUF repo).

Typically, `path` will be a file containing the output of `tuf payload`.

See also `tuf add-signatures`.

#### `tuf add-signatures --signatures <sig_file> <metadata>`


Adds signatures (the output of `tuf sign-payload`) to the given role metadata file.

If the signature does not verify, it will not be added.

#### Usage of environment variables

The `tuf` CLI supports receiving passphrases via environment variables in
Expand Down Expand Up @@ -229,6 +251,46 @@ Enter root keys passphrase:
The staged `root.json` can now be copied back to the repo box ready to be
committed alongside other metadata files.

#### Alternate signing flow

Instead of manually copying `root.json` into the TUF repository on the root box,
you can use the `tuf payload`, `tuf sign-payload`, `tuf add-signatures` flow.

On the repo box, get the `root.json` payload in a canonical format:

``` bash
$ tuf payload root.json > root.json.payload
```

Copy `root.json.payload` to the root box and sign it:


``` bash
$ tuf sign-payload --role=root root.json.payload > root.json.sigs
Enter root keys passphrase:
```

Copy `root.json.sigs` back to the repo box and import the signatures:

``` bash
$ tuf add-signatures --signatures=root.json.sigs root.json
```

This achieves the same state as the above flow for the repo box:

```bash
$ tree .
.
├── keys
│   ├── snapshot.json
│   ├── targets.json
│   └── timestamp.json
├── repository
└── staged
├── root.json
└── targets
```

#### Add a target file

Assuming a staged, signed `root` metadata file and the file to add exists at
Expand Down
30 changes: 30 additions & 0 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,36 @@ func (s *ClientSuite) TestNewTargetsKey(c *C) {
c.Assert(role.KeyIDs, DeepEquals, sets.StringSliceToSet(newIDs))
}

func (s *ClientSuite) TestOfflineSignatureFlow(c *C) {
client := s.newClient(c)

// replace key
oldIDs := s.keyIDs["targets"]
c.Assert(s.repo.RevokeKey("targets", oldIDs[0]), IsNil)
_ = s.genKey(c, "targets")

// re-sign targets using offline flow and generate new snapshot and timestamp
payload, err := s.repo.Payload("targets.json")
c.Assert(err, IsNil)
signed := data.Signed{Signed: payload}
_, err = s.repo.SignPayload("targets", &signed)
c.Assert(err, IsNil)
for _, sig := range signed.Signatures {
// This method checks that the signature verifies!
err = s.repo.AddOrUpdateSignature("targets.json", sig)
c.Assert(err, IsNil)
}
c.Assert(s.repo.Snapshot(), IsNil)
c.Assert(s.repo.Timestamp(), IsNil)
c.Assert(s.repo.Commit(), IsNil)
s.syncRemote(c)

// check update gets new metadata
c.Assert(client.getLocalMeta(), IsNil)
_, err = client.Update()
c.Assert(err, IsNil)
}

func (s *ClientSuite) TestLocalExpired(c *C) {
client := s.newClient(c)

Expand Down
42 changes: 42 additions & 0 deletions cmd/tuf/add_signatures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"encoding/json"
"fmt"
"os"

"github.com/flynn/go-docopt"
"github.com/theupdateframework/go-tuf"
"github.com/theupdateframework/go-tuf/data"
)

func init() {
register("add-signatures", cmdAddSignature, `
usage: tuf add-signatures --signatures <sig_file> <metadata>
Adds signatures (the output of "sign-payload") to the given role metadata file.
If the signature does not verify, it will not be added.
`)
}

func cmdAddSignature(args *docopt.Args, repo *tuf.Repo) error {
roleFilename := args.String["<metadata>"]

f := args.String["<sig_file>"]
sigBytes, err := os.ReadFile(f)
if err != nil {
return err
}
sigs := []data.Signature{}
if err = json.Unmarshal(sigBytes, &sigs); err != nil {
return err
}
for _, sig := range sigs {
if err = repo.AddOrUpdateSignature(roleFilename, sig); err != nil {
return err
}
}
fmt.Fprintln(os.Stderr, "tuf: added", len(sigs), "new signature(s)")
return nil
}
3 changes: 3 additions & 0 deletions cmd/tuf/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,10 @@ Commands:
remove Remove a target file
snapshot Update the snapshot metadata file
timestamp Update the timestamp metadata file
payload Output a role's metadata file for signing
add-signatures Adds signatures generated offline
sign Sign a role's metadata file
sign-payload Sign a file from the "payload" command.
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
25 changes: 25 additions & 0 deletions cmd/tuf/payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"fmt"

"github.com/flynn/go-docopt"
"github.com/theupdateframework/go-tuf"
)

func init() {
register("payload", cmdPayload, `
usage: tuf payload <metadata>
Outputs the metadata file for a role in a ready-to-sign (canonicalized) format.
`)
}

func cmdPayload(args *docopt.Args, repo *tuf.Repo) error {
p, err := repo.Payload(args.String["<metadata>"])
if err != nil {
return err
}
fmt.Print(string(p))
return nil
}
43 changes: 43 additions & 0 deletions cmd/tuf/sign_payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"encoding/json"
"fmt"
"os"

"github.com/flynn/go-docopt"
tuf "github.com/theupdateframework/go-tuf"
tufdata "github.com/theupdateframework/go-tuf/data"
)

func init() {
register("sign-payload", cmdSignPayload, `
usage: tuf sign-payload --role=<role> <path>
Sign a file (outside of the TUF repo) using keys for the given role (from the TUF repo).
Typically, path will be the output of "tuf payload".
`)
}

func cmdSignPayload(args *docopt.Args, repo *tuf.Repo) error {
payload, err := os.ReadFile(args.String["<path>"])
if err != nil {
return err
}
signed := tufdata.Signed{Signed: payload, Signatures: make([]tufdata.Signature, 0)}

numKeys, err := repo.SignPayload(args.String["--role"], &signed)
if err != nil {
return err
}

bytes, err := json.Marshal(signed.Signatures)
if err != nil {
return err
}
fmt.Print(string(bytes))

fmt.Fprintln(os.Stderr, "tuf: signed with", numKeys, "key(s)")
return nil
}
8 changes: 4 additions & 4 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type ErrMissingMetadata struct {
}

func (e ErrMissingMetadata) Error() string {
return fmt.Sprintf("tuf: missing metadata %s", e.Name)
return fmt.Sprintf("tuf: missing metadata file %s", e.Name)
}

type ErrFileNotFound struct {
Expand All @@ -28,12 +28,12 @@ func (e ErrFileNotFound) Error() string {
return fmt.Sprintf("tuf: file not found %s", e.Path)
}

type ErrInsufficientKeys struct {
type ErrNoKeys struct {
Name string
}

func (e ErrInsufficientKeys) Error() string {
return fmt.Sprintf("tuf: insufficient keys to sign %s", e.Name)
func (e ErrNoKeys) Error() string {
return fmt.Sprintf("tuf: no keys available to sign %s", e.Name)
}

type ErrInsufficientSignatures struct {
Expand Down
57 changes: 44 additions & 13 deletions repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"path"
"sort"
"strings"
"time"

"github.com/secure-systems-lab/go-securesystemslib/cjson"
"github.com/theupdateframework/go-tuf/data"
"github.com/theupdateframework/go-tuf/internal/roles"
"github.com/theupdateframework/go-tuf/internal/sets"
Expand Down Expand Up @@ -734,33 +736,48 @@ func (r *Repo) setMeta(roleFilename string, meta interface{}) error {
return r.local.SetMeta(roleFilename, b)
}

func (r *Repo) Sign(roleFilename string) error {
role := strings.TrimSuffix(roleFilename, ".json")

s, err := r.SignedMeta(roleFilename)
if err != nil {
return err
}

// SignPayload signs the given payload using the key(s) associated with role.
//
// It returns the total number of keys used for signing, 0 (along with
// ErrNoKeys) if no keys were found, or -1 (along with an error) in error cases.
func (r *Repo) SignPayload(role string, payload *data.Signed) (int, error) {
keys, err := r.signersForRole(role)
if err != nil {
return err
return -1, err
}
if len(keys) == 0 {
return ErrInsufficientKeys{roleFilename}
return 0, ErrNoKeys{role}
}
for _, k := range keys {
sign.Sign(s, k)
if err = sign.Sign(payload, k); err != nil {
return -1, err
}
}
return len(keys), nil
}

b, err := r.jsonMarshal(s)
func (r *Repo) Sign(roleFilename string) error {
signed, err := r.SignedMeta(roleFilename)
if err != nil {
return err
}

role := strings.TrimSuffix(roleFilename, ".json")
numKeys, err := r.SignPayload(role, signed)
if errors.Is(err, ErrNoKeys{role}) {
return ErrNoKeys{roleFilename}
} else if err != nil {
return err
}

b, err := r.jsonMarshal(signed)
if err != nil {
return err
}
r.meta[roleFilename] = b
err = r.local.SetMeta(roleFilename, b)
if err == nil {
fmt.Println("Signed", roleFilename, "with", len(keys), "key(s)")
fmt.Println("Signed", roleFilename, "with", numKeys, "key(s)")
}
return err
}
Expand Down Expand Up @@ -1527,3 +1544,17 @@ func (r *Repo) timestampFileMeta(roleFilename string) (data.TimestampFileMeta, e
}
return util.GenerateTimestampFileMeta(bytes.NewReader(b), r.hashAlgorithms...)
}

func (r *Repo) Payload(roleFilename string) ([]byte, error) {
s, err := r.SignedMeta(roleFilename)
if err != nil {
return nil, err
}

p, err := cjson.EncodeCanonical(s.Signed)
if err != nil {
return nil, err
}

return p, nil
}
Loading

0 comments on commit 4bf58eb

Please sign in to comment.