Skip to content

Commit

Permalink
Add sign-payload command.
Browse files Browse the repository at this point in the history
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
  • Loading branch information
znewman01 committed Mar 27, 2022
1 parent 1695529 commit 3cde0d7
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 44 deletions.
26 changes: 15 additions & 11 deletions cmd/tuf/add_signature.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

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

"github.com/flynn/go-docopt"
Expand All @@ -9,30 +11,32 @@ import (
)

func init() {
register("add-signature", cmdAddSignature, `
usage: tuf add-signature <metadata> --key-id <key_id> --signature <sig_file>
register("add-signatures", cmdAddSignature, `
usage: tuf add-signatures --signatures <sig_file> <metadata>
Adds a signature (as hex-encoded bytes) generated by an offline tool to the
given role metadata file.
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>"]
keyID := args.String["<key_id>"]

f := args.String["<sig_file>"]
sigBytes, err := os.ReadFile(f)
if err != nil {
return err
}
sigData := data.HexBytes(sigBytes)

sig := data.Signature{
KeyID: keyID,
Signature: sigData,
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
}
}
return repo.AddOrUpdateSignature(roleFilename, sig)
fmt.Fprintln(os.Stderr, "tuf: added", len(sigs), "new signature(s)")
return nil
}
3 changes: 2 additions & 1 deletion cmd/tuf/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ Commands:
snapshot Update the snapshot metadata file
timestamp Update the timestamp metadata file
payload Output a role's metadata file for signing
add-signature Adds a signature generated offline
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
43 changes: 43 additions & 0 deletions cmd/tuf/sign_file.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 (not necessarily in the TUF repo) using keys for the given role.
Typically, this 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
}
40 changes: 27 additions & 13 deletions repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"path"
Expand Down Expand Up @@ -519,36 +520,49 @@ func (r *Repo) setTopLevelMeta(roleFilename string, meta interface{}) error {
return r.local.SetMeta(roleFilename, b)
}

func (r *Repo) Sign(roleFilename string) error {
role := strings.TrimSuffix(roleFilename, ".json")
// Use the keys associated with role to sign the payload in signed.
func (r *Repo) SignPayload(role string, signed *data.Signed) (int, error) {
if !roles.IsTopLevelRole(role) {
return ErrInvalidRole{role, "only signing top-level metadata supported"}
return -1, ErrInvalidRole{role, "only signing top-level metadata supported"}
}

s, err := r.SignedMeta(roleFilename)
keys, err := r.getSortedSigningKeys(role)
if err != nil {
return err
return -1, err
}
if len(keys) == 0 {
return 0, ErrInsufficientKeys{role}
}
for _, k := range keys {
if err = sign.Sign(signed, k); err != nil {
return -1, err
}
}
return len(keys), nil
}

keys, err := r.getSortedSigningKeys(role)
func (r *Repo) Sign(roleFilename string) error {
signed, err := r.SignedMeta(roleFilename)
if err != nil {
return err
}
if len(keys) == 0 {

role := strings.TrimSuffix(roleFilename, ".json")
numKeys, err := r.SignPayload(role, signed)
if errors.Is(err, ErrInsufficientKeys{role}) {
return ErrInsufficientKeys{roleFilename}
}
for _, k := range keys {
sign.Sign(s, k)
} else if err != nil {
return err
}

b, err := r.jsonMarshal(s)
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 @@ -1066,7 +1080,7 @@ func (r *Repo) timestampFileMeta(roleFilename string) (data.TimestampFileMeta, e
func (r *Repo) Payload(roleFilename string) ([]byte, error) {
role := strings.TrimSuffix(roleFilename, ".json")
if !roles.IsTopLevelRole(role) {
return nil, ErrInvalidRole{role}
return nil, ErrInvalidRole{role, "only signing top-level metadata supported"}
}

s, err := r.SignedMeta(roleFilename)
Expand Down
41 changes: 22 additions & 19 deletions repo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ func (rs *RepoSuite) TestSign(c *C) {
r, err := NewRepo(local)
c.Assert(err, IsNil)

c.Assert(r.Sign("foo.json"), Equals, ErrInvalidRole{"foo", "only signing top-level metadata supported"})
c.Assert(r.Sign("foo.json"), Equals, ErrMissingMetadata{"foo.json"})

// signing with no keys returns ErrInsufficientKeys
c.Assert(r.Sign("root.json"), Equals, ErrInsufficientKeys{"root.json"})
Expand Down Expand Up @@ -1865,36 +1865,39 @@ func (rs *RepoSuite) TestSignDigest(c *C) {

}

func (rs *RepoSuite) TestPayload(c *C) {
signer, err := keys.GenerateEd25519Key()
c.Assert(err, IsNil)

// Test the offline signature flow: Payload -> SignPayload -> AddSignature
func (rs *RepoSuite) TestOfflineFlow(c *C) {
// Set up repo.
meta := make(map[string]json.RawMessage)
local := MemoryStore(meta, nil)
r, err := NewRepo(local)
c.Assert(err, IsNil)
c.Assert(r.Init(false), IsNil)

err = r.AddVerificationKey("root", signer.PublicData())
_, err = r.GenKey("root")
c.Assert(err, IsNil)

// Get the payload to sign
_, err = r.Payload("badrole.json")
c.Assert(err, Equals, ErrInvalidRole{"badrole"})

c.Assert(err, Equals, ErrInvalidRole{"badrole", "only signing top-level metadata supported"})
_, err = r.Payload("root")
c.Assert(err, Equals, ErrMissingMetadata{"root"})

payload, err := r.Payload("root.json")
c.Assert(err, IsNil)
rawSig, err := signer.SignMessage(payload)
keyID := signer.PublicData().IDs()[0]
sig := data.Signature{
KeyID: keyID,
Signature: rawSig,
}
c.Assert(err, IsNil)

// This method checks that the signature verifies!
err = r.AddOrUpdateSignature("root.json", sig)
// Sign the payload
signed := data.Signed{Signed: payload}
_, err = r.SignPayload("badrole", &signed)
c.Assert(err, Equals, ErrInvalidRole{"badrole", "only signing top-level metadata supported"})
_, err = r.SignPayload("targets", &signed)
c.Assert(err, Equals, ErrInsufficientKeys{"targets"})
numKeys, err := r.SignPayload("root", &signed)
c.Assert(err, IsNil)
c.Assert(numKeys, Equals, 1)

// Add the payload signatures back
for _, sig := range signed.Signatures {
// This method checks that the signature verifies!
err = r.AddOrUpdateSignature("root.json", sig)
c.Assert(err, IsNil)
}
}

0 comments on commit 3cde0d7

Please sign in to comment.