|
@@ -419,7 +419,7 @@ func (r *NotaryRepository) RemoveTarget(targetName string, roles ...string) erro
|
|
|
// subtree and also the "targets/x" subtree, as we will defer parsing it until
|
|
|
// we explicitly reach it in our iteration of the provided list of roles.
|
|
|
func (r *NotaryRepository) ListTargets(roles ...string) ([]*TargetWithRole, error) {
|
|
|
- _, err := r.Update()
|
|
|
+ _, err := r.Update(false)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
@@ -479,7 +479,7 @@ func (r *NotaryRepository) listSubtree(targets map[string]*TargetWithRole, role
|
|
|
// will be returned
|
|
|
// See the IMPORTANT section on ListTargets above. Those roles also apply here.
|
|
|
func (r *NotaryRepository) GetTargetByName(name string, roles ...string) (*TargetWithRole, error) {
|
|
|
- c, err := r.Update()
|
|
|
+ c, err := r.Update(false)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
@@ -514,7 +514,7 @@ func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) {
|
|
|
func (r *NotaryRepository) Publish() error {
|
|
|
var initialPublish bool
|
|
|
// update first before publishing
|
|
|
- _, err := r.Update()
|
|
|
+ _, err := r.Update(true)
|
|
|
if err != nil {
|
|
|
// If the remote is not aware of the repo, then this is being published
|
|
|
// for the first time. Try to load from disk instead for publishing.
|
|
@@ -555,13 +555,21 @@ func (r *NotaryRepository) Publish() error {
|
|
|
// we send anything to remote
|
|
|
updatedFiles := make(map[string][]byte)
|
|
|
|
|
|
- // check if our root file is nearing expiry. Resign if it is.
|
|
|
- if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty || initialPublish {
|
|
|
+ // check if our root file is nearing expiry or dirty. Resign if it is. If
|
|
|
+ // root is not dirty but we are publishing for the first time, then just
|
|
|
+ // publish the existing root we have.
|
|
|
+ if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty {
|
|
|
rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
updatedFiles[data.CanonicalRootRole] = rootJSON
|
|
|
+ } else if initialPublish {
|
|
|
+ rootJSON, err := r.tufRepo.Root.MarshalJSON()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ updatedFiles[data.CanonicalRootRole] = rootJSON
|
|
|
}
|
|
|
|
|
|
// iterate through all the targets files - if they are dirty, sign and update
|
|
@@ -714,75 +722,94 @@ func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error {
|
|
|
return r.fileStore.SetMeta(data.CanonicalSnapshotRole, snapshotJSON)
|
|
|
}
|
|
|
|
|
|
+// returns a properly constructed ErrRepositoryNotExist error based on this
|
|
|
+// repo's information
|
|
|
+func (r *NotaryRepository) errRepositoryNotExist() error {
|
|
|
+ host := r.baseURL
|
|
|
+ parsed, err := url.Parse(r.baseURL)
|
|
|
+ if err == nil {
|
|
|
+ host = parsed.Host // try to exclude the scheme and any paths
|
|
|
+ }
|
|
|
+ return ErrRepositoryNotExist{remote: host, gun: r.gun}
|
|
|
+}
|
|
|
+
|
|
|
// Update bootstraps a trust anchor (root.json) before updating all the
|
|
|
// metadata from the repo.
|
|
|
-func (r *NotaryRepository) Update() (*tufclient.Client, error) {
|
|
|
- c, err := r.bootstrapClient()
|
|
|
+func (r *NotaryRepository) Update(forWrite bool) (*tufclient.Client, error) {
|
|
|
+ c, err := r.bootstrapClient(forWrite)
|
|
|
if err != nil {
|
|
|
if _, ok := err.(store.ErrMetaNotFound); ok {
|
|
|
- host := r.baseURL
|
|
|
- parsed, err := url.Parse(r.baseURL)
|
|
|
- if err == nil {
|
|
|
- host = parsed.Host // try to exclude the scheme and any paths
|
|
|
- }
|
|
|
- return nil, ErrRepositoryNotExist{remote: host, gun: r.gun}
|
|
|
+ return nil, r.errRepositoryNotExist()
|
|
|
}
|
|
|
return nil, err
|
|
|
}
|
|
|
err = c.Update()
|
|
|
if err != nil {
|
|
|
+ if notFound, ok := err.(store.ErrMetaNotFound); ok && notFound.Resource == data.CanonicalRootRole {
|
|
|
+ return nil, r.errRepositoryNotExist()
|
|
|
+ }
|
|
|
return nil, err
|
|
|
}
|
|
|
return c, nil
|
|
|
}
|
|
|
|
|
|
-func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
|
|
- var rootJSON []byte
|
|
|
- remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip)
|
|
|
- if err == nil {
|
|
|
- // if remote store successfully set up, try and get root from remote
|
|
|
- rootJSON, err = remote.GetMeta("root", maxSize)
|
|
|
+// bootstrapClient attempts to bootstrap a root.json to be used as the trust
|
|
|
+// anchor for a repository. The checkInitialized argument indicates whether
|
|
|
+// we should always attempt to contact the server to determine if the repository
|
|
|
+// is initialized or not. If set to true, we will always attempt to download
|
|
|
+// and return an error if the remote repository errors.
|
|
|
+func (r *NotaryRepository) bootstrapClient(checkInitialized bool) (*tufclient.Client, error) {
|
|
|
+ var (
|
|
|
+ rootJSON []byte
|
|
|
+ err error
|
|
|
+ signedRoot *data.SignedRoot
|
|
|
+ )
|
|
|
+ // try to read root from cache first. We will trust this root
|
|
|
+ // until we detect a problem during update which will cause
|
|
|
+ // us to download a new root and perform a rotation.
|
|
|
+ rootJSON, cachedRootErr := r.fileStore.GetMeta("root", maxSize)
|
|
|
+
|
|
|
+ if cachedRootErr == nil {
|
|
|
+ signedRoot, cachedRootErr = r.validateRoot(rootJSON)
|
|
|
}
|
|
|
|
|
|
- // if remote store couldn't be setup, or we failed to get a root from it
|
|
|
- // load the root from cache (offline operation)
|
|
|
- if err != nil {
|
|
|
- if err, ok := err.(store.ErrMetaNotFound); ok {
|
|
|
- // if the error was MetaNotFound then we successfully contacted
|
|
|
- // the store and it doesn't know about the repo.
|
|
|
- return nil, err
|
|
|
- }
|
|
|
- result, cacheErr := r.fileStore.GetMeta("root", maxSize)
|
|
|
- if cacheErr != nil {
|
|
|
- // if cache didn't return a root, we cannot proceed - just return
|
|
|
- // the original error.
|
|
|
+ remote, remoteErr := getRemoteStore(r.baseURL, r.gun, r.roundTrip)
|
|
|
+ if remoteErr != nil {
|
|
|
+ logrus.Error(remoteErr)
|
|
|
+ } else if cachedRootErr != nil || checkInitialized {
|
|
|
+ // remoteErr was nil and we had a cachedRootErr (or are specifically
|
|
|
+ // checking for initialization of the repo).
|
|
|
+
|
|
|
+ // if remote store successfully set up, try and get root from remote
|
|
|
+ tmpJSON, err := remote.GetMeta("root", maxSize)
|
|
|
+ if err != nil {
|
|
|
+ // we didn't have a root in cache and were unable to load one from
|
|
|
+ // the server. Nothing we can do but error.
|
|
|
return nil, err
|
|
|
}
|
|
|
- rootJSON = result
|
|
|
- logrus.Debugf(
|
|
|
- "Using local cache instead of remote due to failure: %s", err.Error())
|
|
|
- }
|
|
|
- // can't just unmarshal into SignedRoot because validate root
|
|
|
- // needs the root.Signed field to still be []byte for signature
|
|
|
- // validation
|
|
|
- root := &data.Signed{}
|
|
|
- err = json.Unmarshal(rootJSON, root)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
- }
|
|
|
+ if cachedRootErr != nil {
|
|
|
+ // we always want to use the downloaded root if there was a cache
|
|
|
+ // error.
|
|
|
+ signedRoot, err = r.validateRoot(tmpJSON)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
|
|
|
- err = r.CertManager.ValidateRoot(root, r.gun)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
+ err = r.fileStore.SetMeta("root", tmpJSON)
|
|
|
+ if err != nil {
|
|
|
+ // if we can't write cache we should still continue, just log error
|
|
|
+ logrus.Errorf("could not save root to cache: %s", err.Error())
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
kdb := keys.NewDB()
|
|
|
r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
|
|
|
|
|
|
- signedRoot, err := data.RootFromSigned(root)
|
|
|
- if err != nil {
|
|
|
- return nil, err
|
|
|
+ if signedRoot == nil {
|
|
|
+ return nil, ErrRepoNotInitialized{}
|
|
|
}
|
|
|
+
|
|
|
err = r.tufRepo.SetRoot(signedRoot)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
@@ -796,6 +823,28 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
|
|
), nil
|
|
|
}
|
|
|
|
|
|
+// validateRoot MUST only be used during bootstrapping. It will only validate
|
|
|
+// signatures of the root based on known keys, not expiry or other metadata.
|
|
|
+// This is so that an out of date root can be loaded to be used in a rotation
|
|
|
+// should the TUF update process detect a problem.
|
|
|
+func (r *NotaryRepository) validateRoot(rootJSON []byte) (*data.SignedRoot, error) {
|
|
|
+ // can't just unmarshal into SignedRoot because validate root
|
|
|
+ // needs the root.Signed field to still be []byte for signature
|
|
|
+ // validation
|
|
|
+ root := &data.Signed{}
|
|
|
+ err := json.Unmarshal(rootJSON, root)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ err = r.CertManager.ValidateRoot(root, r.gun)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return data.RootFromSigned(root)
|
|
|
+}
|
|
|
+
|
|
|
// RotateKey removes all existing keys associated with the role, and either
|
|
|
// creates and adds one new key or delegates managing the key to the server.
|
|
|
// These changes are staged in a changelist until publish is called.
|