mirror of https://github.com/docker/cli.git
1149 lines
37 KiB
Go
1149 lines
37 KiB
Go
// Package tuf defines the core TUF logic around manipulating a repo.
|
|
package tuf
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/notary"
|
|
"github.com/docker/notary/tuf/data"
|
|
"github.com/docker/notary/tuf/signed"
|
|
"github.com/docker/notary/tuf/utils"
|
|
)
|
|
|
|
// ErrSigVerifyFail - signature verification failed
|
|
type ErrSigVerifyFail struct{}
|
|
|
|
func (e ErrSigVerifyFail) Error() string {
|
|
return "Error: Signature verification failed"
|
|
}
|
|
|
|
// ErrMetaExpired - metadata file has expired
|
|
type ErrMetaExpired struct{}
|
|
|
|
func (e ErrMetaExpired) Error() string {
|
|
return "Error: Metadata has expired"
|
|
}
|
|
|
|
// ErrLocalRootExpired - the local root file is out of date
|
|
type ErrLocalRootExpired struct{}
|
|
|
|
func (e ErrLocalRootExpired) Error() string {
|
|
return "Error: Local Root Has Expired"
|
|
}
|
|
|
|
// ErrNotLoaded - attempted to access data that has not been loaded into
|
|
// the repo. This means specifically that the relevant JSON file has not
|
|
// been loaded.
|
|
type ErrNotLoaded struct {
|
|
Role string
|
|
}
|
|
|
|
func (err ErrNotLoaded) Error() string {
|
|
return fmt.Sprintf("%s role has not been loaded", err.Role)
|
|
}
|
|
|
|
// StopWalk - used by visitor functions to signal WalkTargets to stop walking
|
|
type StopWalk struct{}
|
|
|
|
// Repo is an in memory representation of the TUF Repo.
|
|
// It operates at the data.Signed level, accepting and producing
|
|
// data.Signed objects. Users of a Repo are responsible for
|
|
// fetching raw JSON and using the Set* functions to populate
|
|
// the Repo instance.
|
|
type Repo struct {
|
|
Root *data.SignedRoot
|
|
Targets map[string]*data.SignedTargets
|
|
Snapshot *data.SignedSnapshot
|
|
Timestamp *data.SignedTimestamp
|
|
cryptoService signed.CryptoService
|
|
|
|
// Because Repo is a mutable structure, these keep track of what the root
|
|
// role was when a root is set on the repo (as opposed to what it might be
|
|
// after things like AddBaseKeys and RemoveBaseKeys have been called on it).
|
|
// If we know what the original was, we'll if and how to handle root
|
|
// rotations.
|
|
originalRootRole data.BaseRole
|
|
}
|
|
|
|
// NewRepo initializes a Repo instance with a CryptoService.
|
|
// If the Repo will only be used for reading, the CryptoService
|
|
// can be nil.
|
|
func NewRepo(cryptoService signed.CryptoService) *Repo {
|
|
return &Repo{
|
|
Targets: make(map[string]*data.SignedTargets),
|
|
cryptoService: cryptoService,
|
|
}
|
|
}
|
|
|
|
// AddBaseKeys is used to add keys to the role in root.json
|
|
func (tr *Repo) AddBaseKeys(role string, keys ...data.PublicKey) error {
|
|
if tr.Root == nil {
|
|
return ErrNotLoaded{Role: data.CanonicalRootRole}
|
|
}
|
|
ids := []string{}
|
|
for _, k := range keys {
|
|
// Store only the public portion
|
|
tr.Root.Signed.Keys[k.ID()] = k
|
|
tr.Root.Signed.Roles[role].KeyIDs = append(tr.Root.Signed.Roles[role].KeyIDs, k.ID())
|
|
ids = append(ids, k.ID())
|
|
}
|
|
tr.Root.Dirty = true
|
|
|
|
// also, whichever role was switched out needs to be re-signed
|
|
// root has already been marked dirty.
|
|
switch role {
|
|
case data.CanonicalSnapshotRole:
|
|
if tr.Snapshot != nil {
|
|
tr.Snapshot.Dirty = true
|
|
}
|
|
case data.CanonicalTargetsRole:
|
|
if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok {
|
|
target.Dirty = true
|
|
}
|
|
case data.CanonicalTimestampRole:
|
|
if tr.Timestamp != nil {
|
|
tr.Timestamp.Dirty = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReplaceBaseKeys is used to replace all keys for the given role with the new keys
|
|
func (tr *Repo) ReplaceBaseKeys(role string, keys ...data.PublicKey) error {
|
|
r, err := tr.GetBaseRole(role)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = tr.RemoveBaseKeys(role, r.ListKeyIDs()...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return tr.AddBaseKeys(role, keys...)
|
|
}
|
|
|
|
// RemoveBaseKeys is used to remove keys from the roles in root.json
|
|
func (tr *Repo) RemoveBaseKeys(role string, keyIDs ...string) error {
|
|
if tr.Root == nil {
|
|
return ErrNotLoaded{Role: data.CanonicalRootRole}
|
|
}
|
|
var keep []string
|
|
toDelete := make(map[string]struct{})
|
|
emptyStruct := struct{}{}
|
|
// remove keys from specified role
|
|
for _, k := range keyIDs {
|
|
toDelete[k] = emptyStruct
|
|
}
|
|
|
|
oldKeyIDs := tr.Root.Signed.Roles[role].KeyIDs
|
|
for _, rk := range oldKeyIDs {
|
|
if _, ok := toDelete[rk]; !ok {
|
|
keep = append(keep, rk)
|
|
}
|
|
}
|
|
|
|
tr.Root.Signed.Roles[role].KeyIDs = keep
|
|
|
|
// also, whichever role had keys removed needs to be re-signed
|
|
// root has already been marked dirty.
|
|
switch role {
|
|
case data.CanonicalSnapshotRole:
|
|
if tr.Snapshot != nil {
|
|
tr.Snapshot.Dirty = true
|
|
}
|
|
case data.CanonicalTargetsRole:
|
|
if target, ok := tr.Targets[data.CanonicalTargetsRole]; ok {
|
|
target.Dirty = true
|
|
}
|
|
case data.CanonicalTimestampRole:
|
|
if tr.Timestamp != nil {
|
|
tr.Timestamp.Dirty = true
|
|
}
|
|
}
|
|
|
|
// determine which keys are no longer in use by any roles
|
|
for roleName, r := range tr.Root.Signed.Roles {
|
|
if roleName == role {
|
|
continue
|
|
}
|
|
for _, rk := range r.KeyIDs {
|
|
if _, ok := toDelete[rk]; ok {
|
|
delete(toDelete, rk)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove keys no longer in use by any roles, except for root keys.
|
|
// Root private keys must be kept in tr.cryptoService to be able to sign
|
|
// for rotation, and root certificates must be kept in tr.Root.SignedKeys
|
|
// because we are not necessarily storing them elsewhere (tuf.Repo does not
|
|
// depend on certs.Manager, that is an upper layer), and without storing
|
|
// the certificates in their x509 form we are not able to do the
|
|
// util.CanonicalKeyID conversion.
|
|
if role != data.CanonicalRootRole {
|
|
for k := range toDelete {
|
|
delete(tr.Root.Signed.Keys, k)
|
|
tr.cryptoService.RemoveKey(k)
|
|
}
|
|
}
|
|
tr.Root.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
// GetBaseRole gets a base role from this repo's metadata
|
|
func (tr *Repo) GetBaseRole(name string) (data.BaseRole, error) {
|
|
if !data.ValidRole(name) {
|
|
return data.BaseRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid base role name"}
|
|
}
|
|
if tr.Root == nil {
|
|
return data.BaseRole{}, ErrNotLoaded{data.CanonicalRootRole}
|
|
}
|
|
// Find the role data public keys for the base role from TUF metadata
|
|
baseRole, err := tr.Root.BuildBaseRole(name)
|
|
if err != nil {
|
|
return data.BaseRole{}, err
|
|
}
|
|
|
|
return baseRole, nil
|
|
}
|
|
|
|
// GetDelegationRole gets a delegation role from this repo's metadata, walking from the targets role down to the delegation itself
|
|
func (tr *Repo) GetDelegationRole(name string) (data.DelegationRole, error) {
|
|
if !data.IsDelegation(name) {
|
|
return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "invalid delegation name"}
|
|
}
|
|
if tr.Root == nil {
|
|
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalRootRole}
|
|
}
|
|
_, ok := tr.Root.Signed.Roles[data.CanonicalTargetsRole]
|
|
if !ok {
|
|
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole}
|
|
}
|
|
// Traverse target metadata, down to delegation itself
|
|
// Get all public keys for the base role from TUF metadata
|
|
_, ok = tr.Targets[data.CanonicalTargetsRole]
|
|
if !ok {
|
|
return data.DelegationRole{}, ErrNotLoaded{data.CanonicalTargetsRole}
|
|
}
|
|
|
|
// Start with top level roles in targets. Walk the chain of ancestors
|
|
// until finding the desired role, or we run out of targets files to search.
|
|
var foundRole *data.DelegationRole
|
|
buildDelegationRoleVisitor := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
|
|
// Try to find the delegation and build a DelegationRole structure
|
|
for _, role := range tgt.Signed.Delegations.Roles {
|
|
if role.Name == name {
|
|
delgRole, err := tgt.BuildDelegationRole(name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Check all public key certificates in the role for expiry
|
|
// Currently we do not reject expired delegation keys but warn if they might expire soon or have already
|
|
for keyID, pubKey := range delgRole.Keys {
|
|
certFromKey, err := utils.LoadCertFromPEM(pubKey.Public())
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if err := utils.ValidateCertificate(certFromKey, true); err != nil {
|
|
if _, ok := err.(data.ErrCertExpired); !ok {
|
|
// do not allow other invalid cert errors
|
|
return err
|
|
}
|
|
logrus.Warnf("error with delegation %s key ID %d: %s", delgRole.Name, keyID, err)
|
|
}
|
|
}
|
|
foundRole = &delgRole
|
|
return StopWalk{}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Walk to the parent of this delegation, since that is where its role metadata exists
|
|
err := tr.WalkTargets("", path.Dir(name), buildDelegationRoleVisitor)
|
|
if err != nil {
|
|
return data.DelegationRole{}, err
|
|
}
|
|
|
|
// We never found the delegation. In the context of this repo it is considered
|
|
// invalid. N.B. it may be that it existed at one point but an ancestor has since
|
|
// been modified/removed.
|
|
if foundRole == nil {
|
|
return data.DelegationRole{}, data.ErrInvalidRole{Role: name, Reason: "delegation does not exist"}
|
|
}
|
|
|
|
return *foundRole, nil
|
|
}
|
|
|
|
// GetAllLoadedRoles returns a list of all role entries loaded in this TUF repo, could be empty
|
|
func (tr *Repo) GetAllLoadedRoles() []*data.Role {
|
|
var res []*data.Role
|
|
if tr.Root == nil {
|
|
// if root isn't loaded, we should consider we have no loaded roles because we can't
|
|
// trust any other state that might be present
|
|
return res
|
|
}
|
|
for name, rr := range tr.Root.Signed.Roles {
|
|
res = append(res, &data.Role{
|
|
RootRole: *rr,
|
|
Name: name,
|
|
})
|
|
}
|
|
for _, delegate := range tr.Targets {
|
|
for _, r := range delegate.Signed.Delegations.Roles {
|
|
res = append(res, r)
|
|
}
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Walk to parent, and either create or update this delegation. We can only create a new delegation if we're given keys
|
|
// Ensure all updates are valid, by checking against parent ancestor paths and ensuring the keys meet the role threshold.
|
|
func delegationUpdateVisitor(roleName string, addKeys data.KeyList, removeKeys, addPaths, removePaths []string, clearAllPaths bool, newThreshold int) walkVisitorFunc {
|
|
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
|
|
var err error
|
|
// Validate the changes underneath this restricted validRole for adding paths, reject invalid path additions
|
|
if len(addPaths) != len(data.RestrictDelegationPathPrefixes(validRole.Paths, addPaths)) {
|
|
return data.ErrInvalidRole{Role: roleName, Reason: "invalid paths to add to role"}
|
|
}
|
|
// Try to find the delegation and amend it using our changelist
|
|
var delgRole *data.Role
|
|
for _, role := range tgt.Signed.Delegations.Roles {
|
|
if role.Name == roleName {
|
|
// Make a copy and operate on this role until we validate the changes
|
|
keyIDCopy := make([]string, len(role.KeyIDs))
|
|
copy(keyIDCopy, role.KeyIDs)
|
|
pathsCopy := make([]string, len(role.Paths))
|
|
copy(pathsCopy, role.Paths)
|
|
delgRole = &data.Role{
|
|
RootRole: data.RootRole{
|
|
KeyIDs: keyIDCopy,
|
|
Threshold: role.Threshold,
|
|
},
|
|
Name: role.Name,
|
|
Paths: pathsCopy,
|
|
}
|
|
delgRole.RemovePaths(removePaths)
|
|
if clearAllPaths {
|
|
delgRole.Paths = []string{}
|
|
}
|
|
delgRole.AddPaths(addPaths)
|
|
delgRole.RemoveKeys(removeKeys)
|
|
break
|
|
}
|
|
}
|
|
// We didn't find the role earlier, so create it.
|
|
if addKeys == nil {
|
|
addKeys = data.KeyList{} // initialize to empty list if necessary so calling .IDs() below won't panic
|
|
}
|
|
if delgRole == nil {
|
|
delgRole, err = data.NewRole(roleName, newThreshold, addKeys.IDs(), addPaths)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
// Add the key IDs to the role and the keys themselves to the parent
|
|
for _, k := range addKeys {
|
|
if !utils.StrSliceContains(delgRole.KeyIDs, k.ID()) {
|
|
delgRole.KeyIDs = append(delgRole.KeyIDs, k.ID())
|
|
}
|
|
}
|
|
// Make sure we have a valid role still
|
|
if len(delgRole.KeyIDs) < delgRole.Threshold {
|
|
logrus.Warnf("role %s has fewer keys than its threshold of %d; it will not be usable until keys are added to it", delgRole.Name, delgRole.Threshold)
|
|
}
|
|
// NOTE: this closure CANNOT error after this point, as we've committed to editing the SignedTargets metadata in the repo object.
|
|
// Any errors related to updating this delegation must occur before this point.
|
|
// If all of our changes were valid, we should edit the actual SignedTargets to match our copy
|
|
for _, k := range addKeys {
|
|
tgt.Signed.Delegations.Keys[k.ID()] = k
|
|
}
|
|
foundAt := utils.FindRoleIndex(tgt.Signed.Delegations.Roles, delgRole.Name)
|
|
if foundAt < 0 {
|
|
tgt.Signed.Delegations.Roles = append(tgt.Signed.Delegations.Roles, delgRole)
|
|
} else {
|
|
tgt.Signed.Delegations.Roles[foundAt] = delgRole
|
|
}
|
|
tgt.Dirty = true
|
|
utils.RemoveUnusedKeys(tgt)
|
|
return StopWalk{}
|
|
}
|
|
}
|
|
|
|
// UpdateDelegationKeys updates the appropriate delegations, either adding
|
|
// a new delegation or updating an existing one. If keys are
|
|
// provided, the IDs will be added to the role (if they do not exist
|
|
// there already), and the keys will be added to the targets file.
|
|
func (tr *Repo) UpdateDelegationKeys(roleName string, addKeys data.KeyList, removeKeys []string, newThreshold int) error {
|
|
if !data.IsDelegation(roleName) {
|
|
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
|
|
}
|
|
parent := path.Dir(roleName)
|
|
|
|
if err := tr.VerifyCanSign(parent); err != nil {
|
|
return err
|
|
}
|
|
|
|
// check the parent role's metadata
|
|
_, ok := tr.Targets[parent]
|
|
if !ok { // the parent targetfile may not exist yet - if not, then create it
|
|
var err error
|
|
_, err = tr.InitTargets(parent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Walk to the parent of this delegation, since that is where its role metadata exists
|
|
// We do not have to verify that the walker reached its desired role in this scenario
|
|
// since we've already done another walk to the parent role in VerifyCanSign, and potentially made a targets file
|
|
return tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, addKeys, removeKeys, []string{}, []string{}, false, newThreshold))
|
|
}
|
|
|
|
// PurgeDelegationKeys removes the provided canonical key IDs from all delegations
|
|
// present in the subtree rooted at role. The role argument must be provided in a wildcard
|
|
// format, i.e. targets/* would remove the key from all delegations in the repo
|
|
func (tr *Repo) PurgeDelegationKeys(role string, removeKeys []string) error {
|
|
if !data.IsWildDelegation(role) {
|
|
return data.ErrInvalidRole{
|
|
Role: role,
|
|
Reason: "only wildcard roles can be used in a purge",
|
|
}
|
|
}
|
|
|
|
removeIDs := make(map[string]struct{})
|
|
for _, id := range removeKeys {
|
|
removeIDs[id] = struct{}{}
|
|
}
|
|
|
|
start := path.Dir(role)
|
|
tufIDToCanon := make(map[string]string)
|
|
|
|
purgeKeys := func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
|
|
var (
|
|
deleteCandidates []string
|
|
err error
|
|
)
|
|
for id, key := range tgt.Signed.Delegations.Keys {
|
|
var (
|
|
canonID string
|
|
ok bool
|
|
)
|
|
if canonID, ok = tufIDToCanon[id]; !ok {
|
|
canonID, err = utils.CanonicalKeyID(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tufIDToCanon[id] = canonID
|
|
}
|
|
if _, ok := removeIDs[canonID]; ok {
|
|
deleteCandidates = append(deleteCandidates, id)
|
|
}
|
|
}
|
|
if len(deleteCandidates) == 0 {
|
|
// none of the interesting keys were present. We're done with this role
|
|
return nil
|
|
}
|
|
// now we know there are changes, check if we'll be able to sign them in
|
|
if err := tr.VerifyCanSign(validRole.Name); err != nil {
|
|
logrus.Warnf(
|
|
"role %s contains keys being purged but you do not have the necessary keys present to sign it; keys will not be purged from %s or its immediate children",
|
|
validRole.Name,
|
|
validRole.Name,
|
|
)
|
|
return nil
|
|
}
|
|
// we know we can sign in the changes, delete the keys
|
|
for _, id := range deleteCandidates {
|
|
delete(tgt.Signed.Delegations.Keys, id)
|
|
}
|
|
// delete candidate keys from all roles.
|
|
for _, role := range tgt.Signed.Delegations.Roles {
|
|
role.RemoveKeys(deleteCandidates)
|
|
if len(role.KeyIDs) < role.Threshold {
|
|
logrus.Warnf("role %s has fewer keys than its threshold of %d; it will not be usable until keys are added to it", role.Name, role.Threshold)
|
|
}
|
|
}
|
|
tgt.Dirty = true
|
|
return nil
|
|
}
|
|
return tr.WalkTargets("", start, purgeKeys)
|
|
}
|
|
|
|
// UpdateDelegationPaths updates the appropriate delegation's paths.
|
|
// It is not allowed to create a new delegation.
|
|
func (tr *Repo) UpdateDelegationPaths(roleName string, addPaths, removePaths []string, clearPaths bool) error {
|
|
if !data.IsDelegation(roleName) {
|
|
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
|
|
}
|
|
parent := path.Dir(roleName)
|
|
|
|
if err := tr.VerifyCanSign(parent); err != nil {
|
|
return err
|
|
}
|
|
|
|
// check the parent role's metadata
|
|
_, ok := tr.Targets[parent]
|
|
if !ok { // the parent targetfile may not exist yet
|
|
// if not, this is an error because a delegation must exist to edit only paths
|
|
return data.ErrInvalidRole{Role: roleName, Reason: "no valid delegated role exists"}
|
|
}
|
|
|
|
// Walk to the parent of this delegation, since that is where its role metadata exists
|
|
// We do not have to verify that the walker reached its desired role in this scenario
|
|
// since we've already done another walk to the parent role in VerifyCanSign
|
|
err := tr.WalkTargets("", parent, delegationUpdateVisitor(roleName, data.KeyList{}, []string{}, addPaths, removePaths, clearPaths, notary.MinThreshold))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteDelegation removes a delegated targets role from its parent
|
|
// targets object. It also deletes the delegation from the snapshot.
|
|
// DeleteDelegation will only make use of the role Name field.
|
|
func (tr *Repo) DeleteDelegation(roleName string) error {
|
|
if !data.IsDelegation(roleName) {
|
|
return data.ErrInvalidRole{Role: roleName, Reason: "not a valid delegated role"}
|
|
}
|
|
|
|
parent := path.Dir(roleName)
|
|
if err := tr.VerifyCanSign(parent); err != nil {
|
|
return err
|
|
}
|
|
|
|
// delete delegated data from Targets map and Snapshot - if they don't
|
|
// exist, these are no-op
|
|
delete(tr.Targets, roleName)
|
|
tr.Snapshot.DeleteMeta(roleName)
|
|
|
|
p, ok := tr.Targets[parent]
|
|
if !ok {
|
|
// if there is no parent metadata (the role exists though), then this
|
|
// is as good as done.
|
|
return nil
|
|
}
|
|
|
|
foundAt := utils.FindRoleIndex(p.Signed.Delegations.Roles, roleName)
|
|
|
|
if foundAt >= 0 {
|
|
var roles []*data.Role
|
|
// slice out deleted role
|
|
roles = append(roles, p.Signed.Delegations.Roles[:foundAt]...)
|
|
if foundAt+1 < len(p.Signed.Delegations.Roles) {
|
|
roles = append(roles, p.Signed.Delegations.Roles[foundAt+1:]...)
|
|
}
|
|
p.Signed.Delegations.Roles = roles
|
|
|
|
utils.RemoveUnusedKeys(p)
|
|
|
|
p.Dirty = true
|
|
} // if the role wasn't found, it's a good as deleted
|
|
|
|
return nil
|
|
}
|
|
|
|
// InitRoot initializes an empty root file with the 4 core roles passed to the
|
|
// method, and the consistent flag.
|
|
func (tr *Repo) InitRoot(root, timestamp, snapshot, targets data.BaseRole, consistent bool) error {
|
|
rootRoles := make(map[string]*data.RootRole)
|
|
rootKeys := make(map[string]data.PublicKey)
|
|
|
|
for _, r := range []data.BaseRole{root, timestamp, snapshot, targets} {
|
|
rootRoles[r.Name] = &data.RootRole{
|
|
Threshold: r.Threshold,
|
|
KeyIDs: r.ListKeyIDs(),
|
|
}
|
|
for kid, k := range r.Keys {
|
|
rootKeys[kid] = k
|
|
}
|
|
}
|
|
r, err := data.NewRoot(rootKeys, rootRoles, consistent)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tr.Root = r
|
|
tr.originalRootRole = root
|
|
return nil
|
|
}
|
|
|
|
// InitTargets initializes an empty targets, and returns the new empty target
|
|
func (tr *Repo) InitTargets(role string) (*data.SignedTargets, error) {
|
|
if !data.IsDelegation(role) && role != data.CanonicalTargetsRole {
|
|
return nil, data.ErrInvalidRole{
|
|
Role: role,
|
|
Reason: fmt.Sprintf("role is not a valid targets role name: %s", role),
|
|
}
|
|
}
|
|
targets := data.NewTargets()
|
|
tr.Targets[role] = targets
|
|
return targets, nil
|
|
}
|
|
|
|
// InitSnapshot initializes a snapshot based on the current root and targets
|
|
func (tr *Repo) InitSnapshot() error {
|
|
if tr.Root == nil {
|
|
return ErrNotLoaded{Role: data.CanonicalRootRole}
|
|
}
|
|
root, err := tr.Root.ToSigned()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, ok := tr.Targets[data.CanonicalTargetsRole]; !ok {
|
|
return ErrNotLoaded{Role: data.CanonicalTargetsRole}
|
|
}
|
|
targets, err := tr.Targets[data.CanonicalTargetsRole].ToSigned()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
snapshot, err := data.NewSnapshot(root, targets)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tr.Snapshot = snapshot
|
|
return nil
|
|
}
|
|
|
|
// InitTimestamp initializes a timestamp based on the current snapshot
|
|
func (tr *Repo) InitTimestamp() error {
|
|
snap, err := tr.Snapshot.ToSigned()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
timestamp, err := data.NewTimestamp(snap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tr.Timestamp = timestamp
|
|
return nil
|
|
}
|
|
|
|
// TargetMeta returns the FileMeta entry for the given path in the
|
|
// targets file associated with the given role. This may be nil if
|
|
// the target isn't found in the targets file.
|
|
func (tr Repo) TargetMeta(role, path string) *data.FileMeta {
|
|
if t, ok := tr.Targets[role]; ok {
|
|
if m, ok := t.Signed.Targets[path]; ok {
|
|
return &m
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TargetDelegations returns a slice of Roles that are valid publishers
|
|
// for the target path provided.
|
|
func (tr Repo) TargetDelegations(role, path string) []*data.Role {
|
|
var roles []*data.Role
|
|
if t, ok := tr.Targets[role]; ok {
|
|
for _, r := range t.Signed.Delegations.Roles {
|
|
if r.CheckPaths(path) {
|
|
roles = append(roles, r)
|
|
}
|
|
}
|
|
}
|
|
return roles
|
|
}
|
|
|
|
// VerifyCanSign returns nil if the role exists and we have at least one
|
|
// signing key for the role, false otherwise. This does not check that we have
|
|
// enough signing keys to meet the threshold, since we want to support the use
|
|
// case of multiple signers for a role. It returns an error if the role doesn't
|
|
// exist or if there are no signing keys.
|
|
func (tr *Repo) VerifyCanSign(roleName string) error {
|
|
var (
|
|
role data.BaseRole
|
|
err error
|
|
canonicalKeyIDs []string
|
|
)
|
|
// we only need the BaseRole part of a delegation because we're just
|
|
// checking KeyIDs
|
|
if data.IsDelegation(roleName) {
|
|
r, err := tr.GetDelegationRole(roleName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
role = r.BaseRole
|
|
} else {
|
|
role, err = tr.GetBaseRole(roleName)
|
|
}
|
|
if err != nil {
|
|
return data.ErrInvalidRole{Role: roleName, Reason: "does not exist"}
|
|
}
|
|
|
|
for keyID, k := range role.Keys {
|
|
check := []string{keyID}
|
|
if canonicalID, err := utils.CanonicalKeyID(k); err == nil {
|
|
check = append(check, canonicalID)
|
|
canonicalKeyIDs = append(canonicalKeyIDs, canonicalID)
|
|
}
|
|
for _, id := range check {
|
|
p, _, err := tr.cryptoService.GetPrivateKey(id)
|
|
if err == nil && p != nil {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return signed.ErrNoKeys{KeyIDs: canonicalKeyIDs}
|
|
}
|
|
|
|
// used for walking the targets/delegations tree, potentially modifying the underlying SignedTargets for the repo
|
|
type walkVisitorFunc func(*data.SignedTargets, data.DelegationRole) interface{}
|
|
|
|
// WalkTargets will apply the specified visitor function to iteratively walk the targets/delegation metadata tree,
|
|
// until receiving a StopWalk. The walk starts from the base "targets" role, and searches for the correct targetPath and/or rolePath
|
|
// to call the visitor function on. Any roles passed into skipRoles will be excluded from the walk, as well as roles in those subtrees
|
|
func (tr *Repo) WalkTargets(targetPath, rolePath string, visitTargets walkVisitorFunc, skipRoles ...string) error {
|
|
// Start with the base targets role, which implicitly has the "" targets path
|
|
targetsRole, err := tr.GetBaseRole(data.CanonicalTargetsRole)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Make the targets role have the empty path, when we treat it as a delegation role
|
|
roles := []data.DelegationRole{
|
|
{
|
|
BaseRole: targetsRole,
|
|
Paths: []string{""},
|
|
},
|
|
}
|
|
|
|
for len(roles) > 0 {
|
|
role := roles[0]
|
|
roles = roles[1:]
|
|
|
|
// Check the role metadata
|
|
signedTgt, ok := tr.Targets[role.Name]
|
|
if !ok {
|
|
// The role meta doesn't exist in the repo so continue onward
|
|
continue
|
|
}
|
|
|
|
// We're at a prefix of the desired role subtree, so add its delegation role children and continue walking
|
|
if strings.HasPrefix(rolePath, role.Name+"/") {
|
|
roles = append(roles, signedTgt.GetValidDelegations(role)...)
|
|
continue
|
|
}
|
|
|
|
// Determine whether to visit this role or not:
|
|
// If the paths validate against the specified targetPath and the rolePath is empty or is in the subtree.
|
|
// Also check if we are choosing to skip visiting this role on this walk (see ListTargets and GetTargetByName priority)
|
|
if isValidPath(targetPath, role) && isAncestorRole(role.Name, rolePath) && !utils.StrSliceContains(skipRoles, role.Name) {
|
|
// If we had matching path or role name, visit this target and determine whether or not to keep walking
|
|
res := visitTargets(signedTgt, role)
|
|
switch typedRes := res.(type) {
|
|
case StopWalk:
|
|
// If the visitor function signalled a stop, return nil to finish the walk
|
|
return nil
|
|
case nil:
|
|
// If the visitor function signalled to continue, add this role's delegation to the walk
|
|
roles = append(roles, signedTgt.GetValidDelegations(role)...)
|
|
case error:
|
|
// Propagate any errors from the visitor
|
|
return typedRes
|
|
default:
|
|
// Return out with an error if we got a different result
|
|
return fmt.Errorf("unexpected return while walking: %v", res)
|
|
}
|
|
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// helper function that returns whether the candidateChild role name is an ancestor or equal to the candidateAncestor role name
|
|
// Will return true if given an empty candidateAncestor role name
|
|
// The HasPrefix check is for determining whether the role name for candidateChild is a child (direct or further down the chain)
|
|
// of candidateAncestor, for ex: candidateAncestor targets/a and candidateChild targets/a/b/c
|
|
func isAncestorRole(candidateChild, candidateAncestor string) bool {
|
|
return candidateAncestor == "" || candidateAncestor == candidateChild || strings.HasPrefix(candidateChild, candidateAncestor+"/")
|
|
}
|
|
|
|
// helper function that returns whether the delegation Role is valid against the given path
|
|
// Will return true if given an empty candidatePath
|
|
func isValidPath(candidatePath string, delgRole data.DelegationRole) bool {
|
|
return candidatePath == "" || delgRole.CheckPaths(candidatePath)
|
|
}
|
|
|
|
// AddTargets will attempt to add the given targets specifically to
|
|
// the directed role. If the metadata for the role doesn't exist yet,
|
|
// AddTargets will create one.
|
|
func (tr *Repo) AddTargets(role string, targets data.Files) (data.Files, error) {
|
|
err := tr.VerifyCanSign(role)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// check existence of the role's metadata
|
|
_, ok := tr.Targets[role]
|
|
if !ok { // the targetfile may not exist yet - if not, then create it
|
|
var err error
|
|
_, err = tr.InitTargets(role)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
addedTargets := make(data.Files)
|
|
addTargetVisitor := func(targetPath string, targetMeta data.FileMeta) func(*data.SignedTargets, data.DelegationRole) interface{} {
|
|
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
|
|
// We've already validated the role's target path in our walk, so just modify the metadata
|
|
tgt.Signed.Targets[targetPath] = targetMeta
|
|
tgt.Dirty = true
|
|
// Also add to our new addedTargets map to keep track of every target we've added successfully
|
|
addedTargets[targetPath] = targetMeta
|
|
return StopWalk{}
|
|
}
|
|
}
|
|
|
|
// Walk the role tree while validating the target paths, and add all of our targets
|
|
for path, target := range targets {
|
|
tr.WalkTargets(path, role, addTargetVisitor(path, target))
|
|
}
|
|
if len(addedTargets) != len(targets) {
|
|
return nil, fmt.Errorf("Could not add all targets")
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// RemoveTargets removes the given target (paths) from the given target role (delegation)
|
|
func (tr *Repo) RemoveTargets(role string, targets ...string) error {
|
|
if err := tr.VerifyCanSign(role); err != nil {
|
|
return err
|
|
}
|
|
|
|
removeTargetVisitor := func(targetPath string) func(*data.SignedTargets, data.DelegationRole) interface{} {
|
|
return func(tgt *data.SignedTargets, validRole data.DelegationRole) interface{} {
|
|
// We've already validated the role path in our walk, so just modify the metadata
|
|
// We don't check against the target path against the valid role paths because it's
|
|
// possible we got into an invalid state and are trying to fix it
|
|
delete(tgt.Signed.Targets, targetPath)
|
|
tgt.Dirty = true
|
|
return StopWalk{}
|
|
}
|
|
}
|
|
|
|
// if the role exists but metadata does not yet, then our work is done
|
|
_, ok := tr.Targets[role]
|
|
if ok {
|
|
for _, path := range targets {
|
|
tr.WalkTargets("", role, removeTargetVisitor(path))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateSnapshot updates the FileMeta for the given role based on the Signed object
|
|
func (tr *Repo) UpdateSnapshot(role string, s *data.Signed) error {
|
|
jsonData, err := json.Marshal(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
meta, err := data.NewFileMeta(bytes.NewReader(jsonData), data.NotaryDefaultHashes...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tr.Snapshot.Signed.Meta[role] = meta
|
|
tr.Snapshot.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
// UpdateTimestamp updates the snapshot meta in the timestamp based on the Signed object
|
|
func (tr *Repo) UpdateTimestamp(s *data.Signed) error {
|
|
jsonData, err := json.Marshal(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
meta, err := data.NewFileMeta(bytes.NewReader(jsonData), data.NotaryDefaultHashes...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tr.Timestamp.Signed.Meta[data.CanonicalSnapshotRole] = meta
|
|
tr.Timestamp.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
type versionedRootRole struct {
|
|
data.BaseRole
|
|
version int
|
|
}
|
|
|
|
type versionedRootRoles []versionedRootRole
|
|
|
|
func (v versionedRootRoles) Len() int { return len(v) }
|
|
func (v versionedRootRoles) Swap(i, j int) { v[i], v[j] = v[j], v[i] }
|
|
func (v versionedRootRoles) Less(i, j int) bool { return v[i].version < v[j].version }
|
|
|
|
// SignRoot signs the root, using all keys from the "root" role (i.e. currently trusted)
|
|
// as well as available keys used to sign the previous version, if the public part is
|
|
// carried in tr.Root.Keys and the private key is available (i.e. probably previously
|
|
// trusted keys, to allow rollover). If there are any errors, attempt to put root
|
|
// back to the way it was (so version won't be incremented, for instance).
|
|
func (tr *Repo) SignRoot(expires time.Time) (*data.Signed, error) {
|
|
logrus.Debug("signing root...")
|
|
|
|
// duplicate root and attempt to modify it rather than the existing root
|
|
rootBytes, err := tr.Root.MarshalJSON()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tempRoot := data.SignedRoot{}
|
|
if err := json.Unmarshal(rootBytes, &tempRoot); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
currRoot, err := tr.GetBaseRole(data.CanonicalRootRole)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
oldRootRoles := tr.getOldRootRoles()
|
|
|
|
var latestSavedRole data.BaseRole
|
|
rolesToSignWith := make([]data.BaseRole, 0, len(oldRootRoles))
|
|
|
|
if len(oldRootRoles) > 0 {
|
|
sort.Sort(oldRootRoles)
|
|
for _, vRole := range oldRootRoles {
|
|
rolesToSignWith = append(rolesToSignWith, vRole.BaseRole)
|
|
}
|
|
latest := rolesToSignWith[len(rolesToSignWith)-1]
|
|
latestSavedRole = data.BaseRole{
|
|
Name: data.CanonicalRootRole,
|
|
Threshold: latest.Threshold,
|
|
Keys: latest.Keys,
|
|
}
|
|
}
|
|
|
|
// If the root role (root keys or root threshold) has changed, save the
|
|
// previous role under the role name "root.<n>", such that the "n" is the
|
|
// latest root.json version for which previous root role was valid.
|
|
// Also, guard against re-saving the previous role if the latest
|
|
// saved role is the same (which should not happen).
|
|
// n = root.json version of the originalRootRole (previous role)
|
|
// n+1 = root.json version of the currRoot (current role)
|
|
// n-m = root.json version of latestSavedRole (not necessarily n-1, because the
|
|
// last root rotation could have happened several root.json versions ago
|
|
if !tr.originalRootRole.Equals(currRoot) && !tr.originalRootRole.Equals(latestSavedRole) {
|
|
rolesToSignWith = append(rolesToSignWith, tr.originalRootRole)
|
|
latestSavedRole = tr.originalRootRole
|
|
|
|
versionName := oldRootVersionName(tempRoot.Signed.Version)
|
|
tempRoot.Signed.Roles[versionName] = &data.RootRole{
|
|
KeyIDs: latestSavedRole.ListKeyIDs(), Threshold: latestSavedRole.Threshold}
|
|
}
|
|
|
|
tempRoot.Signed.Expires = expires
|
|
tempRoot.Signed.Version++
|
|
rolesToSignWith = append(rolesToSignWith, currRoot)
|
|
|
|
signed, err := tempRoot.ToSigned()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signed, err = tr.sign(signed, rolesToSignWith, tr.getOptionalRootKeys(rolesToSignWith))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tr.Root = &tempRoot
|
|
tr.Root.Signatures = signed.Signatures
|
|
tr.originalRootRole = currRoot
|
|
return signed, nil
|
|
}
|
|
|
|
// get all the saved previous roles < the current root version
|
|
func (tr *Repo) getOldRootRoles() versionedRootRoles {
|
|
oldRootRoles := make(versionedRootRoles, 0, len(tr.Root.Signed.Roles))
|
|
|
|
// now go through the old roles
|
|
for roleName := range tr.Root.Signed.Roles {
|
|
// ensure that the rolename matches our format and that the version is
|
|
// not too high
|
|
if data.ValidRole(roleName) {
|
|
continue
|
|
}
|
|
nameTokens := strings.Split(roleName, ".")
|
|
if len(nameTokens) != 2 || nameTokens[0] != data.CanonicalRootRole {
|
|
continue
|
|
}
|
|
version, err := strconv.Atoi(nameTokens[1])
|
|
if err != nil || version >= tr.Root.Signed.Version {
|
|
continue
|
|
}
|
|
|
|
// ignore invalid roles, which shouldn't happen
|
|
oldRole, err := tr.Root.BuildBaseRole(roleName)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
oldRootRoles = append(oldRootRoles, versionedRootRole{BaseRole: oldRole, version: version})
|
|
}
|
|
|
|
return oldRootRoles
|
|
}
|
|
|
|
// gets any extra optional root keys from the existing root.json signatures
|
|
// (because older repositories that have already done root rotation may not
|
|
// necessarily have older root roles)
|
|
func (tr *Repo) getOptionalRootKeys(signingRoles []data.BaseRole) []data.PublicKey {
|
|
oldKeysMap := make(map[string]data.PublicKey)
|
|
for _, oldSig := range tr.Root.Signatures {
|
|
if k, ok := tr.Root.Signed.Keys[oldSig.KeyID]; ok {
|
|
oldKeysMap[k.ID()] = k
|
|
}
|
|
}
|
|
for _, role := range signingRoles {
|
|
for keyID := range role.Keys {
|
|
delete(oldKeysMap, keyID)
|
|
}
|
|
}
|
|
|
|
oldKeys := make([]data.PublicKey, 0, len(oldKeysMap))
|
|
for _, key := range oldKeysMap {
|
|
oldKeys = append(oldKeys, key)
|
|
}
|
|
|
|
return oldKeys
|
|
}
|
|
|
|
func oldRootVersionName(version int) string {
|
|
return fmt.Sprintf("%s.%v", data.CanonicalRootRole, version)
|
|
}
|
|
|
|
// SignTargets signs the targets file for the given top level or delegated targets role
|
|
func (tr *Repo) SignTargets(role string, expires time.Time) (*data.Signed, error) {
|
|
logrus.Debugf("sign targets called for role %s", role)
|
|
if _, ok := tr.Targets[role]; !ok {
|
|
return nil, data.ErrInvalidRole{
|
|
Role: role,
|
|
Reason: "SignTargets called with non-existent targets role",
|
|
}
|
|
}
|
|
tr.Targets[role].Signed.Expires = expires
|
|
tr.Targets[role].Signed.Version++
|
|
signed, err := tr.Targets[role].ToSigned()
|
|
if err != nil {
|
|
logrus.Debug("errored getting targets data.Signed object")
|
|
return nil, err
|
|
}
|
|
|
|
var targets data.BaseRole
|
|
if role == data.CanonicalTargetsRole {
|
|
targets, err = tr.GetBaseRole(role)
|
|
} else {
|
|
tr, err := tr.GetDelegationRole(role)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targets = tr.BaseRole
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
signed, err = tr.sign(signed, []data.BaseRole{targets}, nil)
|
|
if err != nil {
|
|
logrus.Debug("errored signing ", role)
|
|
return nil, err
|
|
}
|
|
tr.Targets[role].Signatures = signed.Signatures
|
|
return signed, nil
|
|
}
|
|
|
|
// SignSnapshot updates the snapshot based on the current targets and root then signs it
|
|
func (tr *Repo) SignSnapshot(expires time.Time) (*data.Signed, error) {
|
|
logrus.Debug("signing snapshot...")
|
|
signedRoot, err := tr.Root.ToSigned()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = tr.UpdateSnapshot(data.CanonicalRootRole, signedRoot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tr.Root.Dirty = false // root dirty until changes captures in snapshot
|
|
for role, targets := range tr.Targets {
|
|
signedTargets, err := targets.ToSigned()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = tr.UpdateSnapshot(role, signedTargets)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targets.Dirty = false
|
|
}
|
|
tr.Snapshot.Signed.Expires = expires
|
|
tr.Snapshot.Signed.Version++
|
|
signed, err := tr.Snapshot.ToSigned()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snapshot, err := tr.GetBaseRole(data.CanonicalSnapshotRole)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signed, err = tr.sign(signed, []data.BaseRole{snapshot}, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tr.Snapshot.Signatures = signed.Signatures
|
|
return signed, nil
|
|
}
|
|
|
|
// SignTimestamp updates the timestamp based on the current snapshot then signs it
|
|
func (tr *Repo) SignTimestamp(expires time.Time) (*data.Signed, error) {
|
|
logrus.Debug("SignTimestamp")
|
|
signedSnapshot, err := tr.Snapshot.ToSigned()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = tr.UpdateTimestamp(signedSnapshot)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tr.Timestamp.Signed.Expires = expires
|
|
tr.Timestamp.Signed.Version++
|
|
signed, err := tr.Timestamp.ToSigned()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
timestamp, err := tr.GetBaseRole(data.CanonicalTimestampRole)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signed, err = tr.sign(signed, []data.BaseRole{timestamp}, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tr.Timestamp.Signatures = signed.Signatures
|
|
tr.Snapshot.Dirty = false // snapshot is dirty until changes have been captured in timestamp
|
|
return signed, nil
|
|
}
|
|
|
|
func (tr Repo) sign(signedData *data.Signed, roles []data.BaseRole, optionalKeys []data.PublicKey) (*data.Signed, error) {
|
|
validKeys := optionalKeys
|
|
for _, r := range roles {
|
|
roleKeys := r.ListKeys()
|
|
validKeys = append(roleKeys, validKeys...)
|
|
if err := signed.Sign(tr.cryptoService, signedData, roleKeys, r.Threshold, validKeys); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
// Attempt to sign with the optional keys, but ignore any errors, because these keys are optional
|
|
signed.Sign(tr.cryptoService, signedData, optionalKeys, 0, validKeys)
|
|
|
|
return signedData, nil
|
|
}
|