From d3c12fc839211907d6351b301a11a486ba1241ba Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Tue, 1 Feb 2022 10:57:03 -0500 Subject: [PATCH 01/10] feat: Add `payload` and `add-signature` commands. Fixes #205. --- cmd/tuf/add_signature.go | 37 +++++++++++++++++++++++++++++++++++++ cmd/tuf/main.go | 2 ++ cmd/tuf/payload.go | 27 +++++++++++++++++++++++++++ repo.go | 15 +++++++++++++++ repo_test.go | 31 +++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+) create mode 100644 cmd/tuf/add_signature.go create mode 100644 cmd/tuf/payload.go diff --git a/cmd/tuf/add_signature.go b/cmd/tuf/add_signature.go new file mode 100644 index 00000000..5dbbe19f --- /dev/null +++ b/cmd/tuf/add_signature.go @@ -0,0 +1,37 @@ +package main + +import ( + "os" + + "github.com/flynn/go-docopt" + "github.com/theupdateframework/go-tuf" + "github.com/theupdateframework/go-tuf/data" +) + +func init() { + register("add-signature", cmdAddSignature, ` +usage: tuf add-signature --key-id --signature + +Adds a signature (as hex-encoded bytes) generated by an offline tool to the given role. + +If the signature does not verify, it will not be added. +`) +} + +func cmdAddSignature(args *docopt.Args, repo *tuf.Repo) error { + role := 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, + } + return repo.AddOrUpdateSignature(role, sig) +} diff --git a/cmd/tuf/main.go b/cmd/tuf/main.go index 4017987e..8738ae4c 100644 --- a/cmd/tuf/main.go +++ b/cmd/tuf/main.go @@ -36,6 +36,8 @@ 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-signature Adds a signature generated offline sign Sign a role's metadata file commit Commit staged files to the repository regenerate Recreate the targets metadata file [Not supported yet] diff --git a/cmd/tuf/payload.go b/cmd/tuf/payload.go new file mode 100644 index 00000000..ebb5bcff --- /dev/null +++ b/cmd/tuf/payload.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + + "github.com/flynn/go-docopt" + "github.com/theupdateframework/go-tuf" +) + +func init() { + register("payload", cmdPayload, ` +usage: tuf payload + +Output a role's metadata in a ready-to-sign format. + +The output is canonicalized. +`) +} + +func cmdPayload(args *docopt.Args, repo *tuf.Repo) error { + p, err := repo.Payload(args.String[""]) + if err != nil { + return err + } + fmt.Print(string(p)) + return nil +} diff --git a/repo.go b/repo.go index 0553fd61..6b7cb8f7 100644 --- a/repo.go +++ b/repo.go @@ -1527,3 +1527,18 @@ 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) { + role := strings.TrimSuffix(roleFilename, ".json") + if !roles.IsTopLevelRole(role) { + // TODO: handle payloads with delegated roles + return nil, ErrInvalidRole{role, "Payload() only supports top-level roles"} + } + + s, err := r.SignedMeta(roleFilename) + if err != nil { + return nil, err + } + + return s.Signed, nil +} diff --git a/repo_test.go b/repo_test.go index c71bf5f0..7bbf37f7 100644 --- a/repo_test.go +++ b/repo_test.go @@ -2544,3 +2544,34 @@ func (rs *RepoSuite) TestAddOrUpdateSignatureWithDelegations(c *C) { c.Assert(r.Timestamp(), IsNil) c.Assert(r.Commit(), IsNil) } + +func (rs *RepoSuite) TestPayload(c *C) { + signer, err := keys.GenerateEd25519Key() + c.Assert(err, IsNil) + + 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()) + c.Assert(err, IsNil) + + _, err = r.Payload("badrole.json") + c.Assert(err, NotNil) + + 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) + c.Assert(err, IsNil) +} From 132ef82ee0e57e92036ecfa3c6ebbce057d335fe Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Sun, 20 Mar 2022 09:55:50 -0400 Subject: [PATCH 02/10] docs: Clarify `payload` and `add-signature` args. Specifically, they expect a metadata file name, *not* a role name. Added a test for each. --- cmd/tuf/add_signature.go | 9 +++++---- cmd/tuf/payload.go | 6 +++--- errors.go | 2 +- repo_test.go | 12 +++++++++++- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/cmd/tuf/add_signature.go b/cmd/tuf/add_signature.go index 5dbbe19f..88ab9901 100644 --- a/cmd/tuf/add_signature.go +++ b/cmd/tuf/add_signature.go @@ -10,16 +10,17 @@ import ( func init() { register("add-signature", cmdAddSignature, ` -usage: tuf add-signature --key-id --signature +usage: tuf add-signature --key-id --signature -Adds a signature (as hex-encoded bytes) generated by an offline tool to the given role. +Adds a signature (as hex-encoded bytes) generated by an offline tool 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 { - role := args.String[""] + roleFilename := args.String[""] keyID := args.String[""] f := args.String[""] @@ -33,5 +34,5 @@ func cmdAddSignature(args *docopt.Args, repo *tuf.Repo) error { KeyID: keyID, Signature: sigData, } - return repo.AddOrUpdateSignature(role, sig) + return repo.AddOrUpdateSignature(roleFilename, sig) } diff --git a/cmd/tuf/payload.go b/cmd/tuf/payload.go index ebb5bcff..087f2d54 100644 --- a/cmd/tuf/payload.go +++ b/cmd/tuf/payload.go @@ -9,16 +9,16 @@ import ( func init() { register("payload", cmdPayload, ` -usage: tuf payload +usage: tuf payload -Output a role's metadata in a ready-to-sign format. +Output the metadata file for a role in a ready-to-sign format. The output is canonicalized. `) } func cmdPayload(args *docopt.Args, repo *tuf.Repo) error { - p, err := repo.Payload(args.String[""]) + p, err := repo.Payload(args.String[""]) if err != nil { return err } diff --git a/errors.go b/errors.go index 09df0390..e8120554 100644 --- a/errors.go +++ b/errors.go @@ -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 { diff --git a/repo_test.go b/repo_test.go index 7bbf37f7..edfe87ae 100644 --- a/repo_test.go +++ b/repo_test.go @@ -1784,6 +1784,13 @@ func (rs *RepoSuite) TestBadAddOrUpdateSignatures(c *C) { c.Assert(err, IsNil) c.Assert(r.AddVerificationKey("timestamp", timestampKey.PublicData()), IsNil) + // attempt to sign `root`, rather than `root.json` + for _, id := range rootKey.PublicData().IDs() { + c.Assert(r.AddOrUpdateSignature("root", data.Signature{ + KeyID: id, + Signature: nil}), Equals, ErrMissingMetadata{"root"}) + } + // add a signature with a bad role rootMeta, err := r.SignedMeta("root.json") c.Assert(err, IsNil) @@ -2559,7 +2566,10 @@ func (rs *RepoSuite) TestPayload(c *C) { c.Assert(err, IsNil) _, err = r.Payload("badrole.json") - c.Assert(err, NotNil) + c.Assert(err, Equals, ErrInvalidRole{"badrole", "Payload() only supports top-level roles"}) + + _, err = r.Payload("root") + c.Assert(err, Equals, ErrMissingMetadata{"root"}) payload, err := r.Payload("root.json") c.Assert(err, IsNil) From 4a2f60813b632dc58e32e615d59e96205f0d90e2 Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Sun, 20 Mar 2022 11:58:14 -0400 Subject: [PATCH 03/10] 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 --- cmd/tuf/add_signature.go | 26 ++++++++++++++---------- cmd/tuf/main.go | 3 ++- cmd/tuf/sign_file.go | 43 ++++++++++++++++++++++++++++++++++++++++ repo.go | 41 +++++++++++++++++++++++++++----------- repo_test.go | 39 +++++++++++++++++++----------------- 5 files changed, 110 insertions(+), 42 deletions(-) create mode 100644 cmd/tuf/sign_file.go diff --git a/cmd/tuf/add_signature.go b/cmd/tuf/add_signature.go index 88ab9901..65087360 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 8738ae4c..137420f1 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 00000000..d79fb7da --- /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 6b7cb8f7..35f1657c 100644 --- a/repo.go +++ b/repo.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/hex" "encoding/json" + "errors" "fmt" "io" "path" @@ -734,33 +735,49 @@ 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 +// 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 -1, ErrInvalidRole{role, "only signing top-level metadata supported"} } keys, err := r.signersForRole(role) if err != nil { - return err + return -1, err } if len(keys) == 0 { - return ErrInsufficientKeys{roleFilename} + return 0, ErrInsufficientKeys{role} } for _, k := range keys { - sign.Sign(s, k) + if err = sign.Sign(signed, 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, ErrInsufficientKeys{role}) { + return ErrInsufficientKeys{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 } @@ -1532,7 +1549,7 @@ func (r *Repo) Payload(roleFilename string) ([]byte, error) { role := strings.TrimSuffix(roleFilename, ".json") if !roles.IsTopLevelRole(role) { // TODO: handle payloads with delegated roles - return nil, ErrInvalidRole{role, "Payload() only supports top-level roles"} + return nil, ErrInvalidRole{role, "only signing top-level metadata supported"} } s, err := r.SignedMeta(roleFilename) diff --git a/repo_test.go b/repo_test.go index edfe87ae..9ebdc8fd 100644 --- a/repo_test.go +++ b/repo_test.go @@ -2552,36 +2552,39 @@ func (rs *RepoSuite) TestAddOrUpdateSignatureWithDelegations(c *C) { c.Assert(r.Commit(), IsNil) } -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", "Payload() only supports top-level roles"}) - + 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) + } } From 2197d3bed009a66abfd34e0bb3ff90bfa4f3a8a5 Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Thu, 31 Mar 2022 09:03:22 -0400 Subject: [PATCH 04/10] 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 --- README.md | 61 +++++++++++++++++++ .../{add_signature.go => add_signatures.go} | 0 cmd/tuf/payload.go | 4 +- cmd/tuf/{sign_file.go => sign_payload.go} | 4 +- repo.go | 6 +- 5 files changed, 69 insertions(+), 6 deletions(-) rename cmd/tuf/{add_signature.go => add_signatures.go} (100%) rename cmd/tuf/{sign_file.go => sign_payload.go} (85%) diff --git a/README.md b/README.md index 2ba0a7b7..37c7d50b 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,27 @@ 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 ` + +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= ` + +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`. + +See also `tuf add-signatures`. + +#### `tuf add-signatures --signatures ` + + +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 @@ -229,6 +250,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 diff --git a/cmd/tuf/add_signature.go b/cmd/tuf/add_signatures.go similarity index 100% rename from cmd/tuf/add_signature.go rename to cmd/tuf/add_signatures.go diff --git a/cmd/tuf/payload.go b/cmd/tuf/payload.go index 087f2d54..8cc0c2ff 100644 --- a/cmd/tuf/payload.go +++ b/cmd/tuf/payload.go @@ -11,9 +11,7 @@ func init() { register("payload", cmdPayload, ` usage: tuf payload -Output the metadata file for a role in a ready-to-sign format. - -The output is canonicalized. +Outputs the metadata file for a role in a ready-to-sign (canonicalized) format. `) } diff --git a/cmd/tuf/sign_file.go b/cmd/tuf/sign_payload.go similarity index 85% rename from cmd/tuf/sign_file.go rename to cmd/tuf/sign_payload.go index d79fb7da..8da5642b 100644 --- a/cmd/tuf/sign_file.go +++ b/cmd/tuf/sign_payload.go @@ -14,9 +14,9 @@ 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. +Sign a file (outside of the TUF repo) using keys for the given role (from the TUF repo). -Typically, this will be the output of "tuf payload". +Typically, path will be the output of "tuf payload". `) } diff --git a/repo.go b/repo.go index 35f1657c..7fcc0a7d 100644 --- a/repo.go +++ b/repo.go @@ -735,7 +735,11 @@ func (r *Repo) setMeta(roleFilename string, meta interface{}) error { return r.local.SetMeta(roleFilename, b) } -// Use the keys associated with role to sign the payload in signed. +// SignPayload signs the payload in signed to sign the keys associated with role. +// +// It returns the total number of keys used for signing, 0 (along with +// ErrInsufficientKeys) if no keys were found, or -1 (along with an error) in +// error cases. func (r *Repo) SignPayload(role string, signed *data.Signed) (int, error) { if !roles.IsTopLevelRole(role) { return -1, ErrInvalidRole{role, "only signing top-level metadata supported"} From 23cbb2e28dbf8a32e0dc89c0b5c0fd18c9720887 Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Tue, 5 Apr 2022 09:23:09 -0700 Subject: [PATCH 05/10] docs: Point out where keys are stored in `sign-payload` docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 37c7d50b..a4b7c3f6 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,8 @@ See also `tuf sign-payload` and `tuf add-signatures`. #### `tuf sign-payload --role= ` -Sign a file (outside of the TUF repo) using keys for the given `role` (from the TUF repo). +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 the output of `tuf payload`. From 38544988c937b5c84b4cc32300f4215ddc0a5cfd Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Sun, 17 Apr 2022 19:39:03 -0400 Subject: [PATCH 06/10] fix: ensure that output is canonicalized --- repo.go | 14 ++++++++++---- repo_test.go | 9 +++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/repo.go b/repo.go index 7fcc0a7d..c2d0a3df 100644 --- a/repo.go +++ b/repo.go @@ -12,6 +12,7 @@ import ( "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" @@ -735,12 +736,12 @@ func (r *Repo) setMeta(roleFilename string, meta interface{}) error { return r.local.SetMeta(roleFilename, b) } -// SignPayload signs the payload in signed to sign the keys associated with role. +// 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 // ErrInsufficientKeys) if no keys were found, or -1 (along with an error) in // error cases. -func (r *Repo) SignPayload(role string, signed *data.Signed) (int, error) { +func (r *Repo) SignPayload(role string, payload *data.Signed) (int, error) { if !roles.IsTopLevelRole(role) { return -1, ErrInvalidRole{role, "only signing top-level metadata supported"} } @@ -753,7 +754,7 @@ func (r *Repo) SignPayload(role string, signed *data.Signed) (int, error) { return 0, ErrInsufficientKeys{role} } for _, k := range keys { - if err = sign.Sign(signed, k); err != nil { + if err = sign.Sign(payload, k); err != nil { return -1, err } } @@ -1561,5 +1562,10 @@ func (r *Repo) Payload(roleFilename string) ([]byte, error) { return nil, err } - return s.Signed, nil + p, err := cjson.EncodeCanonical(s.Signed) + if err != nil { + return nil, err + } + + return p, nil } diff --git a/repo_test.go b/repo_test.go index 9ebdc8fd..b337ac5f 100644 --- a/repo_test.go +++ b/repo_test.go @@ -1,6 +1,7 @@ package tuf import ( + "bytes" "crypto" "crypto/rand" "encoding/hex" @@ -2571,6 +2572,14 @@ func (rs *RepoSuite) TestOfflineFlow(c *C) { payload, err := r.Payload("root.json") c.Assert(err, IsNil) + root, err := r.SignedMeta("root.json") + c.Assert(err, IsNil) + rootCanonical, err := cjson.EncodeCanonical(root.Signed) + c.Assert(err, IsNil) + if !bytes.Equal(payload, rootCanonical) { + c.Fatalf("Payload(): not canonical.\n%s\n%s", string(payload), string(rootCanonical)) + } + // Sign the payload signed := data.Signed{Signed: payload} _, err = r.SignPayload("badrole", &signed) From 5610ecd12a485841f6882183699958e6d13a66ec Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Tue, 19 Apr 2022 09:15:50 -0400 Subject: [PATCH 07/10] style: rename ErrInsufficientKeys to ErrNoKeys --- errors.go | 6 +++--- repo.go | 9 ++++----- repo_test.go | 6 +++--- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/errors.go b/errors.go index e8120554..0051c439 100644 --- a/errors.go +++ b/errors.go @@ -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 { diff --git a/repo.go b/repo.go index c2d0a3df..d11def25 100644 --- a/repo.go +++ b/repo.go @@ -739,8 +739,7 @@ func (r *Repo) setMeta(roleFilename string, meta interface{}) error { // 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 -// ErrInsufficientKeys) if no keys were found, or -1 (along with an error) in -// error cases. +// 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) { if !roles.IsTopLevelRole(role) { return -1, ErrInvalidRole{role, "only signing top-level metadata supported"} @@ -751,7 +750,7 @@ func (r *Repo) SignPayload(role string, payload *data.Signed) (int, error) { return -1, err } if len(keys) == 0 { - return 0, ErrInsufficientKeys{role} + return 0, ErrNoKeys{role} } for _, k := range keys { if err = sign.Sign(payload, k); err != nil { @@ -769,8 +768,8 @@ func (r *Repo) Sign(roleFilename string) error { role := strings.TrimSuffix(roleFilename, ".json") numKeys, err := r.SignPayload(role, signed) - if errors.Is(err, ErrInsufficientKeys{role}) { - return ErrInsufficientKeys{roleFilename} + if errors.Is(err, ErrNoKeys{role}) { + return ErrNoKeys{roleFilename} } else if err != nil { return err } diff --git a/repo_test.go b/repo_test.go index b337ac5f..19409c85 100644 --- a/repo_test.go +++ b/repo_test.go @@ -639,8 +639,8 @@ func (rs *RepoSuite) TestSign(c *C) { 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"}) + // signing with no keys returns ErrNoKeys + c.Assert(r.Sign("root.json"), Equals, ErrNoKeys{"root.json"}) checkSigIDs := func(keyIDs ...string) { meta, err := local.GetMeta() @@ -2585,7 +2585,7 @@ func (rs *RepoSuite) TestOfflineFlow(c *C) { _, 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"}) + c.Assert(err, Equals, ErrNoKeys{"targets"}) numKeys, err := r.SignPayload("root", &signed) c.Assert(err, IsNil) c.Assert(numKeys, Equals, 1) From 4ec9a5431b8be767809c156c4cd7cf0488bff161 Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Tue, 19 Apr 2022 09:17:14 -0400 Subject: [PATCH 08/10] doc: minor `tuf sign-payload` clarifiation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a4b7c3f6..5ea8554f 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ See also `tuf sign-payload` and `tuf add-signatures`. 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 the output of `tuf payload`. +Typically, `path` will be a file containing the output of `tuf payload`. See also `tuf add-signatures`. From a0bb5102c41fd4543687fe978a0d4529e0fbe5e7 Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Tue, 19 Apr 2022 09:34:43 -0400 Subject: [PATCH 09/10] test: add client test for offline flow --- client/client_test.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/client/client_test.go b/client/client_test.go index 85fa57e1..2085dcd8 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -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) From 1748ea5f575a758f84a3ffbcafa390d2e5695fe4 Mon Sep 17 00:00:00 2001 From: Zachary Newman Date: Mon, 25 Apr 2022 09:33:13 -0400 Subject: [PATCH 10/10] test: fix tests after rebase --- repo.go | 10 ---------- repo_test.go | 4 +--- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/repo.go b/repo.go index d11def25..e4992fe3 100644 --- a/repo.go +++ b/repo.go @@ -741,10 +741,6 @@ func (r *Repo) setMeta(roleFilename string, meta interface{}) error { // 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) { - if !roles.IsTopLevelRole(role) { - return -1, ErrInvalidRole{role, "only signing top-level metadata supported"} - } - keys, err := r.signersForRole(role) if err != nil { return -1, err @@ -1550,12 +1546,6 @@ 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) { - // TODO: handle payloads with delegated roles - return nil, ErrInvalidRole{role, "only signing top-level metadata supported"} - } - s, err := r.SignedMeta(roleFilename) if err != nil { return nil, err diff --git a/repo_test.go b/repo_test.go index 19409c85..089cf8f6 100644 --- a/repo_test.go +++ b/repo_test.go @@ -2566,7 +2566,7 @@ func (rs *RepoSuite) TestOfflineFlow(c *C) { // Get the payload to sign _, err = r.Payload("badrole.json") - c.Assert(err, Equals, ErrInvalidRole{"badrole", "only signing top-level metadata supported"}) + c.Assert(err, Equals, ErrMissingMetadata{"badrole.json"}) _, err = r.Payload("root") c.Assert(err, Equals, ErrMissingMetadata{"root"}) payload, err := r.Payload("root.json") @@ -2582,8 +2582,6 @@ func (rs *RepoSuite) TestOfflineFlow(c *C) { // 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, ErrNoKeys{"targets"}) numKeys, err := r.SignPayload("root", &signed)