| package data |
| |
| import ( |
| "fmt" |
| "path" |
| "regexp" |
| "strings" |
| |
| "github.com/Sirupsen/logrus" |
| ) |
| |
| // Canonical base role names |
| const ( |
| CanonicalRootRole = "root" |
| CanonicalTargetsRole = "targets" |
| CanonicalSnapshotRole = "snapshot" |
| CanonicalTimestampRole = "timestamp" |
| ) |
| |
| // BaseRoles is an easy to iterate list of the top level |
| // roles. |
| var BaseRoles = []string{ |
| CanonicalRootRole, |
| CanonicalTargetsRole, |
| CanonicalSnapshotRole, |
| CanonicalTimestampRole, |
| } |
| |
| // Regex for validating delegation names |
| var delegationRegexp = regexp.MustCompile("^[-a-z0-9_/]+$") |
| |
| // ErrNoSuchRole indicates the roles doesn't exist |
| type ErrNoSuchRole struct { |
| Role string |
| } |
| |
| func (e ErrNoSuchRole) Error() string { |
| return fmt.Sprintf("role does not exist: %s", e.Role) |
| } |
| |
| // ErrInvalidRole represents an error regarding a role. Typically |
| // something like a role for which sone of the public keys were |
| // not found in the TUF repo. |
| type ErrInvalidRole struct { |
| Role string |
| Reason string |
| } |
| |
| func (e ErrInvalidRole) Error() string { |
| if e.Reason != "" { |
| return fmt.Sprintf("tuf: invalid role %s. %s", e.Role, e.Reason) |
| } |
| return fmt.Sprintf("tuf: invalid role %s.", e.Role) |
| } |
| |
| // ValidRole only determines the name is semantically |
| // correct. For target delegated roles, it does NOT check |
| // the the appropriate parent roles exist. |
| func ValidRole(name string) bool { |
| if IsDelegation(name) { |
| return true |
| } |
| |
| for _, v := range BaseRoles { |
| if name == v { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // IsDelegation checks if the role is a delegation or a root role |
| func IsDelegation(role string) bool { |
| targetsBase := CanonicalTargetsRole + "/" |
| |
| whitelistedChars := delegationRegexp.MatchString(role) |
| |
| // Limit size of full role string to 255 chars for db column size limit |
| correctLength := len(role) < 256 |
| |
| // Removes ., .., extra slashes, and trailing slash |
| isClean := path.Clean(role) == role |
| return strings.HasPrefix(role, targetsBase) && |
| whitelistedChars && |
| correctLength && |
| isClean |
| } |
| |
| // RootRole is a cut down role as it appears in the root.json |
| type RootRole struct { |
| KeyIDs []string `json:"keyids"` |
| Threshold int `json:"threshold"` |
| } |
| |
| // Role is a more verbose role as they appear in targets delegations |
| type Role struct { |
| RootRole |
| Name string `json:"name"` |
| Paths []string `json:"paths,omitempty"` |
| PathHashPrefixes []string `json:"path_hash_prefixes,omitempty"` |
| Email string `json:"email,omitempty"` |
| } |
| |
| // NewRole creates a new Role object from the given parameters |
| func NewRole(name string, threshold int, keyIDs, paths, pathHashPrefixes []string) (*Role, error) { |
| if len(paths) > 0 && len(pathHashPrefixes) > 0 { |
| return nil, ErrInvalidRole{ |
| Role: name, |
| Reason: "roles may not have both Paths and PathHashPrefixes", |
| } |
| } |
| if IsDelegation(name) { |
| if len(paths) == 0 && len(pathHashPrefixes) == 0 { |
| logrus.Debugf("role %s with no Paths and no PathHashPrefixes will never be able to publish content until one or more are added", name) |
| } |
| } |
| if threshold < 1 { |
| return nil, ErrInvalidRole{Role: name} |
| } |
| if !ValidRole(name) { |
| return nil, ErrInvalidRole{Role: name} |
| } |
| return &Role{ |
| RootRole: RootRole{ |
| KeyIDs: keyIDs, |
| Threshold: threshold, |
| }, |
| Name: name, |
| Paths: paths, |
| PathHashPrefixes: pathHashPrefixes, |
| }, nil |
| |
| } |
| |
| // IsValid checks if the role has defined both paths and path hash prefixes, |
| // having both is invalid |
| func (r Role) IsValid() bool { |
| return !(len(r.Paths) > 0 && len(r.PathHashPrefixes) > 0) |
| } |
| |
| // ValidKey checks if the given id is a recognized signing key for the role |
| func (r Role) ValidKey(id string) bool { |
| for _, key := range r.KeyIDs { |
| if key == id { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // CheckPaths checks if a given path is valid for the role |
| func (r Role) CheckPaths(path string) bool { |
| for _, p := range r.Paths { |
| if strings.HasPrefix(path, p) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // CheckPrefixes checks if a given hash matches the prefixes for the role |
| func (r Role) CheckPrefixes(hash string) bool { |
| for _, p := range r.PathHashPrefixes { |
| if strings.HasPrefix(hash, p) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // IsDelegation checks if the role is a delegation or a root role |
| func (r Role) IsDelegation() bool { |
| return IsDelegation(r.Name) |
| } |
| |
| // AddKeys merges the ids into the current list of role key ids |
| func (r *Role) AddKeys(ids []string) { |
| r.KeyIDs = mergeStrSlices(r.KeyIDs, ids) |
| } |
| |
| // AddPaths merges the paths into the current list of role paths |
| func (r *Role) AddPaths(paths []string) error { |
| if len(paths) == 0 { |
| return nil |
| } |
| if len(r.PathHashPrefixes) > 0 { |
| return ErrInvalidRole{Role: r.Name, Reason: "attempted to add paths to role that already has hash prefixes"} |
| } |
| r.Paths = mergeStrSlices(r.Paths, paths) |
| return nil |
| } |
| |
| // AddPathHashPrefixes merges the prefixes into the list of role path hash prefixes |
| func (r *Role) AddPathHashPrefixes(prefixes []string) error { |
| if len(prefixes) == 0 { |
| return nil |
| } |
| if len(r.Paths) > 0 { |
| return ErrInvalidRole{Role: r.Name, Reason: "attempted to add hash prefixes to role that already has paths"} |
| } |
| r.PathHashPrefixes = mergeStrSlices(r.PathHashPrefixes, prefixes) |
| return nil |
| } |
| |
| // RemoveKeys removes the ids from the current list of key ids |
| func (r *Role) RemoveKeys(ids []string) { |
| r.KeyIDs = subtractStrSlices(r.KeyIDs, ids) |
| } |
| |
| // RemovePaths removes the paths from the current list of role paths |
| func (r *Role) RemovePaths(paths []string) { |
| r.Paths = subtractStrSlices(r.Paths, paths) |
| } |
| |
| // RemovePathHashPrefixes removes the prefixes from the current list of path hash prefixes |
| func (r *Role) RemovePathHashPrefixes(prefixes []string) { |
| r.PathHashPrefixes = subtractStrSlices(r.PathHashPrefixes, prefixes) |
| } |
| |
| func mergeStrSlices(orig, new []string) []string { |
| have := make(map[string]bool) |
| for _, e := range orig { |
| have[e] = true |
| } |
| merged := make([]string, len(orig), len(orig)+len(new)) |
| copy(merged, orig) |
| for _, e := range new { |
| if !have[e] { |
| merged = append(merged, e) |
| } |
| } |
| return merged |
| } |
| |
| func subtractStrSlices(orig, remove []string) []string { |
| kill := make(map[string]bool) |
| for _, e := range remove { |
| kill[e] = true |
| } |
| var keep []string |
| for _, e := range orig { |
| if !kill[e] { |
| keep = append(keep, e) |
| } |
| } |
| return keep |
| } |
| |
| // Restrict restricts the paths and path hash prefixes for the passed in delegation role, |
| // returning a copy of the role with validated paths as if it was a direct child |
| func Restrict(parent, child Role) (*Role, error) { |
| if path.Dir(child.Name) != parent.Name { |
| return nil, fmt.Errorf("%s is not a parent of %s", parent.Name, child.Name) |
| } |
| return &Role{ |
| RootRole: child.RootRole, |
| Name: child.Name, |
| Paths: RestrictDelegationPathPrefixes(parent.Paths, child.Paths), |
| }, nil |
| } |
| |
| // RestrictDelegationPathPrefixes returns the list of valid delegationPaths that are prefixed by parentPaths |
| func RestrictDelegationPathPrefixes(parentPaths, delegationPaths []string) []string { |
| validPaths := []string{} |
| if len(delegationPaths) == 0 { |
| return validPaths |
| } |
| |
| // Validate each individual delegation path |
| for _, delgPath := range delegationPaths { |
| isPrefixed := false |
| for _, parentPath := range parentPaths { |
| if strings.HasPrefix(delgPath, parentPath) { |
| isPrefixed = true |
| break |
| } |
| } |
| // If the delegation path did not match prefix against any parent path, it is not valid |
| if isPrefixed { |
| validPaths = append(validPaths, delgPath) |
| } |
| } |
| return validPaths |
| } |