From fd8ac04979553e40795536c5c95e31696e9343fd Mon Sep 17 00:00:00 2001 From: Marina Moore Date: Tue, 19 Apr 2022 14:12:51 -0400 Subject: [PATCH] feat: Support delegated targets roles in repo writer (#175) * Support delegated targets roles in repo writer * Add back return err in writeTopLevelTargetWithExpires * Remove space * Move hash bin impl to pkg/targets * Fix build after rebase * Make AddTargetsWithDigest support delegated targets * Remove unused GetPublicKeys * Revert "Remove unused GetPublicKeys" This reverts commit 3daaeebce3318af575481c54e0359cb78d8363fd. * Fix typo * Easier hash bin usage when setting up delegations * Remove comment that ChangePassphrase doesn't support delegated roles * Update comments * Add API to reset delegations * clean up verifySignatures * Fix tests for ChangePassphrase * Fix comments * Deduplicate key IDs when setting up hash bins * Revert "Revert "Remove unused GetPublicKeys"" This reverts commit dbe6a4d014089267239026a33d1f6950a99f83b8. * Add warning about GetSigners. TODO: fix usage * Clean up some usage of setMetawithSigners * Remove setMetaWithSigners since it's easy to misuse * cleanup * cleanup * Cleanup usage of delegatorDBs * Simplify AddTargetsWithExpiresToPreferredRole * Remove debugging * Remove targetsMetaWithSigners * Fix comment * Turn FIXME into an issue * Fix comment position * Remove out-of-place comment * Add missing hyphen * Test delegations construction more rigorously * Support removing delegated targets files * Fix tests * Add more tests for delegation * Fix unchecked error * Clarify comment * Better naming * Support delegated targets in Sign and AddOrUpdateSignature * Add test for ResetTargetsDelegationsWithExpires * Improve naming of delegation methods * Update tests for int64 version numbers * Add tests for Sign and AddOrUpdateSignature with delegations * Remove unused functions in errors.go * Remove commented out test code * Add test to verify that adding a delegation to an existing role doesn't change signed targets * Fix SignMessage usage in test with new canonicalization pattern Co-authored-by: Ethan Lowman --- client/errors.go | 16 - errors.go | 8 + local_store.go | 6 +- repo.go | 785 +++++++++++++++++++++++++++++++++++++---------- repo_test.go | 691 ++++++++++++++++++++++++++++++++++++++++- verify/db.go | 9 +- 6 files changed, 1327 insertions(+), 188 deletions(-) diff --git a/client/errors.go b/client/errors.go index 44a36d24..0edd9578 100644 --- a/client/errors.go +++ b/client/errors.go @@ -3,8 +3,6 @@ package client import ( "errors" "fmt" - - "github.com/theupdateframework/go-tuf/verify" ) var ( @@ -49,20 +47,6 @@ func (e ErrMaxDelegations) Error() string { return fmt.Sprintf("tuf: max delegation of %d reached searching for %s with snapshot version %d", e.MaxDelegations, e.Target, e.SnapshotVersion) } -//lint:ignore U1000 unused -func isDecodeFailedWithErrRoleThreshold(err error) bool { - e, ok := err.(ErrDecodeFailed) - if !ok { - return false - } - return isErrRoleThreshold(e.Err) -} - -func isErrRoleThreshold(err error) bool { - _, ok := err.(verify.ErrRoleThreshold) - return ok -} - type ErrNotFound struct { File string } diff --git a/errors.go b/errors.go index 1a8ab9ff..09df0390 100644 --- a/errors.go +++ b/errors.go @@ -88,3 +88,11 @@ type ErrPassphraseRequired struct { func (e ErrPassphraseRequired) Error() string { return fmt.Sprintf("tuf: a passphrase is required to access the encrypted %s keys file", e.Role) } + +type ErrNoDelegatedTarget struct { + Path string +} + +func (e ErrNoDelegatedTarget) Error() string { + return fmt.Sprintf("tuf: no delegated target for path %s", e.Path) +} diff --git a/local_store.go b/local_store.go index 9456016a..375a1efc 100644 --- a/local_store.go +++ b/local_store.go @@ -14,7 +14,6 @@ import ( "github.com/theupdateframework/go-tuf/data" "github.com/theupdateframework/go-tuf/encrypted" - "github.com/theupdateframework/go-tuf/internal/roles" "github.com/theupdateframework/go-tuf/internal/sets" "github.com/theupdateframework/go-tuf/pkg/keys" "github.com/theupdateframework/go-tuf/util" @@ -43,6 +42,8 @@ type LocalStore interface { Commit(bool, map[string]int64, map[string]data.Hashes) error // GetSigners return a list of signers for a role. + // This may include revoked keys, so the signers should not + // be used without filtering. GetSigners(role string) ([]keys.Signer, error) // SaveSigner adds a signer to a role. @@ -222,8 +223,7 @@ func (f *fileSystemStore) stagedDir() string { } func isMetaFile(e os.DirEntry) (bool, error) { - name := e.Name() - if e.IsDir() || !(filepath.Ext(name) == ".json" && roles.IsTopLevelManifest(name)) { + if e.IsDir() || filepath.Ext(e.Name()) != ".json" { return false, nil } diff --git a/repo.go b/repo.go index 10d13af3..0553fd61 100644 --- a/repo.go +++ b/repo.go @@ -13,13 +13,20 @@ import ( "github.com/theupdateframework/go-tuf/data" "github.com/theupdateframework/go-tuf/internal/roles" + "github.com/theupdateframework/go-tuf/internal/sets" "github.com/theupdateframework/go-tuf/internal/signer" "github.com/theupdateframework/go-tuf/pkg/keys" + "github.com/theupdateframework/go-tuf/pkg/targets" "github.com/theupdateframework/go-tuf/sign" "github.com/theupdateframework/go-tuf/util" "github.com/theupdateframework/go-tuf/verify" ) +const ( + // The maximum number of delegations to visit while traversing the delegations graph. + defaultMaxDelegations = 32 +) + // topLevelMetadata determines the order signatures are verified when committing. var topLevelMetadata = []string{ "root.json", @@ -73,12 +80,15 @@ func (r *Repo) Init(consistentSnapshot bool) error { root.ConsistentSnapshot = consistentSnapshot // Set root version to 1 for a new root. root.Version = 1 - if err = r.setTopLevelMeta("root.json", root); err != nil { + if err = r.setMeta("root.json", root); err != nil { return err } - if err = r.writeTargetWithExpires(t, data.DefaultExpires("targets")); err != nil { + + t.Version = 1 + if err = r.setMeta("targets.json", t); err != nil { return err } + fmt.Println("Repository initialized") return nil } @@ -151,9 +161,9 @@ func (r *Repo) RootVersion() (int64, error) { } func (r *Repo) GetThreshold(keyRole string) (int, error) { - if !roles.IsTopLevelRole(keyRole) { - // Delegations are not currently supported, so return an error if this is not a - // top-level metadata file. + if roles.IsDelegatedTargetsRole(keyRole) { + // The signature threshold for a delegated targets role + // depends on the incoming delegation edge. return -1, ErrInvalidRole{keyRole, "only thresholds for top-level roles supported"} } root, err := r.root() @@ -169,9 +179,9 @@ func (r *Repo) GetThreshold(keyRole string) (int, error) { } func (r *Repo) SetThreshold(keyRole string, t int) error { - if !roles.IsTopLevelRole(keyRole) { - // Delegations are not currently supported, so return an error if this is not a - // top-level metadata file. + if roles.IsDelegatedTargetsRole(keyRole) { + // The signature threshold for a delegated targets role + // depends on the incoming delegation edge. return ErrInvalidRole{keyRole, "only thresholds for top-level roles supported"} } root, err := r.root() @@ -190,7 +200,7 @@ func (r *Repo) SetThreshold(keyRole string, t int) error { if !r.local.FileIsStaged("root.json") { root.Version++ } - return r.setTopLevelMeta("root.json", root) + return r.setMeta("root.json", root) } func (r *Repo) Targets() (data.TargetFiles, error) { @@ -207,7 +217,7 @@ func (r *Repo) SetTargetsVersion(v int64) error { return err } t.Version = v - return r.setTopLevelMeta("targets.json", t) + return r.setMeta("targets.json", t) } func (r *Repo) TargetsVersion() (int64, error) { @@ -224,7 +234,7 @@ func (r *Repo) SetTimestampVersion(v int64) error { return err } ts.Version = v - return r.setTopLevelMeta("timestamp.json", ts) + return r.setMeta("timestamp.json", ts) } func (r *Repo) TimestampVersion() (int64, error) { @@ -242,7 +252,7 @@ func (r *Repo) SetSnapshotVersion(v int64) error { } s.Version = v - return r.setTopLevelMeta("snapshot.json", s) + return r.setMeta("snapshot.json", s) } func (r *Repo) SnapshotVersion() (int64, error) { @@ -254,17 +264,21 @@ func (r *Repo) SnapshotVersion() (int64, error) { } func (r *Repo) topLevelTargets() (*data.Targets, error) { - targetsJSON, ok := r.meta["targets.json"] + return r.targets("targets") +} + +func (r *Repo) targets(metaName string) (*data.Targets, error) { + targetsJSON, ok := r.meta[metaName+".json"] if !ok { return data.NewTargets(), nil } s := &data.Signed{} if err := json.Unmarshal(targetsJSON, s); err != nil { - return nil, err + return nil, fmt.Errorf("error unmarshalling for targets %q: %w", metaName, err) } targets := &data.Targets{} if err := json.Unmarshal(s.Signed, targets); err != nil { - return nil, err + return nil, fmt.Errorf("error unmarshalling signed data for targets %q: %w", metaName, err) } return targets, nil } @@ -286,10 +300,6 @@ func (r *Repo) timestamp() (*data.Timestamp, error) { } func (r *Repo) ChangePassphrase(keyRole string) error { - if !roles.IsTopLevelRole(keyRole) { - return ErrInvalidRole{keyRole, "only support passphrases for top-level roles"} - } - if p, ok := r.local.(PassphraseChanger); ok { return p.ChangePassphrase(keyRole) } @@ -298,10 +308,16 @@ func (r *Repo) ChangePassphrase(keyRole string) error { } func (r *Repo) GenKey(role string) ([]string, error) { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + return r.GenKeyWithExpires(role, data.DefaultExpires(role)) } func (r *Repo) GenKeyWithExpires(keyRole string, expires time.Time) (keyids []string, err error) { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + signer, err := keys.GenerateEd25519Key() if err != nil { return []string{}, err @@ -315,11 +331,17 @@ func (r *Repo) GenKeyWithExpires(keyRole string, expires time.Time) (keyids []st } func (r *Repo) AddPrivateKey(role string, signer keys.Signer) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + return r.AddPrivateKeyWithExpires(role, signer, data.DefaultExpires(role)) } func (r *Repo) AddPrivateKeyWithExpires(keyRole string, signer keys.Signer, expires time.Time) error { - if !roles.IsTopLevelRole(keyRole) { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + if roles.IsDelegatedTargetsRole(keyRole) { return ErrInvalidRole{keyRole, "only support adding keys for top-level roles"} } @@ -327,6 +349,8 @@ func (r *Repo) AddPrivateKeyWithExpires(keyRole string, signer keys.Signer, expi return ErrInvalidExpires{expires} } + // Must add signer before adding verification key, so + // root.json can be signed when a new root key is added. if err := r.local.SaveSigner(keyRole, signer); err != nil { return err } @@ -339,10 +363,27 @@ func (r *Repo) AddPrivateKeyWithExpires(keyRole string, signer keys.Signer, expi } func (r *Repo) AddVerificationKey(keyRole string, pk *data.PublicKey) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + return r.AddVerificationKeyWithExpiration(keyRole, pk, data.DefaultExpires(keyRole)) } func (r *Repo) AddVerificationKeyWithExpiration(keyRole string, pk *data.PublicKey, expires time.Time) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + if roles.IsDelegatedTargetsRole(keyRole) { + return ErrInvalidRole{ + Role: keyRole, + Reason: "only top-level targets roles are supported", + } + } + + if !validExpires(expires) { + return ErrInvalidExpires{expires} + } + root, err := r.root() if err != nil { return err @@ -371,7 +412,7 @@ func (r *Repo) AddVerificationKeyWithExpiration(keyRole string, pk *data.PublicK root.Version++ } - return r.setTopLevelMeta("root.json", root) + return r.setMeta("root.json", root) } func validExpires(expires time.Time) bool { @@ -413,11 +454,17 @@ func (r *Repo) RootKeys() ([]*data.PublicKey, error) { } func (r *Repo) RevokeKey(role, id string) error { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + return r.RevokeKeyWithExpires(role, id, data.DefaultExpires("root")) } func (r *Repo) RevokeKeyWithExpires(keyRole, id string, expires time.Time) error { - if !roles.IsTopLevelRole(keyRole) { + // Not compatible with delegated targets roles, since delegated targets keys + // are associated with a delegation (edge), not a role (node). + + if roles.IsDelegatedTargetsRole(keyRole) { return ErrInvalidRole{keyRole, "only revocations for top-level roles supported"} } @@ -476,13 +523,150 @@ func (r *Repo) RevokeKeyWithExpires(keyRole, id string, expires time.Time) error root.Version++ } - err = r.setTopLevelMeta("root.json", root) + err = r.setMeta("root.json", root) if err == nil { fmt.Println("Revoked", keyRole, "key with ID", id, "in root metadata") } return err } +// AddDelegatedRole is equivalent to AddDelegatedRoleWithExpires, but +// with a default expiration time. +func (r *Repo) AddDelegatedRole(delegator string, delegatedRole data.DelegatedRole, keys []*data.PublicKey) error { + return r.AddDelegatedRoleWithExpires(delegator, delegatedRole, keys, data.DefaultExpires("targets")) +} + +// AddDelegatedRoleWithExpires adds a delegation from the delegator to the +// role specified in the role argument. Key IDs referenced in role.KeyIDs +// should have corresponding Key entries in the keys argument. New metadata is +// written with the given expiration time. +func (r *Repo) AddDelegatedRoleWithExpires(delegator string, delegatedRole data.DelegatedRole, keys []*data.PublicKey, expires time.Time) error { + expires = expires.Round(time.Second) + + t, err := r.targets(delegator) + if err != nil { + return fmt.Errorf("error getting delegator (%q) metadata: %w", delegator, err) + } + + if t.Delegations == nil { + t.Delegations = &data.Delegations{} + t.Delegations.Keys = make(map[string]*data.PublicKey) + } + + for _, keyID := range delegatedRole.KeyIDs { + for _, key := range keys { + if key.ContainsID(keyID) { + t.Delegations.Keys[keyID] = key + break + } + } + } + + for _, r := range t.Delegations.Roles { + if r.Name == delegatedRole.Name { + return fmt.Errorf("role: %s is already delegated to by %s", delegatedRole.Name, r.Name) + } + } + t.Delegations.Roles = append(t.Delegations.Roles, delegatedRole) + t.Expires = expires + + delegatorFile := delegator + ".json" + if !r.local.FileIsStaged(delegatorFile) { + t.Version++ + } + + err = r.setMeta(delegatorFile, t) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", delegatorFile, err) + } + + delegatee := delegatedRole.Name + dt, err := r.targets(delegatee) + if err != nil { + return fmt.Errorf("error getting delegatee (%q) metadata: %w", delegatee, err) + } + dt.Expires = expires + + delegateeFile := delegatee + ".json" + if !r.local.FileIsStaged(delegateeFile) { + dt.Version++ + } + + err = r.setMeta(delegateeFile, dt) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", delegateeFile, err) + } + + return nil +} + +// AddDelegatedRolesForPathHashBins is equivalent to +// AddDelegatedRolesForPathHashBinsWithExpires, but with a default +// expiration time. +func (r *Repo) AddDelegatedRolesForPathHashBins(delegator string, bins *targets.HashBins, keys []*data.PublicKey, threshold int) error { + return r.AddDelegatedRolesForPathHashBinsWithExpires(delegator, bins, keys, threshold, data.DefaultExpires("targets")) +} + +// AddDelegatedRolesForPathHashBinsWithExpires adds delegations to the +// delegator role for the given hash bins configuration. New metadata is +// written with the given expiration time. +func (r *Repo) AddDelegatedRolesForPathHashBinsWithExpires(delegator string, bins *targets.HashBins, keys []*data.PublicKey, threshold int, expires time.Time) error { + keyIDs := []string{} + for _, key := range keys { + keyIDs = append(keyIDs, key.IDs()...) + } + + n := bins.NumBins() + for i := uint64(0); i < n; i += 1 { + bin := bins.GetBin(i) + name := bin.RoleName() + err := r.AddDelegatedRoleWithExpires(delegator, data.DelegatedRole{ + Name: name, + KeyIDs: sets.DeduplicateStrings(keyIDs), + PathHashPrefixes: bin.HashPrefixes(), + Threshold: threshold, + }, keys, expires) + if err != nil { + return fmt.Errorf("error adding delegation from %v to %v: %w", delegator, name, err) + } + } + + return nil +} + +// ResetTargetsDelegation is equivalent to ResetTargetsDelegationsWithExpires +// with a default expiry time. +func (r *Repo) ResetTargetsDelegations(delegator string) error { + return r.ResetTargetsDelegationsWithExpires(delegator, data.DefaultExpires("targets")) +} + +// ResetTargetsDelegationsWithExpires removes all targets delegations from the +// given delegator role. New metadata is written with the given expiration +// time. +func (r *Repo) ResetTargetsDelegationsWithExpires(delegator string, expires time.Time) error { + t, err := r.targets(delegator) + if err != nil { + return fmt.Errorf("error getting delegator (%q) metadata: %w", delegator, err) + } + + t.Delegations = &data.Delegations{} + t.Delegations.Keys = make(map[string]*data.PublicKey) + + t.Expires = expires.Round(time.Second) + + delegatorFile := delegator + ".json" + if !r.local.FileIsStaged(delegatorFile) { + t.Version++ + } + + err = r.setMeta(delegatorFile, t) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", delegatorFile, err) + } + + return nil +} + func (r *Repo) jsonMarshal(v interface{}) ([]byte, error) { if r.prefix == "" && r.indent == "" { return json.Marshal(v) @@ -490,12 +674,55 @@ func (r *Repo) jsonMarshal(v interface{}) ([]byte, error) { return json.MarshalIndent(v, r.prefix, r.indent) } -func (r *Repo) setTopLevelMeta(roleFilename string, meta interface{}) error { - keys, err := r.getSortedSigningKeys(strings.TrimSuffix(roleFilename, ".json")) +func (r *Repo) dbsForRole(role string) ([]*verify.DB, error) { + dbs := []*verify.DB{} + + if roles.IsTopLevelRole(role) { + db, err := r.topLevelKeysDB() + if err != nil { + return nil, err + } + dbs = append(dbs, db) + } else { + ddbs, err := r.delegatorDBs(role) + if err != nil { + return nil, err + } + + dbs = append(dbs, ddbs...) + } + + return dbs, nil +} + +func (r *Repo) signersForRole(role string) ([]keys.Signer, error) { + dbs, err := r.dbsForRole(role) + if err != nil { + return nil, err + } + + signers := []keys.Signer{} + for _, db := range dbs { + ss, err := r.getSignersInDB(role, db) + if err != nil { + return nil, err + } + + signers = append(signers, ss...) + } + + return signers, nil +} + +func (r *Repo) setMeta(roleFilename string, meta interface{}) error { + role := strings.TrimSuffix(roleFilename, ".json") + + signers, err := r.signersForRole(role) if err != nil { return err } - s, err := sign.Marshal(meta, keys...) + + s, err := sign.Marshal(meta, signers...) if err != nil { return err } @@ -509,16 +736,13 @@ func (r *Repo) setTopLevelMeta(roleFilename string, meta interface{}) error { func (r *Repo) Sign(roleFilename string) error { role := strings.TrimSuffix(roleFilename, ".json") - if !roles.IsTopLevelRole(role) { - return ErrInvalidRole{role, "only signing top-level metadata supported"} - } s, err := r.SignedMeta(roleFilename) if err != nil { return err } - keys, err := r.getSortedSigningKeys(role) + keys, err := r.signersForRole(role) if err != nil { return err } @@ -545,20 +769,28 @@ func (r *Repo) Sign(roleFilename string) error { // The name must be a valid metadata file name, like root.json. func (r *Repo) AddOrUpdateSignature(roleFilename string, signature data.Signature) error { role := strings.TrimSuffix(roleFilename, ".json") - if !roles.IsTopLevelRole(role) { - return ErrInvalidRole{role, "only signing top-level metadata supported"} - } // Check key ID is in valid for the role. - db, err := r.topLevelKeysDB() + dbs, err := r.dbsForRole(role) if err != nil { return err } - roleData := db.GetRole(role) - if roleData == nil { - return ErrInvalidRole{role, "role missing from top-level keys"} + + if len(dbs) == 0 { + return ErrInvalidRole{role, "no trusted keys for role"} + } + + keyInDB := false + for _, db := range dbs { + roleData := db.GetRole(role) + if roleData == nil { + return ErrInvalidRole{role, "role is not in verifier DB"} + } + if roleData.ValidKey(signature.KeyID) { + keyInDB = true + } } - if !roleData.ValidKey(signature.KeyID) { + if !keyInDB { return verify.ErrInvalidKey } @@ -579,9 +811,11 @@ func (r *Repo) AddOrUpdateSignature(roleFilename string, signature data.Signatur // Check signature on signed meta. Ignore threshold errors as this may not be fully // signed. - if err := db.VerifySignatures(s, role); err != nil { - if _, ok := err.(verify.ErrRoleThreshold); !ok { - return err + for _, db := range dbs { + if err := db.VerifySignatures(s, role); err != nil { + if _, ok := err.(verify.ErrRoleThreshold); !ok { + return err + } } } @@ -594,46 +828,45 @@ func (r *Repo) AddOrUpdateSignature(roleFilename string, signature data.Signatur return r.local.SetMeta(roleFilename, b) } -// getSortedSigningKeys returns available signing keys, sorted by key ID. +// getSignersInDB returns available signing interfaces, sorted by key ID. // // Only keys contained in the keys db are returned (i.e. local keys which have // been revoked are omitted), except for the root role in which case all local // keys are returned (revoked root keys still need to sign new root metadata so // clients can verify the new root.json and update their keys db accordingly). -func (r *Repo) getSortedSigningKeys(name string) ([]keys.Signer, error) { - signingKeys, err := r.local.GetSigners(name) +func (r *Repo) getSignersInDB(roleName string, db *verify.DB) ([]keys.Signer, error) { + signers, err := r.local.GetSigners(roleName) if err != nil { return nil, err } - if name == "root" { - sorted := make([]keys.Signer, len(signingKeys)) - copy(sorted, signingKeys) + + if roleName == "root" { + sorted := make([]keys.Signer, len(signers)) + copy(sorted, signers) sort.Sort(signer.ByIDs(sorted)) return sorted, nil } - db, err := r.topLevelKeysDB() - if err != nil { - return nil, err - } - role := db.GetRole(name) + + role := db.GetRole(roleName) if role == nil { return nil, nil } if len(role.KeyIDs) == 0 { return nil, nil } - keys := make([]keys.Signer, 0, len(role.KeyIDs)) - for _, key := range signingKeys { - for _, id := range key.PublicData().IDs() { + + signersInDB := []keys.Signer{} + for _, s := range signers { + for _, id := range s.PublicData().IDs() { if _, ok := role.KeyIDs[id]; ok { - keys = append(keys, key) + signersInDB = append(signersInDB, s) } } } - sort.Sort(signer.ByIDs(keys)) + sort.Sort(signer.ByIDs(signersInDB)) - return keys, nil + return signersInDB, nil } // Used to retrieve the signable portion of the metadata when using an external signing tool. @@ -649,22 +882,141 @@ func (r *Repo) SignedMeta(roleFilename string) (*data.Signed, error) { return s, nil } +// delegatorDBs returns a list of key DBs for all incoming delegations. +func (r *Repo) delegatorDBs(delegateeRole string) ([]*verify.DB, error) { + delegatorDBs := []*verify.DB{} + for metaName := range r.meta { + if roles.IsTopLevelManifest(metaName) && metaName != "targets.json" { + continue + } + roleName := strings.TrimSuffix(metaName, ".json") + + t, err := r.targets(roleName) + if err != nil { + return nil, err + } + + if t.Delegations == nil { + continue + } + + delegatesToRole := false + for _, d := range t.Delegations.Roles { + if d.Name == delegateeRole { + delegatesToRole = true + break + } + } + if !delegatesToRole { + continue + } + + db, err := verify.NewDBFromDelegations(t.Delegations) + if err != nil { + return nil, err + } + + delegatorDBs = append(delegatorDBs, db) + } + + return delegatorDBs, nil +} + +// targetDelegationForPath finds the targets metadata for the role that should +// sign the given path. The final delegation that led to the returned target +// metadata is also returned. +// +// Since there may be multiple targets roles that are able to sign a specific +// path, we must choose which roles's metadata to return. If preferredRole is +// specified (non-empty string) and eligible to sign the given path by way of +// some delegation chain, targets metadata for that role is returned. If +// preferredRole is not specified (""), we return targets metadata for the +// final role visited in the depth-first delegation traversal. +func (r *Repo) targetDelegationForPath(path string, preferredRole string) (*data.Targets, *targets.Delegation, error) { + topLevelKeysDB, err := r.topLevelKeysDB() + if err != nil { + return nil, nil, err + } + + iterator, err := targets.NewDelegationsIterator(path, topLevelKeysDB) + if err != nil { + return nil, nil, err + } + d, ok := iterator.Next() + if !ok { + return nil, nil, ErrNoDelegatedTarget{Path: path} + } + + for i := 0; i < defaultMaxDelegations; i++ { + targetsMeta, err := r.targets(d.Delegatee.Name) + if err != nil { + return nil, nil, err + } + + if preferredRole != "" && d.Delegatee.Name == preferredRole { + // The preferredRole is eligible to sign for the given path, and we've + // found its metadata. Return it. + return targetsMeta, &d, nil + } + + if targetsMeta.Delegations != nil && len(targetsMeta.Delegations.Roles) > 0 { + db, err := verify.NewDBFromDelegations(targetsMeta.Delegations) + if err != nil { + return nil, nil, err + } + + // Add delegations to the iterator that are eligible to sign for the + // given path (there may be none). + iterator.Add(targetsMeta.Delegations.Roles, d.Delegatee.Name, db) + } + + next, ok := iterator.Next() + if !ok { // No more roles to traverse. + if preferredRole == "" { + // No preferredRole was given, so return metadata for the final role in the traversal. + return targetsMeta, &d, nil + } else { + // There are no more roles to traverse, so preferredRole is either an + // invalid role, or not eligible to sign the given path. + return nil, nil, ErrNoDelegatedTarget{Path: path} + } + } + + d = next + } + + return nil, nil, ErrNoDelegatedTarget{Path: path} +} + func (r *Repo) AddTarget(path string, custom json.RawMessage) error { return r.AddTargets([]string{path}, custom) } +func (r *Repo) AddTargetToPreferredRole(path string, custom json.RawMessage, preferredRole string) error { + return r.AddTargetsToPreferredRole([]string{path}, custom, preferredRole) +} + func (r *Repo) AddTargets(paths []string, custom json.RawMessage) error { - return r.AddTargetsWithExpires(paths, custom, data.DefaultExpires("targets")) + return r.AddTargetsToPreferredRole(paths, custom, "") +} + +func (r *Repo) AddTargetsToPreferredRole(paths []string, custom json.RawMessage, preferredRole string) error { + return r.AddTargetsWithExpiresToPreferredRole(paths, custom, data.DefaultExpires("targets"), preferredRole) } func (r *Repo) AddTargetsWithDigest(digest string, digestAlg string, length int64, path string, custom json.RawMessage) error { + // TODO: Rename this to AddTargetWithDigest + // https://github.com/theupdateframework/go-tuf/issues/242 + expires := data.DefaultExpires("targets") + path = util.NormalizeTarget(path) - // TODO: support delegated targets - t, err := r.topLevelTargets() + targetsMeta, delegation, err := r.targetDelegationForPath(path, "") if err != nil { return err } + // This is the targets role that needs to sign the target file. + targetsRoleName := delegation.Delegatee.Name meta := data.FileMeta{Length: length, Hashes: make(data.Hashes, 1)} meta.Hashes[digestAlg], err = hex.DecodeString(digest) @@ -676,13 +1028,28 @@ func (r *Repo) AddTargetsWithDigest(digest string, digestAlg string, length int6 // metadata if len(custom) > 0 { meta.Custom = &custom - } else if t, ok := t.Targets[path]; ok { + } else if t, ok := targetsMeta.Targets[path]; ok { meta.Custom = t.Custom } - t.Targets[path] = data.TargetFileMeta{FileMeta: meta} + // What does G2 mean? Copying and pasting this comment from elsewhere in this file. + // G2 -> we no longer desire any readers to ever observe non-prefix targets. + delete(targetsMeta.Targets, "/"+path) + targetsMeta.Targets[path] = data.TargetFileMeta{FileMeta: meta} + + targetsMeta.Expires = expires.Round(time.Second) + + manifestName := targetsRoleName + ".json" + if !r.local.FileIsStaged(manifestName) { + targetsMeta.Version++ + } + + err = r.setMeta(manifestName, targetsMeta) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", manifestName, err) + } - return r.writeTargetWithExpires(t, expires) + return nil } func (r *Repo) AddTargetWithExpires(path string, custom json.RawMessage, expires time.Time) error { @@ -690,57 +1057,98 @@ func (r *Repo) AddTargetWithExpires(path string, custom json.RawMessage, expires } func (r *Repo) AddTargetsWithExpires(paths []string, custom json.RawMessage, expires time.Time) error { + return r.AddTargetsWithExpiresToPreferredRole(paths, custom, expires, "") +} + +func (r *Repo) AddTargetWithExpiresToPreferredRole(path string, custom json.RawMessage, expires time.Time, preferredRole string) error { + return r.AddTargetsWithExpiresToPreferredRole([]string{path}, custom, expires, preferredRole) +} + +// AddTargetsWithExpiresToPreferredRole signs the staged targets at `paths`. +// +// If preferredRole is not the empty string, the target is added to the given +// role's manifest if delegations allow it. If delegations do not allow the +// preferredRole to sign the given path, an error is returned. +func (r *Repo) AddTargetsWithExpiresToPreferredRole(paths []string, custom json.RawMessage, expires time.Time, preferredRole string) error { if !validExpires(expires) { return ErrInvalidExpires{expires} } - t, err := r.topLevelTargets() - if err != nil { - return err - } normalizedPaths := make([]string, len(paths)) for i, path := range paths { normalizedPaths[i] = util.NormalizeTarget(path) } + + // As we iterate through staged targets files, we accumulate changes to their + // corresponding targets metadata. + updatedTargetsMeta := map[string]*data.Targets{} + if err := r.local.WalkStagedTargets(normalizedPaths, func(path string, target io.Reader) (err error) { - meta, err := util.GenerateTargetFileMeta(target, r.hashAlgorithms...) + originalMeta, delegation, err := r.targetDelegationForPath(path, preferredRole) + if err != nil { + return err + } + + // This is the targets role that needs to sign the target file. + targetsRoleName := delegation.Delegatee.Name + + targetsMeta := originalMeta + if tm, ok := updatedTargetsMeta[targetsRoleName]; ok { + // Metadata in updatedTargetsMeta overrides staged/commited metadata. + targetsMeta = tm + } + + fileMeta, err := util.GenerateTargetFileMeta(target, r.hashAlgorithms...) if err != nil { return err } - path = util.NormalizeTarget(path) - // if we have custom metadata, set it, otherwise maintain + // If we have custom metadata, set it, otherwise maintain // existing metadata if present if len(custom) > 0 { - meta.Custom = &custom - } else if t, ok := t.Targets[path]; ok { - meta.Custom = t.Custom + fileMeta.Custom = &custom + } else if tf, ok := targetsMeta.Targets[path]; ok { + fileMeta.Custom = tf.Custom } // G2 -> we no longer desire any readers to ever observe non-prefix targets. - delete(t.Targets, "/"+path) - t.Targets[path] = meta + delete(targetsMeta.Targets, "/"+path) + targetsMeta.Targets[path] = fileMeta + + updatedTargetsMeta[targetsRoleName] = targetsMeta + return nil }); err != nil { return err } - return r.writeTargetWithExpires(t, expires) -} -func (r *Repo) writeTargetWithExpires(t *data.Targets, expires time.Time) error { - t.Expires = expires.Round(time.Second) - if !r.local.FileIsStaged("targets.json") { - t.Version++ + if len(updatedTargetsMeta) == 0 { + // This is potentially unexpected behavior kept for backwards compatibility. + // See https://github.com/theupdateframework/go-tuf/issues/243 + t, err := r.topLevelTargets() + if err != nil { + return err + } + + updatedTargetsMeta["targets"] = t } - err := r.setTopLevelMeta("targets.json", t) - if err == nil && len(t.Targets) > 0 { - fmt.Println("Added/staged targets:") - for k := range t.Targets { - fmt.Println("*", k) + exp := expires.Round(time.Second) + for roleName, targetsMeta := range updatedTargetsMeta { + targetsMeta.Expires = exp + + manifestName := roleName + ".json" + if !r.local.FileIsStaged(manifestName) { + targetsMeta.Version++ + } + + err := r.setMeta(manifestName, targetsMeta) + if err != nil { + return fmt.Errorf("error setting metadata for %q: %w", manifestName, err) } } - return err + + return nil } func (r *Repo) RemoveTarget(path string) error { @@ -761,7 +1169,23 @@ func (r *Repo) RemoveTargetsWithExpires(paths []string, expires time.Time) error return ErrInvalidExpires{expires} } - t, err := r.topLevelTargets() + for metaName := range r.meta { + if metaName != "targets.json" && !roles.IsDelegatedTargetsManifest(metaName) { + continue + } + + err := r.removeTargetsWithExpiresFromMeta(metaName, paths, expires) + if err != nil { + return fmt.Errorf("could not remove %v from %v: %w", paths, metaName, err) + } + } + + return nil +} + +func (r *Repo) removeTargetsWithExpiresFromMeta(metaName string, paths []string, expires time.Time) error { + roleName := strings.TrimSuffix(metaName, ".json") + t, err := r.targets(roleName) if err != nil { return err } @@ -776,7 +1200,7 @@ func (r *Repo) RemoveTargetsWithExpires(paths []string, expires time.Time) error for _, path := range paths { path = util.NormalizeTarget(path) if _, ok := t.Targets[path]; !ok { - fmt.Println("The following target is not present:", path) + fmt.Printf("[%v] The following target is not present: %v\n", metaName, path) continue } removed = true @@ -790,23 +1214,23 @@ func (r *Repo) RemoveTargetsWithExpires(paths []string, expires time.Time) error } } t.Expires = expires.Round(time.Second) - if !r.local.FileIsStaged("targets.json") { + if !r.local.FileIsStaged(metaName) { t.Version++ } - err = r.setTopLevelMeta("targets.json", t) + err = r.setMeta(metaName, t) if err == nil { - fmt.Println("Removed targets:") + fmt.Printf("[%v] Removed targets:\n", metaName) for _, v := range removed_targets { fmt.Println("*", v) } if len(t.Targets) != 0 { - fmt.Println("Added/staged targets:") + fmt.Printf("[%v] Added/staged targets:\n", metaName) for k := range t.Targets { fmt.Println("*", k) } } else { - fmt.Println("There are no added/staged targets") + fmt.Printf("[%v] There are no added/staged targets\n", metaName) } } return err @@ -817,7 +1241,16 @@ func (r *Repo) Snapshot() error { } func (r *Repo) snapshotMetadata() []string { - return []string{"targets.json"} + ret := []string{"targets.json"} + + for name := range r.meta { + if !roles.IsVersionedManifest(name) && + roles.IsDelegatedTargetsManifest(name) { + ret = append(ret, name) + } + } + + return ret } func (r *Repo) SnapshotWithExpires(expires time.Time) error { @@ -829,13 +1262,9 @@ func (r *Repo) SnapshotWithExpires(expires time.Time) error { if err != nil { return err } - db, err := r.topLevelKeysDB() - if err != nil { - return err - } for _, metaName := range r.snapshotMetadata() { - if err := r.verifySignature(metaName, db); err != nil { + if err := r.verifySignatures(metaName); err != nil { return err } var err error @@ -848,7 +1277,7 @@ func (r *Repo) SnapshotWithExpires(expires time.Time) error { if !r.local.FileIsStaged("snapshot.json") { snapshot.Version++ } - err = r.setTopLevelMeta("snapshot.json", snapshot) + err = r.setMeta("snapshot.json", snapshot) if err == nil { fmt.Println("Staged snapshot.json metadata with expiration date:", snapshot.Expires) } @@ -864,11 +1293,7 @@ func (r *Repo) TimestampWithExpires(expires time.Time) error { return ErrInvalidExpires{expires} } - db, err := r.topLevelKeysDB() - if err != nil { - return err - } - if err := r.verifySignature("snapshot.json", db); err != nil { + if err := r.verifySignatures("snapshot.json"); err != nil { return err } timestamp, err := r.timestamp() @@ -884,7 +1309,7 @@ func (r *Repo) TimestampWithExpires(expires time.Time) error { timestamp.Version++ } - err = r.setTopLevelMeta("timestamp.json", timestamp) + err = r.setMeta("timestamp.json", timestamp) if err == nil { fmt.Println("Staged timestamp.json metadata with expiration date:", timestamp.Expires) } @@ -892,48 +1317,95 @@ func (r *Repo) TimestampWithExpires(expires time.Time) error { } func (r *Repo) fileVersions() (map[string]int64, error) { - root, err := r.root() - if err != nil { - return nil, err - } - targets, err := r.topLevelTargets() - if err != nil { - return nil, err - } - snapshot, err := r.snapshot() - if err != nil { - return nil, err - } versions := make(map[string]int64) - versions["root.json"] = root.Version - versions["targets.json"] = targets.Version - versions["snapshot.json"] = snapshot.Version + + for fileName := range r.meta { + if roles.IsVersionedManifest(fileName) { + continue + } + + roleName := strings.TrimSuffix(fileName, ".json") + + var version int64 + + switch roleName { + case "root": + root, err := r.root() + if err != nil { + return nil, err + } + version = root.Version + case "snapshot": + snapshot, err := r.snapshot() + if err != nil { + return nil, err + } + version = snapshot.Version + case "timestamp": + continue + default: + // Targets or delegated targets manifest. + targets, err := r.targets(roleName) + if err != nil { + return nil, err + } + + version = targets.Version + } + + versions[fileName] = version + } + return versions, nil } func (r *Repo) fileHashes() (map[string]data.Hashes, error) { hashes := make(map[string]data.Hashes) - timestamp, err := r.timestamp() - if err != nil { - return nil, err - } - snapshot, err := r.snapshot() - if err != nil { - return nil, err - } - if m, ok := snapshot.Meta["targets.json"]; ok { - hashes["targets.json"] = m.Hashes - } - if m, ok := timestamp.Meta["snapshot.json"]; ok { - hashes["snapshot.json"] = m.Hashes - } - t, err := r.topLevelTargets() - if err != nil { - return nil, err - } - for name, meta := range t.Targets { - hashes[path.Join("targets", name)] = meta.Hashes + + for fileName := range r.meta { + if roles.IsVersionedManifest(fileName) { + continue + } + + roleName := strings.TrimSuffix(fileName, ".json") + + switch roleName { + case "snapshot": + timestamp, err := r.timestamp() + if err != nil { + return nil, err + } + + if m, ok := timestamp.Meta[fileName]; ok { + hashes[fileName] = m.Hashes + } + case "timestamp": + continue + default: + snapshot, err := r.snapshot() + if err != nil { + return nil, err + } + if m, ok := snapshot.Meta[fileName]; ok { + hashes[fileName] = m.Hashes + } + + if roleName != "root" { + // Scalability issue: Commit/fileHashes loads all targets metadata into memory + // https://github.com/theupdateframework/go-tuf/issues/245 + t, err := r.targets(roleName) + if err != nil { + return nil, err + } + for name, m := range t.Targets { + hashes[path.Join("targets", name)] = m.Hashes + } + } + + } + } + return hashes, nil } @@ -988,13 +1460,8 @@ func (r *Repo) Commit() error { return fmt.Errorf("tuf: invalid snapshot.json in timestamp.json: %s", err) } - // verify all signatures are correct - db, err := r.topLevelKeysDB() - if err != nil { - return err - } for _, name := range topLevelMetadata { - if err := r.verifySignature(name, db); err != nil { + if err := r.verifySignatures(name); err != nil { return err } } @@ -1023,15 +1490,25 @@ func (r *Repo) Clean() error { return err } -func (r *Repo) verifySignature(roleFilename string, db *verify.DB) error { - s, err := r.SignedMeta(roleFilename) +func (r *Repo) verifySignatures(metaFilename string) error { + s, err := r.SignedMeta(metaFilename) if err != nil { return err } - role := strings.TrimSuffix(roleFilename, ".json") - if err := db.Verify(s, role, 0); err != nil { - return ErrInsufficientSignatures{roleFilename, err} + + role := strings.TrimSuffix(metaFilename, ".json") + + dbs, err := r.dbsForRole(role) + if err != nil { + return err } + + for _, db := range dbs { + if err := db.Verify(s, role, 0); err != nil { + return ErrInsufficientSignatures{metaFilename, err} + } + } + return nil } diff --git a/repo_test.go b/repo_test.go index 476e0b47..c71bf5f0 100644 --- a/repo_test.go +++ b/repo_test.go @@ -21,6 +21,7 @@ import ( "github.com/theupdateframework/go-tuf/encrypted" "github.com/theupdateframework/go-tuf/internal/sets" "github.com/theupdateframework/go-tuf/pkg/keys" + "github.com/theupdateframework/go-tuf/pkg/targets" "github.com/theupdateframework/go-tuf/util" "github.com/theupdateframework/go-tuf/verify" "golang.org/x/crypto/ed25519" @@ -165,8 +166,11 @@ func (rs *RepoSuite) TestInit(c *C) { c.Assert(root.ConsistentSnapshot, Equals, v) } - // Init() fails if targets have been added + // Add a target. + generateAndAddPrivateKey(c, r, "targets") c.Assert(r.AddTarget("foo.txt", nil), IsNil) + + // Init() fails if targets have been added c.Assert(r.Init(true), Equals, ErrInitNotAllowed) } @@ -632,7 +636,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"}) @@ -1386,16 +1390,20 @@ func (rs *RepoSuite) TestKeyPersistence(c *C) { c.Assert(insecureStore.SaveSigner("targets", signer), IsNil) assertKeys("targets", false, []*data.PrivateKey{privateKey}) + c.Assert(insecureStore.SaveSigner("foo", signer), IsNil) + assertKeys("foo", false, []*data.PrivateKey{privateKey}) + // Test changing the passphrase // 1. Create a secure store with a passphrase (create new object and temp folder so we discard any previous state) tmp = newTmpDir(c) store = FileSystemStore(tmp.path, testPassphraseFunc) - // 1.5. Changing passphrase only works for top-level roles. + // 1.5. Changing passphrase works for top-level and delegated roles. r, err := NewRepo(store) c.Assert(err, IsNil) - c.Assert(r.ChangePassphrase("foo"), DeepEquals, ErrInvalidRole{"foo", "only support passphrases for top-level roles"}) + c.Assert(r.ChangePassphrase("targets"), NotNil) + c.Assert(r.ChangePassphrase("foo"), NotNil) // 2. Test changing the passphrase when the keys file does not exist - should FAIL c.Assert(store.(PassphraseChanger).ChangePassphrase("root"), NotNil) @@ -1506,6 +1514,8 @@ func (rs *RepoSuite) TestCustomTargetMetadata(c *C) { r, err := NewRepo(local) c.Assert(err, IsNil) + generateAndAddPrivateKey(c, r, "targets") + custom := json.RawMessage(`{"foo":"bar"}`) assertCustomMeta := func(file string, custom *json.RawMessage) { t, err := r.topLevelTargets() @@ -1553,7 +1563,7 @@ func (rs *RepoSuite) TestUnknownKeyIDs(c *C) { c.Assert(root.Version, Equals, int64(1)) root.Keys["unknown-key-id"] = signer.PublicData() - r.setTopLevelMeta("root.json", root) + r.setMeta("root.json", root) // commit the metadata to the store. c.Assert(r.AddTargets([]string{}, nil), IsNil) @@ -1758,7 +1768,7 @@ func (rs *RepoSuite) TestBadAddOrUpdateSignatures(c *C) { c.Assert(r.AddOrUpdateSignature("targets.json", data.Signature{ KeyID: "foo", - Signature: nil}), Equals, ErrInvalidRole{"targets", "role missing from top-level keys"}) + Signature: nil}), Equals, ErrInvalidRole{"targets", "role is not in verifier DB"}) // generate root key offline and add as a verification key rootKey, err := keys.GenerateEd25519Key() @@ -1784,7 +1794,7 @@ func (rs *RepoSuite) TestBadAddOrUpdateSignatures(c *C) { for _, id := range rootKey.PublicData().IDs() { c.Assert(r.AddOrUpdateSignature("invalid_root.json", data.Signature{ KeyID: id, - Signature: rootSig}), Equals, ErrInvalidRole{"invalid_root", "only signing top-level metadata supported"}) + Signature: rootSig}), Equals, ErrInvalidRole{"invalid_root", "no trusted keys for role"}) } // add a root signature with an key ID that is for the targets role @@ -1865,5 +1875,672 @@ func (rs *RepoSuite) TestSignDigest(c *C) { c.Assert(err, IsNil) c.Assert(targets.Targets["sha256:bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"].FileMeta.Length, Equals, size) c.Assert(targets.Targets["sha256:bc11b176a293bb341a0f2d0d226f52e7fcebd186a7c4dfca5fc64f305f06b94c"].FileMeta.Hashes["sha256"], DeepEquals, hex_digest_bytes) +} + +func concat(ss ...[]string) []string { + ret := []string{} + for _, s := range ss { + ret = append(ret, s...) + } + return ret +} + +func checkSigKeyIDs(c *C, local LocalStore, fileToKeyIDs map[string][]string) { + metas, err := local.GetMeta() + c.Assert(err, IsNil) + + for f, keyIDs := range fileToKeyIDs { + meta, ok := metas[f] + c.Assert(ok, Equals, true, Commentf("meta file: %v", f)) + + s := &data.Signed{} + err = json.Unmarshal(meta, s) + c.Assert(err, IsNil) + + gotKeyIDs := []string{} + for _, sig := range s.Signatures { + gotKeyIDs = append(gotKeyIDs, sig.KeyID) + } + gotKeyIDs = sets.DeduplicateStrings(gotKeyIDs) + sort.Strings(gotKeyIDs) + + sort.Strings(keyIDs) + c.Assert(gotKeyIDs, DeepEquals, keyIDs) + } +} +func (rs *RepoSuite) TestDelegations(c *C) { + tmp := newTmpDir(c) + local := FileSystemStore(tmp.path, nil) + r, err := NewRepo(local) + c.Assert(err, IsNil) + + // Add one key to each role + genKey(c, r, "root") + targetsKeyIDs := genKey(c, r, "targets") + genKey(c, r, "snapshot") + genKey(c, r, "timestamp") + + // commit the metadata to the store. + c.Assert(r.AddTargets([]string{}, nil), IsNil) + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err := r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 1) + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(1)) + + checkSigKeyIDs(c, local, map[string][]string{ + "1.targets.json": targetsKeyIDs, + }) + + saveNewKey := func(role string) keys.Signer { + key, err := keys.GenerateEd25519Key() + c.Assert(err, IsNil) + + err = local.SaveSigner(role, key) + c.Assert(err, IsNil) + + return key + } + + // Delegate from targets -> role1 for A/*, B/* with one key, threshold 1. + role1ABKey := saveNewKey("role1") + role1AB := data.DelegatedRole{ + Name: "role1", + KeyIDs: role1ABKey.PublicData().IDs(), + Paths: []string{"A/*", "B/*"}, + Threshold: 1, + } + err = r.AddDelegatedRole("targets", role1AB, []*data.PublicKey{ + role1ABKey.PublicData(), + }) + c.Assert(err, IsNil) + + // Adding duplicate delegation should return an error. + err = r.AddDelegatedRole("targets", role1AB, []*data.PublicKey{ + role1ABKey.PublicData(), + }) + c.Assert(err, NotNil) + + // Delegate from targets -> role2 for C/*, D/* with three key, threshold 2. + role2CDKey1 := saveNewKey("role2") + role2CDKey2 := saveNewKey("role2") + role2CDKey3 := saveNewKey("role2") + role2CD := data.DelegatedRole{ + Name: "role2", + KeyIDs: concat( + role2CDKey1.PublicData().IDs(), + role2CDKey2.PublicData().IDs(), + role2CDKey3.PublicData().IDs(), + ), + Paths: []string{"C/*", "D/*"}, + Threshold: 2, + } + err = r.AddDelegatedRole("targets", role2CD, []*data.PublicKey{ + role2CDKey1.PublicData(), + role2CDKey2.PublicData(), + role2CDKey3.PublicData(), + }) + c.Assert(err, IsNil) + + // Delegate from role1 -> role2 for A/allium.txt with one key, threshold 1. + role1To2Key := saveNewKey("role2") + role1To2 := data.DelegatedRole{ + Name: "role2", + KeyIDs: role1To2Key.PublicData().IDs(), + Paths: []string{"A/allium.txt"}, + Threshold: 1, + Terminating: true, + } + err = r.AddDelegatedRole("role1", role1To2, []*data.PublicKey{ + role1To2Key.PublicData(), + }) + c.Assert(err, IsNil) + + checkDelegations := func(delegator string, delegatedRoles ...data.DelegatedRole) { + t, err := r.targets(delegator) + c.Assert(err, IsNil) + + // Check that delegated roles are copied verbatim. + c.Assert(t.Delegations.Roles, DeepEquals, delegatedRoles) + + // Check that public keys match key IDs in roles. + expectedKeyIDs := []string{} + for _, dr := range delegatedRoles { + expectedKeyIDs = append(expectedKeyIDs, dr.KeyIDs...) + } + expectedKeyIDs = sets.DeduplicateStrings(expectedKeyIDs) + sort.Strings(expectedKeyIDs) + + gotKeyIDs := []string{} + for _, k := range t.Delegations.Keys { + gotKeyIDs = append(gotKeyIDs, k.IDs()...) + } + gotKeyIDs = sets.DeduplicateStrings(gotKeyIDs) + sort.Strings(gotKeyIDs) + + c.Assert(gotKeyIDs, DeepEquals, expectedKeyIDs) + } + + checkDelegations("targets", role1AB, role2CD) + checkDelegations("role1", role1To2) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err = r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 3) + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(2)) + c.Assert(snapshot.Meta["role1.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["role2.json"].Version, Equals, int64(1)) + + checkSigKeyIDs(c, local, map[string][]string{ + "2.targets.json": targetsKeyIDs, + "1.role1.json": role1ABKey.PublicData().IDs(), + "1.role2.json": concat( + role2CDKey1.PublicData().IDs(), + role2CDKey2.PublicData().IDs(), + role2CDKey3.PublicData().IDs(), + role1To2Key.PublicData().IDs(), + ), + }) + + // Add a variety of targets. + files := map[string]string{ + // targets.json + "potato.txt": "potatoes can be starchy or waxy", + // role1.json + "A/apple.txt": "apples are sometimes red", + "B/banana.txt": "bananas are yellow and sometimes brown", + // role2.json + "C/clementine.txt": "clementines are a citrus fruit", + "D/durian.txt": "durians are spiky", + "A/allium.txt": "alliums include garlic and leeks", + } + for name, content := range files { + tmp.writeStagedTarget(name, content) + c.Assert(r.AddTarget(name, nil), IsNil) + } + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err = r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 3) + // All roles should have new targets. + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(3)) + c.Assert(snapshot.Meta["role1.json"].Version, Equals, int64(2)) + c.Assert(snapshot.Meta["role2.json"].Version, Equals, int64(2)) + + checkSigKeyIDs(c, local, map[string][]string{ + "3.targets.json": targetsKeyIDs, + "2.role1.json": role1ABKey.PublicData().IDs(), + "2.role2.json": concat( + role2CDKey1.PublicData().IDs(), + role2CDKey2.PublicData().IDs(), + role2CDKey3.PublicData().IDs(), + role1To2Key.PublicData().IDs(), + ), + }) + + // Check that the given targets role has signed for the given filenames, with + // the correct file metadata. + checkTargets := func(role string, filenames ...string) { + t, err := r.targets(role) + c.Assert(err, IsNil) + c.Assert(t.Targets, HasLen, len(filenames)) + + for _, fn := range filenames { + content := files[fn] + + fm, err := util.GenerateTargetFileMeta(strings.NewReader(content)) + c.Assert(err, IsNil) + + c.Assert(util.TargetFileMetaEqual(t.Targets[fn], fm), IsNil) + } + } + + checkTargets("targets", "potato.txt") + checkTargets("role1", "A/apple.txt", "B/banana.txt") + checkTargets("role2", "C/clementine.txt", "D/durian.txt", "A/allium.txt") + + // Test AddTargetToPreferredRole. + // role2 is the default signer for A/allium.txt, but role1 is also eligible + // for A/*.txt according to the delegation from the top-level targets role. + c.Assert(r.RemoveTarget("A/allium.txt"), IsNil) + tmp.writeStagedTarget("A/allium.txt", files["A/allium.txt"]) + c.Assert(r.AddTargetToPreferredRole("A/allium.txt", nil, "role1"), IsNil) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err = r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 3) + // Only role1 and role2 should have bumped versions. + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(3)) + c.Assert(snapshot.Meta["role1.json"].Version, Equals, int64(3)) + c.Assert(snapshot.Meta["role2.json"].Version, Equals, int64(3)) + + checkSigKeyIDs(c, local, map[string][]string{ + "3.targets.json": targetsKeyIDs, + "3.role1.json": role1ABKey.PublicData().IDs(), + "3.role2.json": concat( + role2CDKey1.PublicData().IDs(), + role2CDKey2.PublicData().IDs(), + role2CDKey3.PublicData().IDs(), + role1To2Key.PublicData().IDs(), + ), + }) + + // role1 now signs A/allium.txt. + checkTargets("targets", "potato.txt") + checkTargets("role1", "A/apple.txt", "B/banana.txt", "A/allium.txt") + checkTargets("role2", "C/clementine.txt", "D/durian.txt") + + // Remove the delegation from role1 to role2. + c.Assert(r.ResetTargetsDelegations("role1"), IsNil) + checkDelegations("targets", role1AB, role2CD) + checkDelegations("role1") + + // Try to sign A/allium.txt with role2. + // It should fail since we removed the role1 -> role2 delegation. + c.Assert(r.RemoveTarget("A/allium.txt"), IsNil) + tmp.writeStagedTarget("A/allium.txt", files["A/allium.txt"]) + c.Assert(r.AddTargetToPreferredRole("A/allium.txt", nil, "role2"), Equals, ErrNoDelegatedTarget{Path: "A/allium.txt"}) + + // Try to sign A/allium.txt with the default role (role1). + c.Assert(r.AddTarget("A/allium.txt", nil), IsNil) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err = r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 3) + // Only role1 should have a bumped version. + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(3)) + c.Assert(snapshot.Meta["role1.json"].Version, Equals, int64(4)) + c.Assert(snapshot.Meta["role2.json"].Version, Equals, int64(3)) + + checkSigKeyIDs(c, local, map[string][]string{ + "3.targets.json": targetsKeyIDs, + "4.role1.json": role1ABKey.PublicData().IDs(), + "3.role2.json": concat( + // Metadata (and therefore signers) for role2.json shouldn't have + // changed, even though we revoked role1To2Key. Clients verify the + // signature using keys specified by 4.role1.json, so role1To2Key + // shouldn't contribute to the threshold. + role2CDKey1.PublicData().IDs(), + role2CDKey2.PublicData().IDs(), + role2CDKey3.PublicData().IDs(), + role1To2Key.PublicData().IDs(), + ), + }) + + // Re-sign target signed by role2 to test that role1To2Key is not used going + // forward. + c.Assert(r.RemoveTarget("C/clementine.txt"), IsNil) + tmp.writeStagedTarget("C/clementine.txt", files["C/clementine.txt"]) + c.Assert(r.AddTarget("C/clementine.txt", nil), IsNil) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err = r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 3) + // Only role2 should have a bumped version. + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(3)) + c.Assert(snapshot.Meta["role1.json"].Version, Equals, int64(4)) + c.Assert(snapshot.Meta["role2.json"].Version, Equals, int64(4)) + + checkSigKeyIDs(c, local, map[string][]string{ + "3.targets.json": targetsKeyIDs, + "4.role1.json": role1ABKey.PublicData().IDs(), + "4.role2.json": concat( + role2CDKey1.PublicData().IDs(), + role2CDKey2.PublicData().IDs(), + role2CDKey3.PublicData().IDs(), + // Note that role1To2Key no longer signs since the role1 -> role2 + // delegation was removed. + ), + }) + + // Targets should still be signed by the same roles. + checkTargets("targets", "potato.txt") + checkTargets("role1", "A/apple.txt", "B/banana.txt", "A/allium.txt") + checkTargets("role2", "C/clementine.txt", "D/durian.txt") + + // Add back the role1 -> role2 delegation, and verify that it doesn't change + // existing targets in role2.json. + err = r.AddDelegatedRole("role1", role1To2, []*data.PublicKey{ + role1To2Key.PublicData(), + }) + c.Assert(err, IsNil) + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err = r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 3) + // Both role1 and role2 should have a bumped version. + // role1 is bumped because the delegations changed. + // role2 is only bumped because its expiration is bumped. + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(3)) + c.Assert(snapshot.Meta["role1.json"].Version, Equals, int64(5)) + c.Assert(snapshot.Meta["role2.json"].Version, Equals, int64(5)) + + checkTargets("targets", "potato.txt") + checkTargets("role1", "A/apple.txt", "B/banana.txt", "A/allium.txt") + checkTargets("role2", "C/clementine.txt", "D/durian.txt") +} + +func (rs *RepoSuite) TestHashBinDelegations(c *C) { + tmp := newTmpDir(c) + local := FileSystemStore(tmp.path, nil) + r, err := NewRepo(local) + c.Assert(err, IsNil) + + // Add one key to each role + genKey(c, r, "root") + targetsKeyIDs := genKey(c, r, "targets") + genKey(c, r, "snapshot") + genKey(c, r, "timestamp") + + hb, err := targets.NewHashBins("bins_", 3) + if err != nil { + c.Assert(err, IsNil) + } + + // Generate key for the intermediate bins role. + binsKey, err := keys.GenerateEd25519Key() + c.Assert(err, IsNil) + err = local.SaveSigner("bins", binsKey) + c.Assert(err, IsNil) + + // Generate key for the leaf bins role. + leafKey, err := keys.GenerateEd25519Key() + c.Assert(err, IsNil) + for i := uint64(0); i < hb.NumBins(); i++ { + b := hb.GetBin(i) + err = local.SaveSigner(b.RoleName(), leafKey) + if err != nil { + c.Assert(err, IsNil) + } + } + + err = r.AddDelegatedRole("targets", data.DelegatedRole{ + Name: "bins", + KeyIDs: binsKey.PublicData().IDs(), + Paths: []string{"*.txt"}, + Threshold: 1, + }, []*data.PublicKey{ + binsKey.PublicData(), + }) + c.Assert(err, IsNil) + + err = r.AddDelegatedRolesForPathHashBins("bins", hb, []*data.PublicKey{leafKey.PublicData()}, 1) + c.Assert(err, IsNil) + targets, err := r.targets("bins") + c.Assert(err, IsNil) + c.Assert(targets.Delegations.Roles, HasLen, 8) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + tmp.writeStagedTarget("foo.txt", "foo") + err = r.AddTarget("foo.txt", nil) + c.Assert(err, IsNil) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err := r.snapshot() + c.Assert(err, IsNil) + // 1 targets.json, 1 bins.json, 8 bins_*.json. + c.Assert(snapshot.Meta, HasLen, 10) + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["bins.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["bins_0-1.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["bins_2-3.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["bins_4-5.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["bins_6-7.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["bins_8-9.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["bins_a-b.json"].Version, Equals, int64(1)) + c.Assert(snapshot.Meta["bins_c-d.json"].Version, Equals, int64(2)) + c.Assert(snapshot.Meta["bins_e-f.json"].Version, Equals, int64(1)) + + targets, err = r.targets("bins_c-d") + c.Assert(err, IsNil) + c.Assert(targets.Targets, HasLen, 1) + + checkSigKeyIDs(c, local, map[string][]string{ + "targets.json": targetsKeyIDs, + "1.bins.json": binsKey.PublicData().IDs(), + "1.bins_0-1.json": leafKey.PublicData().IDs(), + "1.bins_2-3.json": leafKey.PublicData().IDs(), + "1.bins_4-5.json": leafKey.PublicData().IDs(), + "1.bins_6-7.json": leafKey.PublicData().IDs(), + "1.bins_8-9.json": leafKey.PublicData().IDs(), + "1.bins_a-b.json": leafKey.PublicData().IDs(), + "1.bins_c-d.json": leafKey.PublicData().IDs(), + "2.bins_c-d.json": leafKey.PublicData().IDs(), + "1.bins_e-f.json": leafKey.PublicData().IDs(), + }) +} + +func (rs *RepoSuite) TestResetTargetsDelegationsWithExpires(c *C) { + tmp := newTmpDir(c) + local := FileSystemStore(tmp.path, nil) + r, err := NewRepo(local) + c.Assert(err, IsNil) + + // Add one key to each role + genKey(c, r, "root") + targetsKeyIDs := genKey(c, r, "targets") + genKey(c, r, "snapshot") + genKey(c, r, "timestamp") + + // commit the metadata to the store. + c.Assert(r.AddTargets([]string{}, nil), IsNil) + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err := r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 1) + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(1)) + + checkSigKeyIDs(c, local, map[string][]string{ + "1.targets.json": targetsKeyIDs, + }) + + role1Key, err := keys.GenerateEd25519Key() + c.Assert(err, IsNil) + + err = local.SaveSigner("role1", role1Key) + c.Assert(err, IsNil) + + // Delegate from targets -> role1 for A/*, B/* with one key, threshold 1. + role1 := data.DelegatedRole{ + Name: "role1", + KeyIDs: role1Key.PublicData().IDs(), + Paths: []string{"A/*", "B/*"}, + Threshold: 1, + } + err = r.AddDelegatedRole("targets", role1, []*data.PublicKey{ + role1Key.PublicData(), + }) + c.Assert(err, IsNil) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err = r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 2) + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(2)) + c.Assert(snapshot.Meta["role1.json"].Version, Equals, int64(1)) + + checkSigKeyIDs(c, local, map[string][]string{ + "1.targets.json": targetsKeyIDs, + "targets.json": targetsKeyIDs, + "1.role1.json": role1Key.PublicData().IDs(), + "role1.json": role1Key.PublicData().IDs(), + }) + + c.Assert(r.ResetTargetsDelegations("targets"), IsNil) + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) + + snapshot, err = r.snapshot() + c.Assert(err, IsNil) + c.Assert(snapshot.Meta, HasLen, 2) + c.Assert(snapshot.Meta["targets.json"].Version, Equals, int64(3)) + c.Assert(snapshot.Meta["role1.json"].Version, Equals, int64(1)) + + checkSigKeyIDs(c, local, map[string][]string{ + "2.targets.json": targetsKeyIDs, + "targets.json": targetsKeyIDs, + "1.role1.json": role1Key.PublicData().IDs(), + "role1.json": role1Key.PublicData().IDs(), + }) +} + +func (rs *RepoSuite) TestSignWithDelegations(c *C) { + tmp := newTmpDir(c) + local := FileSystemStore(tmp.path, nil) + r, err := NewRepo(local) + c.Assert(err, IsNil) + + // Add one key to each role + genKey(c, r, "root") + genKey(c, r, "targets") + genKey(c, r, "snapshot") + genKey(c, r, "timestamp") + + role1Key, err := keys.GenerateEd25519Key() + c.Assert(err, IsNil) + + role1 := data.DelegatedRole{ + Name: "role1", + KeyIDs: role1Key.PublicData().IDs(), + Paths: []string{"A/*", "B/*"}, + Threshold: 1, + } + err = r.AddDelegatedRole("targets", role1, []*data.PublicKey{ + role1Key.PublicData(), + }) + c.Assert(err, IsNil) + + // targets.json should be signed, but role1.json is not signed because there + // is no key in the local store. + m, err := local.GetMeta() + c.Assert(err, IsNil) + targetsMeta := &data.Signed{} + c.Assert(json.Unmarshal(m["targets.json"], targetsMeta), IsNil) + c.Assert(len(targetsMeta.Signatures), Equals, 1) + role1Meta := &data.Signed{} + c.Assert(json.Unmarshal(m["role1.json"], role1Meta), IsNil) + c.Assert(len(role1Meta.Signatures), Equals, 0) + + c.Assert(r.Snapshot(), DeepEquals, ErrInsufficientSignatures{"role1.json", verify.ErrNoSignatures}) + + // Sign role1.json. + c.Assert(local.SaveSigner("role1", role1Key), IsNil) + c.Assert(r.Sign("role1.json"), IsNil) + + m, err = local.GetMeta() + c.Assert(err, IsNil) + targetsMeta = &data.Signed{} + c.Assert(json.Unmarshal(m["targets.json"], targetsMeta), IsNil) + c.Assert(len(targetsMeta.Signatures), Equals, 1) + role1Meta = &data.Signed{} + c.Assert(json.Unmarshal(m["role1.json"], role1Meta), IsNil) + c.Assert(len(role1Meta.Signatures), Equals, 1) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) +} + +func (rs *RepoSuite) TestAddOrUpdateSignatureWithDelegations(c *C) { + tmp := newTmpDir(c) + local := FileSystemStore(tmp.path, nil) + r, err := NewRepo(local) + c.Assert(err, IsNil) + + // Add one key to each role + genKey(c, r, "root") + genKey(c, r, "targets") + genKey(c, r, "snapshot") + genKey(c, r, "timestamp") + + role1Key, err := keys.GenerateEd25519Key() + c.Assert(err, IsNil) + + role1 := data.DelegatedRole{ + Name: "role1", + KeyIDs: role1Key.PublicData().IDs(), + Paths: []string{"A/*", "B/*"}, + Threshold: 1, + } + err = r.AddDelegatedRole("targets", role1, []*data.PublicKey{ + role1Key.PublicData(), + }) + c.Assert(err, IsNil) + + // targets.json should be signed, but role1.json is not signed because there + // is no key in the local store. + m, err := local.GetMeta() + c.Assert(err, IsNil) + targetsMeta := &data.Signed{} + c.Assert(json.Unmarshal(m["targets.json"], targetsMeta), IsNil) + c.Assert(len(targetsMeta.Signatures), Equals, 1) + role1Meta := &data.Signed{} + c.Assert(json.Unmarshal(m["role1.json"], role1Meta), IsNil) + c.Assert(len(role1Meta.Signatures), Equals, 0) + + c.Assert(r.Snapshot(), DeepEquals, ErrInsufficientSignatures{"role1.json", verify.ErrNoSignatures}) + + // Sign role1.json. + canonical, err := cjson.EncodeCanonical(role1Meta.Signed) + c.Assert(err, IsNil) + sig, err := role1Key.SignMessage(canonical) + c.Assert(err, IsNil) + err = r.AddOrUpdateSignature("role1.json", data.Signature{ + KeyID: role1Key.PublicData().IDs()[0], + Signature: sig, + }) + c.Assert(err, IsNil) + + m, err = local.GetMeta() + c.Assert(err, IsNil) + targetsMeta = &data.Signed{} + c.Assert(json.Unmarshal(m["targets.json"], targetsMeta), IsNil) + c.Assert(len(targetsMeta.Signatures), Equals, 1) + role1Meta = &data.Signed{} + c.Assert(json.Unmarshal(m["role1.json"], role1Meta), IsNil) + c.Assert(len(role1Meta.Signatures), Equals, 1) + + c.Assert(r.Snapshot(), IsNil) + c.Assert(r.Timestamp(), IsNil) + c.Assert(r.Commit(), IsNil) } diff --git a/verify/db.go b/verify/db.go index a14a5149..657c2c55 100644 --- a/verify/db.go +++ b/verify/db.go @@ -40,7 +40,7 @@ func NewDBFromDelegations(d *data.Delegations) (*DB, error) { return nil, ErrInvalidDelegatedRole } role := &data.Role{Threshold: r.Threshold, KeyIDs: r.KeyIDs} - if err := db.addRole(r.Name, role); err != nil { + if err := db.AddRole(r.Name, role); err != nil { return nil, err } } @@ -65,13 +65,6 @@ func (db *DB) AddKey(id string, k *data.PublicKey) error { } func (db *DB) AddRole(name string, r *data.Role) error { - if !roles.IsTopLevelRole(name) { - return ErrInvalidRole - } - return db.addRole(name, r) -} - -func (db *DB) addRole(name string, r *data.Role) error { if r.Threshold < 1 { return ErrInvalidThreshold }