diff --git a/cmd/tuf/app/add-delegation.go b/cmd/tuf/app/add-delegation.go index 5a94cb9f..1d4e25c2 100644 --- a/cmd/tuf/app/add-delegation.go +++ b/cmd/tuf/app/add-delegation.go @@ -185,9 +185,9 @@ func DelegationCmd(ctx context.Context, directory, name, path string, terminatin if err != nil { return err } - signed, err := jsonMarshal(t) + signed, err := prepo.MarshalMetadata(t) if err != nil { return err } - return setSignedMeta(store, "targets.json", &data.Signed{Signatures: sigs, Signed: signed}) + return prepo.SetSignedMeta(store, "targets.json", &data.Signed{Signatures: sigs, Signed: signed}) } diff --git a/cmd/tuf/app/init.go b/cmd/tuf/app/init.go index 23f59e51..ce1437a1 100644 --- a/cmd/tuf/app/init.go +++ b/cmd/tuf/app/init.go @@ -16,7 +16,6 @@ package app import ( - "bytes" "context" "encoding/json" "flag" @@ -286,16 +285,8 @@ func InitCmd(ctx context.Context, directory, previous string, return setMetaWithSigKeyIDs(store, "root.json", root, maps.Keys(allRootKeys)) } -func setSignedMeta(store tuf.LocalStore, role string, s *data.Signed) error { - b, err := jsonMarshal(s) - if err != nil { - return err - } - return store.SetMeta(role, b) -} - func setMetaWithSigKeyIDs(store tuf.LocalStore, role string, meta interface{}, keyIDs []string) error { - signed, err := jsonMarshal(meta) + signed, err := prepo.MarshalMetadata(meta) if err != nil { return err } @@ -310,7 +301,7 @@ func setMetaWithSigKeyIDs(store tuf.LocalStore, role string, meta interface{}, k } - return setSignedMeta(store, role, &data.Signed{Signatures: emptySigs, Signed: signed}) + return prepo.SetSignedMeta(store, role, &data.Signed{Signatures: emptySigs, Signed: signed}) } func ClearEmptySignatures(store tuf.LocalStore, role string) error { @@ -328,22 +319,7 @@ func ClearEmptySignatures(store tuf.LocalStore, role string) error { sigs = append(sigs, signature) } - return setSignedMeta(store, role, &data.Signed{Signatures: sigs, Signed: signedMeta.Signed}) -} - -func jsonMarshal(v interface{}) ([]byte, error) { - // We don't need to canonically encode the payload in the store. - b, err := json.Marshal(v) - if err != nil { - return nil, err - } - - var out bytes.Buffer - if err := json.Indent(&out, b, "", "\t"); err != nil { - return nil, err - } - - return out.Bytes(), nil + return prepo.SetSignedMeta(store, role, &data.Signed{Signatures: sigs, Signed: signedMeta.Signed}) } func getKeysFromDir(dir string, deprecatedKeyFormat bool) ([]*data.PublicKey, error) { diff --git a/cmd/tuf/app/sign.go b/cmd/tuf/app/sign.go index 9a58d745..127cbec2 100644 --- a/cmd/tuf/app/sign.go +++ b/cmd/tuf/app/sign.go @@ -30,6 +30,7 @@ import ( csignature "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/root-signing/pkg/keys" "github.com/sigstore/root-signing/pkg/repo" + prepo "github.com/sigstore/root-signing/pkg/repo" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/options" cjson "github.com/tent/canonical-json-go" @@ -58,6 +59,7 @@ func Sign() *ffcli.Command { // TODO(https://github.com/sigstore/root-signing/issues/381): // This can be removed after v5 root-signing is complete. addDeprecatedKeyFormat = flagset.Bool("add-deprecated", false, "adds the deprecated ecdsa key format to associate signatures") + bumpVersion = flagset.Bool("bump-version", false, "bumps the version; useful for re-signing without changes") ) flagset.Var(&roles, "roles", "role(s) to sign") return &ffcli.Command{ @@ -84,7 +86,7 @@ func Sign() *ffcli.Command { if err != nil { return err } - return SignCmd(ctx, *repository, roles, signer, *addDeprecatedKeyFormat) + return SignCmd(ctx, *repository, roles, signer, *bumpVersion, *addDeprecatedKeyFormat) }, } } @@ -138,7 +140,7 @@ func getSigner(ctx context.Context, sk bool, keyRef string) (signature.Signer, e } func SignCmd(ctx context.Context, directory string, roles []string, signer signature.Signer, - addDeprecatedKeyFormat bool) error { + bumpVersion bool, addDeprecatedKeyFormat bool) error { store := tuf.FileSystemStore(directory, nil) if err := checkMetaForRole(store, roles); err != nil { @@ -146,6 +148,11 @@ func SignCmd(ctx context.Context, directory string, roles []string, signer signa } for _, name := range roles { + if bumpVersion { + if err := prepo.BumpMetadataVersion(store, name); err != nil { + return err + } + } if err := SignMeta(ctx, store, name+".json", signer, addDeprecatedKeyFormat); err != nil { return err } @@ -249,7 +256,7 @@ func SignMeta(ctx context.Context, store tuf.LocalStore, name string, signer sig strings.Join(keyIDs, ", "), name, roleSigningKeys) } - return setSignedMeta(store, name, &data.Signed{Signatures: sigs, Signed: s.Signed}) + return prepo.SetSignedMeta(store, name, &data.Signed{Signatures: sigs, Signed: s.Signed}) } // Pre-entries are defined when there are Signatures in the Signed metadata diff --git a/pkg/repo/repo.go b/pkg/repo/repo.go index a5ba28a3..ec7a9b44 100644 --- a/pkg/repo/repo.go +++ b/pkg/repo/repo.go @@ -16,6 +16,7 @@ package repo import ( + "bytes" "encoding/json" "errors" "fmt" @@ -302,3 +303,56 @@ func UpdateRoleKeys(repo *tuf.Repo, store tuf.LocalStore, role string, keys []*d } return nil } + +func MarshalMetadata(v interface{}) ([]byte, error) { + // We don't need to canonically encode the payload in the store. + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + + var out bytes.Buffer + if err := json.Indent(&out, b, "", "\t"); err != nil { + return nil, err + } + + return out.Bytes(), nil +} + +func SetSignedMeta(store tuf.LocalStore, role string, s *data.Signed) error { + b, err := MarshalMetadata(s) + if err != nil { + return err + } + return store.SetMeta(role, b) +} + +// BumpMetadataVersion increments the version of the manifest. +// Note: This does NOT increase expiration! +// This ONLY handles delegated targets roles! The repo.SetRootVersion, +// repo.SetTargetsVersion, repo.SetSnapshotVersion, or repo.SetTimestampVersion +// handle top-level metadata. +func BumpMetadataVersion(store tuf.LocalStore, name string) error { + for _, topName := range []string{"root", "targets", "snapshot", "timestamp"} { + if name == topName { + return fmt.Errorf("unsupported metadata version bump %s", topName) + } + } + manifest := fmt.Sprintf("%s.json", name) + s, err := GetSignedMeta(store, manifest) + if err != nil { + return err + } + targets := &data.Targets{} + if err := json.Unmarshal(s.Signed, targets); err != nil { + return err + } + targets.Version++ + + signed, err := MarshalMetadata(targets) + if err != nil { + return err + } + + return SetSignedMeta(store, manifest, &data.Signed{Signed: signed}) +} diff --git a/tests/e2e_test.go b/tests/e2e_test.go index d6df8cd7..730aec66 100644 --- a/tests/e2e_test.go +++ b/tests/e2e_test.go @@ -189,7 +189,7 @@ func TestSignRootTargets(t *testing.T) { // Sign root and targets. rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -254,7 +254,7 @@ func TestSnapshotUnvalidatedFails(t *testing.T) { // Now sign root and targets with 1/1 threshold key. rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -304,7 +304,7 @@ func TestPublishSuccess(t *testing.T) { // Sign root & targets rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -363,7 +363,7 @@ func TestRotateRootKey(t *testing.T) { // Sign root & targets with key 1 rootSigner1 := stack.getSigner(t, rootKeyRef1) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner1, app.DeprecatedEcdsaFormat); err != nil { + rootSigner1, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } rootTufKey1, err := keys.ConstructTufKey(ctx, rootSigner1, app.DeprecatedEcdsaFormat) @@ -438,7 +438,8 @@ func TestRotateRootKey(t *testing.T) { } // Sign root & targets - if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, rootSigner1, app.DeprecatedEcdsaFormat); err != nil { + if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, rootSigner1, + false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -470,7 +471,7 @@ func TestRotateTarget(t *testing.T) { // Sign root & targets rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -511,7 +512,7 @@ func TestRotateTarget(t *testing.T) { // Sign root & targets if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -559,7 +560,7 @@ func TestConsistentSnapshotFlip(t *testing.T) { // Sign root & targets rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -598,7 +599,7 @@ func TestConsistentSnapshotFlip(t *testing.T) { // Sign root & targets if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } // Sign snapshot and timestamp @@ -660,7 +661,7 @@ func TestSignWithEcdsaHexHSM(t *testing.T) { // Sign root rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root"}, - rootSigner, true); err != nil { + rootSigner, false, true); err != nil { t.Fatal(err) } @@ -720,13 +721,13 @@ func TestEcdsaHexToPEMMigration(t *testing.T) { // Just to make sure, try this with the PEM signer and expect failure rootSigner := stack.getSigner(t, rootKeyRef) - if err := app.SignCmd(ctx, stack.repoDir, []string{"root"}, rootSigner, false); err == nil { + if err := app.SignCmd(ctx, stack.repoDir, []string{"root"}, rootSigner, false, false); err == nil { t.Fatal("expected error signing with PEM key") } // Sign root with deprecated format signer. if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, deprecatedEcdsaFormat); err != nil { + rootSigner, false, deprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -841,7 +842,7 @@ func TestEcdsaHexToPEMMigration(t *testing.T) { } // Now sign with both key types. - if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, rootSigner, true); err != nil { + if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, rootSigner, false, true); err != nil { t.Fatal(err) } @@ -877,7 +878,7 @@ func TestSnapshotKeyRotate(t *testing.T) { // Sign root & targets with key 1 rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -920,7 +921,7 @@ func TestSnapshotKeyRotate(t *testing.T) { // Sign root & targets with key 1 if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -980,7 +981,7 @@ func TestProdTargetsConfig(t *testing.T) { // Sign root & targets rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -1041,7 +1042,7 @@ func TestDelegationsClearedOnInit(t *testing.T) { // Sign root & targets with key 1 rootSigner := stack.getSigner(t, rootKeyRef) if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, - rootSigner, app.DeprecatedEcdsaFormat); err != nil { + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { t.Fatal(err) } @@ -1060,3 +1061,66 @@ func TestDelegationsClearedOnInit(t *testing.T) { t.Errorf("Expected top-level targets delegations to be cleared") } } + +func TestSignWithVersionBump(t *testing.T) { + ctx := context.Background() + stack := newRepoTestStack(ctx, t) + stack.addTarget(t, "foo.txt", "abc", nil) + rootKeyRef := stack.genKey(t, true) + + // Initialize succeeds. + if err := app.InitCmd(ctx, stack.repoDir, "", 1, + stack.targetsConfig, stack.repoDir, stack.snapshotRef, stack.timestampRef, + app.DeprecatedEcdsaFormat); err != nil { + t.Fatal(err) + } + + // Add a delegation + delegationKeyRef := stack.genKey(t, false) + if err := app.DelegationCmd(ctx, stack.repoDir, + "delegation", "path/*", true, []string{delegationKeyRef}, ""); err != nil { + t.Fatal(err) + } + + // Sign root & targets with key 1 + rootSigner := stack.getSigner(t, rootKeyRef) + if err := app.SignCmd(ctx, stack.repoDir, []string{"root", "targets"}, + rootSigner, false, app.DeprecatedEcdsaFormat); err != nil { + t.Fatal(err) + } + // Sign delegation + dSigner := stack.getSigner(t, delegationKeyRef) + if err := app.SignCmd(ctx, stack.repoDir, []string{"delegation"}, + dSigner, false, app.DeprecatedEcdsaFormat); err != nil { + t.Fatal(err) + } + + // Sign snapshot and timestamp + stack.snapshot(t, app.DeprecatedEcdsaFormat) + stack.timestamp(t, app.DeprecatedEcdsaFormat) + stack.publish(t) + + if _, err := verifyTuf(t, stack.repoDir, stack.getManifest(t, "root.json")); err != nil { + t.Fatal(err) + } + checkMetadataVersion(t, stack.repoDir, []string{"delegation.json"}, 1) + + // Increment the delegation metadata + if err := app.SignCmd(ctx, stack.repoDir, []string{"delegation"}, + dSigner, true, app.DeprecatedEcdsaFormat); err != nil { + t.Fatal(err) + } + + // Sign snapshot and timestamp + stack.snapshot(t, app.DeprecatedEcdsaFormat) + stack.timestamp(t, app.DeprecatedEcdsaFormat) + stack.publish(t) + + // Verify with go-tuf + if _, err := verifyTuf(t, stack.repoDir, stack.getManifest(t, "root.json")); err != nil { + t.Fatal(err) + } + + // Check delegation version bump + checkMetadataVersion(t, stack.repoDir, []string{"delegation.json"}, 2) +} diff --git a/tests/utils.go b/tests/utils.go index ddb447b7..2ab5c9bc 100644 --- a/tests/utils.go +++ b/tests/utils.go @@ -141,7 +141,7 @@ func (s *repoTestStack) snapshot(t *testing.T, deprecated bool) { if err != nil { t.Fatal(err) } - if err := app.SignCmd(s.ctx, s.repoDir, []string{"snapshot"}, snapshotSigner, deprecated); err != nil { + if err := app.SignCmd(s.ctx, s.repoDir, []string{"snapshot"}, snapshotSigner, false, deprecated); err != nil { t.Fatal(err) } } @@ -154,7 +154,7 @@ func (s *repoTestStack) timestamp(t *testing.T, deprecated bool) { if err != nil { t.Fatal(err) } - if err := app.SignCmd(s.ctx, s.repoDir, []string{"timestamp"}, timestampSigner, deprecated); err != nil { + if err := app.SignCmd(s.ctx, s.repoDir, []string{"timestamp"}, timestampSigner, false, deprecated); err != nil { t.Fatal(err) } }