diff --git a/cmd/tuf/add_signature.go b/cmd/tuf/add_signature.go index 88ab99018..650873601 100644 --- a/cmd/tuf/add_signature.go +++ b/cmd/tuf/add_signature.go @@ -1,6 +1,8 @@ package main import ( + "encoding/json" + "fmt" "os" "github.com/flynn/go-docopt" @@ -9,11 +11,10 @@ import ( ) func init() { - register("add-signature", cmdAddSignature, ` -usage: tuf add-signature --key-id --signature + register("add-signatures", cmdAddSignature, ` +usage: tuf add-signatures --signatures -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. `) @@ -21,18 +22,21 @@ If the signature does not verify, it will not be added. func cmdAddSignature(args *docopt.Args, repo *tuf.Repo) error { roleFilename := args.String[""] - keyID := args.String[""] f := args.String[""] 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 } diff --git a/cmd/tuf/main.go b/cmd/tuf/main.go index 8738ae4cc..137420f12 100644 --- a/cmd/tuf/main.go +++ b/cmd/tuf/main.go @@ -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 diff --git a/cmd/tuf/sign_file.go b/cmd/tuf/sign_file.go new file mode 100644 index 000000000..d79fb7dad --- /dev/null +++ b/cmd/tuf/sign_file.go @@ -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= + +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[""]) + 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 +} diff --git a/repo.go b/repo.go index 25ff4b1b0..8639c9c88 100644 --- a/repo.go +++ b/repo.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "path" @@ -549,36 +550,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} + return -1, ErrInvalidRole{role} } - 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 } diff --git a/repo_test.go b/repo_test.go index 488f915ad..37affe0df 100644 --- a/repo_test.go +++ b/repo_test.go @@ -1838,36 +1838,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"}) - _, 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"}) + _, 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) + } }