package client import ( "encoding/json" "fmt" "path/filepath" "github.com/sirupsen/logrus" "github.com/docker/notary" "github.com/docker/notary/client/changelist" store "github.com/docker/notary/storage" "github.com/docker/notary/tuf/data" "github.com/docker/notary/tuf/utils" ) // AddDelegation creates changelist entries to add provided delegation public keys and paths. // This method composes AddDelegationRoleAndKeys and AddDelegationPaths (each creates one changelist if called). func (r *NotaryRepository) AddDelegation(name string, delegationKeys []data.PublicKey, paths []string) error { if len(delegationKeys) > 0 { err := r.AddDelegationRoleAndKeys(name, delegationKeys) if err != nil { return err } } if len(paths) > 0 { err := r.AddDelegationPaths(name, paths) if err != nil { return err } } return nil } // AddDelegationRoleAndKeys creates a changelist entry to add provided delegation public keys. // This method is the simplest way to create a new delegation, because the delegation must have at least // one key upon creation to be valid since we will reject the changelist while validating the threshold. func (r *NotaryRepository) AddDelegationRoleAndKeys(name string, delegationKeys []data.PublicKey) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err } defer cl.Close() logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`, name, notary.MinThreshold, len(delegationKeys)) // Defaulting to threshold of 1, since we don't allow for larger thresholds at the moment. tdJSON, err := json.Marshal(&changelist.TUFDelegation{ NewThreshold: notary.MinThreshold, AddKeys: data.KeyList(delegationKeys), }) if err != nil { return err } template := newCreateDelegationChange(name, tdJSON) return addChange(cl, template, name) } // AddDelegationPaths creates a changelist entry to add provided paths to an existing delegation. // This method cannot create a new delegation itself because the role must meet the key threshold upon creation. func (r *NotaryRepository) AddDelegationPaths(name string, paths []string) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err } defer cl.Close() logrus.Debugf(`Adding %s paths to delegation %s\n`, paths, name) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ AddPaths: paths, }) if err != nil { return err } template := newCreateDelegationChange(name, tdJSON) return addChange(cl, template, name) } // RemoveDelegationKeysAndPaths creates changelist entries to remove provided delegation key IDs and paths. // This method composes RemoveDelegationPaths and RemoveDelegationKeys (each creates one changelist if called). func (r *NotaryRepository) RemoveDelegationKeysAndPaths(name string, keyIDs, paths []string) error { if len(paths) > 0 { err := r.RemoveDelegationPaths(name, paths) if err != nil { return err } } if len(keyIDs) > 0 { err := r.RemoveDelegationKeys(name, keyIDs) if err != nil { return err } } return nil } // RemoveDelegationRole creates a changelist to remove all paths and keys from a role, and delete the role in its entirety. func (r *NotaryRepository) RemoveDelegationRole(name string) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err } defer cl.Close() logrus.Debugf(`Removing delegation "%s"\n`, name) template := newDeleteDelegationChange(name, nil) return addChange(cl, template, name) } // RemoveDelegationPaths creates a changelist entry to remove provided paths from an existing delegation. func (r *NotaryRepository) RemoveDelegationPaths(name string, paths []string) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err } defer cl.Close() logrus.Debugf(`Removing %s paths from delegation "%s"\n`, paths, name) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ RemovePaths: paths, }) if err != nil { return err } template := newUpdateDelegationChange(name, tdJSON) return addChange(cl, template, name) } // RemoveDelegationKeys creates a changelist entry to remove provided keys from an existing delegation. // When this changelist is applied, if the specified keys are the only keys left in the role, // the role itself will be deleted in its entirety. // It can also delete a key from all delegations under a parent using a name // with a wildcard at the end. func (r *NotaryRepository) RemoveDelegationKeys(name string, keyIDs []string) error { if !data.IsDelegation(name) && !data.IsWildDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err } defer cl.Close() logrus.Debugf(`Removing %s keys from delegation "%s"\n`, keyIDs, name) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ RemoveKeys: keyIDs, }) if err != nil { return err } template := newUpdateDelegationChange(name, tdJSON) return addChange(cl, template, name) } // ClearDelegationPaths creates a changelist entry to remove all paths from an existing delegation. func (r *NotaryRepository) ClearDelegationPaths(name string) error { if !data.IsDelegation(name) { return data.ErrInvalidRole{Role: name, Reason: "invalid delegation role name"} } cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist")) if err != nil { return err } defer cl.Close() logrus.Debugf(`Removing all paths from delegation "%s"\n`, name) tdJSON, err := json.Marshal(&changelist.TUFDelegation{ ClearAllPaths: true, }) if err != nil { return err } template := newUpdateDelegationChange(name, tdJSON) return addChange(cl, template, name) } func newUpdateDelegationChange(name string, content []byte) *changelist.TUFChange { return changelist.NewTUFChange( changelist.ActionUpdate, name, changelist.TypeTargetsDelegation, "", // no path for delegations content, ) } func newCreateDelegationChange(name string, content []byte) *changelist.TUFChange { return changelist.NewTUFChange( changelist.ActionCreate, name, changelist.TypeTargetsDelegation, "", // no path for delegations content, ) } func newDeleteDelegationChange(name string, content []byte) *changelist.TUFChange { return changelist.NewTUFChange( changelist.ActionDelete, name, changelist.TypeTargetsDelegation, "", // no path for delegations content, ) } // GetDelegationRoles returns the keys and roles of the repository's delegations // Also converts key IDs to canonical key IDs to keep consistent with signing prompts func (r *NotaryRepository) GetDelegationRoles() ([]data.Role, error) { // Update state of the repo to latest if err := r.Update(false); err != nil { return nil, err } // All top level delegations (ex: targets/level1) are stored exclusively in targets.json _, ok := r.tufRepo.Targets[data.CanonicalTargetsRole] if !ok { return nil, store.ErrMetaNotFound{Resource: data.CanonicalTargetsRole} } // make a copy for traversing nested delegations allDelegations := []data.Role{} // Define a visitor function to populate the delegations list and translate their key IDs to canonical IDs delegationCanonicalListVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} { // For the return list, update with a copy that includes canonicalKeyIDs // These aren't validated by the validRole canonicalDelegations, err := translateDelegationsToCanonicalIDs(tgt.Signed.Delegations) if err != nil { return err } allDelegations = append(allDelegations, canonicalDelegations...) return nil } err := r.tufRepo.WalkTargets("", "", delegationCanonicalListVisitor) if err != nil { return nil, err } return allDelegations, nil } func translateDelegationsToCanonicalIDs(delegationInfo data.Delegations) ([]data.Role, error) { canonicalDelegations := make([]data.Role, len(delegationInfo.Roles)) // Do a copy by value to ensure local delegation metadata is untouched for idx, origRole := range delegationInfo.Roles { canonicalDelegations[idx] = *origRole } delegationKeys := delegationInfo.Keys for i, delegation := range canonicalDelegations { canonicalKeyIDs := []string{} for _, keyID := range delegation.KeyIDs { pubKey, ok := delegationKeys[keyID] if !ok { return []data.Role{}, fmt.Errorf("Could not translate canonical key IDs for %s", delegation.Name) } canonicalKeyID, err := utils.CanonicalKeyID(pubKey) if err != nil { return []data.Role{}, fmt.Errorf("Could not translate canonical key IDs for %s: %v", delegation.Name, err) } canonicalKeyIDs = append(canonicalKeyIDs, canonicalKeyID) } canonicalDelegations[i].KeyIDs = canonicalKeyIDs } return canonicalDelegations, nil }