Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add payload and add-signature commands. #214

Merged
merged 10 commits into from
May 9, 2022
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.
znewman01 marked this conversation as resolved.
Show resolved Hide resolved

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
znewman01 marked this conversation as resolved.
Show resolved Hide resolved
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
znewman01 marked this conversation as resolved.
Show resolved Hide resolved
}
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
znewman01 marked this conversation as resolved.
Show resolved Hide resolved
}
}
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)
znewman01 marked this conversation as resolved.
Show resolved Hide resolved
znewman01 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return nil, err
}

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

return p, nil
}
Loading