|
@@ -9,11 +9,13 @@ import (
|
|
|
"net/http"
|
|
|
"os"
|
|
|
"path/filepath"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
|
+ "github.com/docker/notary/certs"
|
|
|
"github.com/docker/notary/client/changelist"
|
|
|
"github.com/docker/notary/cryptoservice"
|
|
|
- "github.com/docker/notary/keystoremanager"
|
|
|
"github.com/docker/notary/trustmanager"
|
|
|
"github.com/docker/notary/tuf"
|
|
|
tufclient "github.com/docker/notary/tuf/client"
|
|
@@ -52,6 +54,17 @@ type ErrExpired struct {
|
|
|
signed.ErrExpired
|
|
|
}
|
|
|
|
|
|
+// ErrInvalidRemoteRole is returned when the server is requested to manage
|
|
|
+// an unsupported key type
|
|
|
+type ErrInvalidRemoteRole struct {
|
|
|
+ Role string
|
|
|
+}
|
|
|
+
|
|
|
+func (e ErrInvalidRemoteRole) Error() string {
|
|
|
+ return fmt.Sprintf(
|
|
|
+ "notary does not support the server managing the %s key", e.Role)
|
|
|
+}
|
|
|
+
|
|
|
const (
|
|
|
tufDir = "tuf"
|
|
|
)
|
|
@@ -63,23 +76,67 @@ var ErrRepositoryNotExist = errors.New("repository does not exist")
|
|
|
// NotaryRepository stores all the information needed to operate on a notary
|
|
|
// repository.
|
|
|
type NotaryRepository struct {
|
|
|
- baseDir string
|
|
|
- gun string
|
|
|
- baseURL string
|
|
|
- tufRepoPath string
|
|
|
- fileStore store.MetadataStore
|
|
|
- CryptoService signed.CryptoService
|
|
|
- tufRepo *tuf.Repo
|
|
|
- roundTrip http.RoundTripper
|
|
|
- KeyStoreManager *keystoremanager.KeyStoreManager
|
|
|
+ baseDir string
|
|
|
+ gun string
|
|
|
+ baseURL string
|
|
|
+ tufRepoPath string
|
|
|
+ fileStore store.MetadataStore
|
|
|
+ CryptoService signed.CryptoService
|
|
|
+ tufRepo *tuf.Repo
|
|
|
+ roundTrip http.RoundTripper
|
|
|
+ CertManager *certs.Manager
|
|
|
+}
|
|
|
+
|
|
|
+// repositoryFromKeystores is a helper function for NewNotaryRepository that
|
|
|
+// takes some basic NotaryRepository parameters as well as keystores (in order
|
|
|
+// of usage preference), and returns a NotaryRepository.
|
|
|
+func repositoryFromKeystores(baseDir, gun, baseURL string, rt http.RoundTripper,
|
|
|
+ keyStores []trustmanager.KeyStore) (*NotaryRepository, error) {
|
|
|
+
|
|
|
+ certManager, err := certs.NewManager(baseDir)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ cryptoService := cryptoservice.NewCryptoService(gun, keyStores...)
|
|
|
+
|
|
|
+ nRepo := &NotaryRepository{
|
|
|
+ gun: gun,
|
|
|
+ baseDir: baseDir,
|
|
|
+ baseURL: baseURL,
|
|
|
+ tufRepoPath: filepath.Join(baseDir, tufDir, filepath.FromSlash(gun)),
|
|
|
+ CryptoService: cryptoService,
|
|
|
+ roundTrip: rt,
|
|
|
+ CertManager: certManager,
|
|
|
+ }
|
|
|
+
|
|
|
+ fileStore, err := store.NewFilesystemStore(
|
|
|
+ nRepo.tufRepoPath,
|
|
|
+ "metadata",
|
|
|
+ "json",
|
|
|
+ "",
|
|
|
+ )
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ nRepo.fileStore = fileStore
|
|
|
+
|
|
|
+ return nRepo, nil
|
|
|
}
|
|
|
|
|
|
// Target represents a simplified version of the data TUF operates on, so external
|
|
|
// applications don't have to depend on tuf data types.
|
|
|
type Target struct {
|
|
|
- Name string
|
|
|
- Hashes data.Hashes
|
|
|
- Length int64
|
|
|
+ Name string // the name of the target
|
|
|
+ Hashes data.Hashes // the hash of the target
|
|
|
+ Length int64 // the size in bytes of the target
|
|
|
+}
|
|
|
+
|
|
|
+// TargetWithRole represents a Target that exists in a particular role - this is
|
|
|
+// produced by ListTargets and GetTargetByName
|
|
|
+type TargetWithRole struct {
|
|
|
+ Target
|
|
|
+ Role string
|
|
|
}
|
|
|
|
|
|
// NewTarget is a helper method that returns a Target
|
|
@@ -99,18 +156,48 @@ func NewTarget(targetName string, targetPath string) (*Target, error) {
|
|
|
|
|
|
// Initialize creates a new repository by using rootKey as the root Key for the
|
|
|
// TUF repository.
|
|
|
-func (r *NotaryRepository) Initialize(rootKeyID string) error {
|
|
|
+func (r *NotaryRepository) Initialize(rootKeyID string, serverManagedRoles ...string) error {
|
|
|
privKey, _, err := r.CryptoService.GetPrivateKey(rootKeyID)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- rootCert, err := cryptoservice.GenerateCertificate(privKey, r.gun)
|
|
|
+ // currently we only support server managing timestamps and snapshots, and
|
|
|
+ // nothing else - timestamps are always managed by the server, and implicit
|
|
|
+ // (do not have to be passed in as part of `serverManagedRoles`, so that
|
|
|
+ // the API of Initialize doens't change).
|
|
|
+ var serverManagesSnapshot bool
|
|
|
+ locallyManagedKeys := []string{
|
|
|
+ data.CanonicalTargetsRole,
|
|
|
+ data.CanonicalSnapshotRole,
|
|
|
+ // root is also locally managed, but that should have been created
|
|
|
+ // already
|
|
|
+ }
|
|
|
+ remotelyManagedKeys := []string{data.CanonicalTimestampRole}
|
|
|
+ for _, role := range serverManagedRoles {
|
|
|
+ switch role {
|
|
|
+ case data.CanonicalTimestampRole:
|
|
|
+ continue // timestamp is already in the right place
|
|
|
+ case data.CanonicalSnapshotRole:
|
|
|
+ // because we put Snapshot last
|
|
|
+ locallyManagedKeys = []string{data.CanonicalTargetsRole}
|
|
|
+ remotelyManagedKeys = append(
|
|
|
+ remotelyManagedKeys, data.CanonicalSnapshotRole)
|
|
|
+ serverManagesSnapshot = true
|
|
|
+ default:
|
|
|
+ return ErrInvalidRemoteRole{Role: role}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Hard-coded policy: the generated certificate expires in 10 years.
|
|
|
+ startTime := time.Now()
|
|
|
+ rootCert, err := cryptoservice.GenerateCertificate(
|
|
|
+ privKey, r.gun, startTime, startTime.AddDate(10, 0, 0))
|
|
|
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- r.KeyStoreManager.AddTrustedCert(rootCert)
|
|
|
+ r.CertManager.AddTrustedCert(rootCert)
|
|
|
|
|
|
// The root key gets stored in the TUF metadata X509 encoded, linking
|
|
|
// the tuf root.json to our X509 PKI.
|
|
@@ -127,112 +214,211 @@ func (r *NotaryRepository) Initialize(rootKeyID string) error {
|
|
|
return fmt.Errorf("invalid format for root key: %s", privKey.Algorithm())
|
|
|
}
|
|
|
|
|
|
- // All the timestamp keys are generated by the remote server.
|
|
|
- remote, err := getRemoteStore(r.baseURL, r.gun, r.roundTrip)
|
|
|
+ kdb := keys.NewDB()
|
|
|
+ err = addKeyForRole(kdb, data.CanonicalRootRole, rootKey)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- rawTSKey, err := remote.GetKey("timestamp")
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
+
|
|
|
+ // we want to create all the local keys first so we don't have to
|
|
|
+ // make unnecessary network calls
|
|
|
+ for _, role := range locallyManagedKeys {
|
|
|
+ // This is currently hardcoding the keys to ECDSA.
|
|
|
+ key, err := r.CryptoService.Create(role, data.ECDSAKey)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if err := addKeyForRole(kdb, role, key); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for _, role := range remotelyManagedKeys {
|
|
|
+ // This key is generated by the remote server.
|
|
|
+ key, err := getRemoteKey(r.baseURL, r.gun, role, r.roundTrip)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ logrus.Debugf("got remote %s %s key with keyID: %s",
|
|
|
+ role, key.Algorithm(), key.ID())
|
|
|
+ if err := addKeyForRole(kdb, role, key); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- timestampKey, err := data.UnmarshalPublicKey(rawTSKey)
|
|
|
+ r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
|
|
|
+
|
|
|
+ err = r.tufRepo.InitRoot(false)
|
|
|
if err != nil {
|
|
|
+ logrus.Debug("Error on InitRoot: ", err.Error())
|
|
|
return err
|
|
|
}
|
|
|
-
|
|
|
- logrus.Debugf("got remote %s timestamp key with keyID: %s", timestampKey.Algorithm(), timestampKey.ID())
|
|
|
-
|
|
|
- // This is currently hardcoding the targets and snapshots keys to ECDSA
|
|
|
- // Targets and snapshot keys are always generated locally.
|
|
|
- targetsKey, err := r.CryptoService.Create("targets", data.ECDSAKey)
|
|
|
+ _, err = r.tufRepo.InitTargets(data.CanonicalTargetsRole)
|
|
|
if err != nil {
|
|
|
+ logrus.Debug("Error on InitTargets: ", err.Error())
|
|
|
return err
|
|
|
}
|
|
|
- snapshotKey, err := r.CryptoService.Create("snapshot", data.ECDSAKey)
|
|
|
+ err = r.tufRepo.InitSnapshot()
|
|
|
if err != nil {
|
|
|
+ logrus.Debug("Error on InitSnapshot: ", err.Error())
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- kdb := keys.NewDB()
|
|
|
+ return r.saveMetadata(serverManagesSnapshot)
|
|
|
+}
|
|
|
|
|
|
- kdb.AddKey(rootKey)
|
|
|
- kdb.AddKey(targetsKey)
|
|
|
- kdb.AddKey(snapshotKey)
|
|
|
- kdb.AddKey(timestampKey)
|
|
|
+// adds a TUF Change template to the given roles
|
|
|
+func addChange(cl *changelist.FileChangelist, c changelist.Change, roles ...string) error {
|
|
|
|
|
|
- err = initRoles(kdb, rootKey, targetsKey, snapshotKey, timestampKey)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
+ if len(roles) == 0 {
|
|
|
+ roles = []string{data.CanonicalTargetsRole}
|
|
|
}
|
|
|
|
|
|
- r.tufRepo = tuf.NewRepo(kdb, r.CryptoService)
|
|
|
+ var changes []changelist.Change
|
|
|
+ for _, role := range roles {
|
|
|
+ role = strings.ToLower(role)
|
|
|
|
|
|
- err = r.tufRepo.InitRoot(false)
|
|
|
- if err != nil {
|
|
|
- logrus.Debug("Error on InitRoot: ", err.Error())
|
|
|
- switch err.(type) {
|
|
|
- case signed.ErrInsufficientSignatures, trustmanager.ErrPasswordInvalid:
|
|
|
- default:
|
|
|
+ // Ensure we can only add targets to the CanonicalTargetsRole,
|
|
|
+ // or a Delegation role (which is <CanonicalTargetsRole>/something else)
|
|
|
+ if role != data.CanonicalTargetsRole && !data.IsDelegation(role) {
|
|
|
+ return data.ErrInvalidRole{
|
|
|
+ Role: role,
|
|
|
+ Reason: "cannot add targets to this role",
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ changes = append(changes, changelist.NewTufChange(
|
|
|
+ c.Action(),
|
|
|
+ role,
|
|
|
+ c.Type(),
|
|
|
+ c.Path(),
|
|
|
+ c.Content(),
|
|
|
+ ))
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, c := range changes {
|
|
|
+ if err := cl.Add(c); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
}
|
|
|
- err = r.tufRepo.InitTargets()
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// AddDelegation creates a new changelist entry to add a delegation to the repository
|
|
|
+// when the changelist gets applied at publish time. This does not do any validation
|
|
|
+// other than checking the name of the delegation to add - all that will happen
|
|
|
+// at publish time.
|
|
|
+func (r *NotaryRepository) AddDelegation(name string, threshold int,
|
|
|
+ delegationKeys []data.PublicKey, 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 {
|
|
|
- logrus.Debug("Error on InitTargets: ", err.Error())
|
|
|
return err
|
|
|
}
|
|
|
- err = r.tufRepo.InitSnapshot()
|
|
|
+ defer cl.Close()
|
|
|
+
|
|
|
+ logrus.Debugf(`Adding delegation "%s" with threshold %d, and %d keys\n`,
|
|
|
+ name, threshold, len(delegationKeys))
|
|
|
+
|
|
|
+ tdJSON, err := json.Marshal(&changelist.TufDelegation{
|
|
|
+ NewThreshold: threshold,
|
|
|
+ AddKeys: data.KeyList(delegationKeys),
|
|
|
+ AddPaths: paths,
|
|
|
+ })
|
|
|
if err != nil {
|
|
|
- logrus.Debug("Error on InitSnapshot: ", err.Error())
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- return r.saveMetadata()
|
|
|
+ template := changelist.NewTufChange(
|
|
|
+ changelist.ActionCreate,
|
|
|
+ name,
|
|
|
+ changelist.TypeTargetsDelegation,
|
|
|
+ "", // no path
|
|
|
+ tdJSON,
|
|
|
+ )
|
|
|
+
|
|
|
+ return addChange(cl, template, name)
|
|
|
}
|
|
|
|
|
|
-// AddTarget adds a new target to the repository, forcing a timestamps check from TUF
|
|
|
-func (r *NotaryRepository) AddTarget(target *Target) error {
|
|
|
+// RemoveDelegation creates a new changelist entry to remove a delegation from
|
|
|
+// the repository when the changelist gets applied at publish time.
|
|
|
+// This does not validate that the delegation exists, since one might exist
|
|
|
+// after applying all changes.
|
|
|
+func (r *NotaryRepository) RemoveDelegation(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("Adding target \"%s\" with sha256 \"%x\" and size %d bytes.\n", target.Name, target.Hashes["sha256"], target.Length)
|
|
|
|
|
|
- meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes}
|
|
|
- metaJSON, err := json.Marshal(meta)
|
|
|
+ logrus.Debugf(`Removing delegation "%s"\n`, name)
|
|
|
+
|
|
|
+ template := changelist.NewTufChange(
|
|
|
+ changelist.ActionDelete,
|
|
|
+ name,
|
|
|
+ changelist.TypeTargetsDelegation,
|
|
|
+ "", // no path
|
|
|
+ nil,
|
|
|
+ )
|
|
|
+
|
|
|
+ return addChange(cl, template, name)
|
|
|
+}
|
|
|
+
|
|
|
+// AddTarget creates new changelist entries to add a target to the given roles
|
|
|
+// in the repository when the changelist gets appied at publish time.
|
|
|
+// If roles are unspecified, the default role is "targets".
|
|
|
+func (r *NotaryRepository) AddTarget(target *Target, roles ...string) error {
|
|
|
+
|
|
|
+ cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
+ defer cl.Close()
|
|
|
+ logrus.Debugf("Adding target \"%s\" with sha256 \"%x\" and size %d bytes.\n", target.Name, target.Hashes["sha256"], target.Length)
|
|
|
|
|
|
- c := changelist.NewTufChange(changelist.ActionCreate, changelist.ScopeTargets, "target", target.Name, metaJSON)
|
|
|
- err = cl.Add(c)
|
|
|
+ meta := data.FileMeta{Length: target.Length, Hashes: target.Hashes}
|
|
|
+ metaJSON, err := json.Marshal(meta)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- return nil
|
|
|
+
|
|
|
+ template := changelist.NewTufChange(
|
|
|
+ changelist.ActionCreate, "", changelist.TypeTargetsTarget,
|
|
|
+ target.Name, metaJSON)
|
|
|
+ return addChange(cl, template, roles...)
|
|
|
}
|
|
|
|
|
|
-// RemoveTarget creates a new changelist entry to remove a target from the repository
|
|
|
-// when the changelist gets applied at publish time
|
|
|
-func (r *NotaryRepository) RemoveTarget(targetName string) error {
|
|
|
+// RemoveTarget creates new changelist entries to remove a target from the given
|
|
|
+// roles in the repository when the changelist gets applied at publish time.
|
|
|
+// If roles are unspecified, the default role is "target".
|
|
|
+func (r *NotaryRepository) RemoveTarget(targetName string, roles ...string) error {
|
|
|
+
|
|
|
cl, err := changelist.NewFileChangelist(filepath.Join(r.tufRepoPath, "changelist"))
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
logrus.Debugf("Removing target \"%s\"", targetName)
|
|
|
- c := changelist.NewTufChange(changelist.ActionDelete, changelist.ScopeTargets, "target", targetName, nil)
|
|
|
- err = cl.Add(c)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- return nil
|
|
|
+ template := changelist.NewTufChange(changelist.ActionDelete, "",
|
|
|
+ changelist.TypeTargetsTarget, targetName, nil)
|
|
|
+ return addChange(cl, template, roles...)
|
|
|
}
|
|
|
|
|
|
-// ListTargets lists all targets for the current repository
|
|
|
-func (r *NotaryRepository) ListTargets() ([]*Target, error) {
|
|
|
+// ListTargets lists all targets for the current repository. The list of
|
|
|
+// roles should be passed in order from highest to lowest priority.
|
|
|
+// IMPORTANT: if you pass a set of roles such as [ "targets/a", "targets/x"
|
|
|
+// "targets/a/b" ], even though "targets/a/b" is part of the "targets/a" subtree
|
|
|
+// its entries will be strictly shadowed by those in other parts of the "targets/a"
|
|
|
+// 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) {
|
|
|
c, err := r.bootstrapClient()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
@@ -246,17 +432,61 @@ func (r *NotaryRepository) ListTargets() ([]*Target, error) {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
- var targetList []*Target
|
|
|
- for name, meta := range r.tufRepo.Targets["targets"].Signed.Targets {
|
|
|
- target := &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}
|
|
|
- targetList = append(targetList, target)
|
|
|
+ if len(roles) == 0 {
|
|
|
+ roles = []string{data.CanonicalTargetsRole}
|
|
|
+ }
|
|
|
+ targets := make(map[string]*TargetWithRole)
|
|
|
+ for _, role := range roles {
|
|
|
+ // we don't need to do anything special with removing role from
|
|
|
+ // roles because listSubtree always processes role and only excludes
|
|
|
+ // descendent delegations that appear in roles.
|
|
|
+ r.listSubtree(targets, role, roles...)
|
|
|
+ }
|
|
|
+
|
|
|
+ var targetList []*TargetWithRole
|
|
|
+ for _, v := range targets {
|
|
|
+ targetList = append(targetList, v)
|
|
|
}
|
|
|
|
|
|
return targetList, nil
|
|
|
}
|
|
|
|
|
|
-// GetTargetByName returns a target given a name
|
|
|
-func (r *NotaryRepository) GetTargetByName(name string) (*Target, error) {
|
|
|
+func (r *NotaryRepository) listSubtree(targets map[string]*TargetWithRole, role string, exclude ...string) {
|
|
|
+ excl := make(map[string]bool)
|
|
|
+ for _, r := range exclude {
|
|
|
+ excl[r] = true
|
|
|
+ }
|
|
|
+ roles := []string{role}
|
|
|
+ for len(roles) > 0 {
|
|
|
+ role = roles[0]
|
|
|
+ roles = roles[1:]
|
|
|
+ tgts, ok := r.tufRepo.Targets[role]
|
|
|
+ if !ok {
|
|
|
+ // not every role has to exist
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ for name, meta := range tgts.Signed.Targets {
|
|
|
+ if _, ok := targets[name]; !ok {
|
|
|
+ targets[name] = &TargetWithRole{
|
|
|
+ Target: Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, Role: role}
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for _, d := range tgts.Signed.Delegations.Roles {
|
|
|
+ if !excl[d.Name] {
|
|
|
+ roles = append(roles, d.Name)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// GetTargetByName returns a target given a name. If no roles are passed
|
|
|
+// it uses the targets role and does a search of the entire delegation
|
|
|
+// graph, finding the first entry in a breadth first search of the delegations.
|
|
|
+// If roles are passed, they should be passed in descending priority and
|
|
|
+// the target entry found in the subtree of the highest priority 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.bootstrapClient()
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
@@ -270,14 +500,18 @@ func (r *NotaryRepository) GetTargetByName(name string) (*Target, error) {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
- meta, err := c.TargetMeta(name)
|
|
|
- if meta == nil {
|
|
|
- return nil, fmt.Errorf("No trust data for %s", name)
|
|
|
- } else if err != nil {
|
|
|
- return nil, err
|
|
|
+ if len(roles) == 0 {
|
|
|
+ roles = append(roles, data.CanonicalTargetsRole)
|
|
|
+ }
|
|
|
+ for _, role := range roles {
|
|
|
+ meta, foundRole := c.TargetMeta(role, name, roles...)
|
|
|
+ if meta != nil {
|
|
|
+ return &TargetWithRole{
|
|
|
+ Target: Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, Role: foundRole}, nil
|
|
|
+ }
|
|
|
}
|
|
|
+ return nil, fmt.Errorf("No trust data for %s", name)
|
|
|
|
|
|
- return &Target{Name: name, Hashes: meta.Hashes, Length: meta.Length}, nil
|
|
|
}
|
|
|
|
|
|
// GetChangelist returns the list of the repository's unpublished changes
|
|
@@ -294,30 +528,30 @@ func (r *NotaryRepository) GetChangelist() (changelist.Changelist, error) {
|
|
|
// Publish pushes the local changes in signed material to the remote notary-server
|
|
|
// Conceptually it performs an operation similar to a `git rebase`
|
|
|
func (r *NotaryRepository) Publish() error {
|
|
|
- var updateRoot bool
|
|
|
- var root *data.Signed
|
|
|
+ var initialPublish bool
|
|
|
// attempt to initialize the repo from the remote store
|
|
|
c, err := r.bootstrapClient()
|
|
|
if err != nil {
|
|
|
if _, ok := err.(store.ErrMetaNotFound); ok {
|
|
|
// if the remote store return a 404 (translated into ErrMetaNotFound),
|
|
|
- // the repo hasn't been initialized yet. Attempt to load it from disk.
|
|
|
+ // there is no trust data for yet. Attempt to load it from disk.
|
|
|
err := r.bootstrapRepo()
|
|
|
if err != nil {
|
|
|
- // Repo hasn't been initialized, It must be initialized before
|
|
|
- // it can be published. Return an error and let caller determine
|
|
|
- // what it wants to do.
|
|
|
- logrus.Debug(err.Error())
|
|
|
- logrus.Debug("Repository not initialized during Publish")
|
|
|
- return &ErrRepoNotInitialized{}
|
|
|
- }
|
|
|
- // We had local data but the server doesn't know about the repo yet,
|
|
|
- // ensure we will push the initial root file
|
|
|
- root, err = r.tufRepo.Root.ToSigned()
|
|
|
- if err != nil {
|
|
|
+ // There are lots of reasons there might be an error, such as
|
|
|
+ // corrupt metadata. We need better errors from bootstrapRepo.
|
|
|
+ logrus.Debugf("Unable to load repository from local files: %s",
|
|
|
+ err.Error())
|
|
|
+ if _, ok := err.(store.ErrMetaNotFound); ok {
|
|
|
+ return &ErrRepoNotInitialized{}
|
|
|
+ }
|
|
|
return err
|
|
|
}
|
|
|
- updateRoot = true
|
|
|
+ // We had local data but the server doesn't know about the repo yet,
|
|
|
+ // ensure we will push the initial root and targets file. Either or
|
|
|
+ // both of the root and targets may not be marked as Dirty, since
|
|
|
+ // there may not be any changes that update them, so use a
|
|
|
+ // different boolean.
|
|
|
+ initialPublish = true
|
|
|
} else {
|
|
|
// The remote store returned an error other than 404. We're
|
|
|
// unable to determine if the repo has been initialized or not.
|
|
@@ -326,8 +560,8 @@ func (r *NotaryRepository) Publish() error {
|
|
|
}
|
|
|
} else {
|
|
|
// If we were successfully able to bootstrap the client (which only pulls
|
|
|
- // root.json), update it the rest of the tuf metadata in preparation for
|
|
|
- // applying the changelist.
|
|
|
+ // root.json), update it with the rest of the tuf metadata in
|
|
|
+ // preparation for applying the changelist.
|
|
|
err = c.Update()
|
|
|
if err != nil {
|
|
|
if err, ok := err.(signed.ErrExpired); ok {
|
|
@@ -347,24 +581,53 @@ func (r *NotaryRepository) Publish() error {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
+ // these are the tuf files we will need to update, serialized as JSON before
|
|
|
+ // 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 {
|
|
|
+ if nearExpiry(r.tufRepo.Root) || r.tufRepo.Root.Dirty || initialPublish {
|
|
|
+ rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- root, err = r.tufRepo.SignRoot(data.DefaultExpires("root"))
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
+ updatedFiles[data.CanonicalRootRole] = rootJSON
|
|
|
+ }
|
|
|
+
|
|
|
+ // iterate through all the targets files - if they are dirty, sign and update
|
|
|
+ for roleName, roleObj := range r.tufRepo.Targets {
|
|
|
+ if roleObj.Dirty || (roleName == data.CanonicalTargetsRole && initialPublish) {
|
|
|
+ targetsJSON, err := serializeCanonicalRole(r.tufRepo, roleName)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ updatedFiles[roleName] = targetsJSON
|
|
|
}
|
|
|
- updateRoot = true
|
|
|
}
|
|
|
- // we will always resign targets and snapshots
|
|
|
- targets, err := r.tufRepo.SignTargets("targets", data.DefaultExpires("targets"))
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
+
|
|
|
+ // if we initialized the repo while designating the server as the snapshot
|
|
|
+ // signer, then there won't be a snapshots file. However, we might now
|
|
|
+ // have a local key (if there was a rotation), so initialize one.
|
|
|
+ if r.tufRepo.Snapshot == nil {
|
|
|
+ if err := r.tufRepo.InitSnapshot(); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
}
|
|
|
- snapshot, err := r.tufRepo.SignSnapshot(data.DefaultExpires("snapshot"))
|
|
|
- if err != nil {
|
|
|
+
|
|
|
+ snapshotJSON, err := serializeCanonicalRole(
|
|
|
+ r.tufRepo, data.CanonicalSnapshotRole)
|
|
|
+
|
|
|
+ if err == nil {
|
|
|
+ // Only update the snapshot if we've sucessfully signed it.
|
|
|
+ updatedFiles[data.CanonicalSnapshotRole] = snapshotJSON
|
|
|
+ } else if _, ok := err.(signed.ErrNoKeys); ok {
|
|
|
+ // If signing fails due to us not having the snapshot key, then
|
|
|
+ // assume the server is going to sign, and do not include any snapshot
|
|
|
+ // data.
|
|
|
+ logrus.Debugf("Client does not have the key to sign snapshot. " +
|
|
|
+ "Assuming that server should sign the snapshot.")
|
|
|
+ } else {
|
|
|
+ logrus.Debugf("Client was unable to sign the snapshot: %s", err.Error())
|
|
|
return err
|
|
|
}
|
|
|
|
|
@@ -373,27 +636,7 @@ func (r *NotaryRepository) Publish() error {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- // ensure we can marshal all the json before sending anything to remote
|
|
|
- targetsJSON, err := json.Marshal(targets)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- snapshotJSON, err := json.Marshal(snapshot)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- update := make(map[string][]byte)
|
|
|
- // if we need to update the root, marshal it and push the update to remote
|
|
|
- if updateRoot {
|
|
|
- rootJSON, err := json.Marshal(root)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- update["root"] = rootJSON
|
|
|
- }
|
|
|
- update["targets"] = targetsJSON
|
|
|
- update["snapshot"] = snapshotJSON
|
|
|
- err = remote.SetMultiMeta(update)
|
|
|
+ err = remote.SetMultiMeta(updatedFiles)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
@@ -407,6 +650,11 @@ func (r *NotaryRepository) Publish() error {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
+// bootstrapRepo loads the repository from the local file system. This attempts
|
|
|
+// to load metadata for all roles. Since server snapshots are supported,
|
|
|
+// if the snapshot metadata fails to load, that's ok.
|
|
|
+// This can also be unified with some cache reading tools from tuf/client.
|
|
|
+// This assumes that bootstrapRepo is only used by Publish()
|
|
|
func (r *NotaryRepository) bootstrapRepo() error {
|
|
|
kdb := keys.NewDB()
|
|
|
tufRepo := tuf.NewRepo(kdb, r.CryptoService)
|
|
@@ -435,30 +683,32 @@ func (r *NotaryRepository) bootstrapRepo() error {
|
|
|
return err
|
|
|
}
|
|
|
tufRepo.SetTargets("targets", targets)
|
|
|
+
|
|
|
snapshotJSON, err := r.fileStore.GetMeta("snapshot", 0)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- snapshot := &data.SignedSnapshot{}
|
|
|
- err = json.Unmarshal(snapshotJSON, snapshot)
|
|
|
- if err != nil {
|
|
|
+ if err == nil {
|
|
|
+ snapshot := &data.SignedSnapshot{}
|
|
|
+ err = json.Unmarshal(snapshotJSON, snapshot)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ tufRepo.SetSnapshot(snapshot)
|
|
|
+ } else if _, ok := err.(store.ErrMetaNotFound); !ok {
|
|
|
return err
|
|
|
}
|
|
|
- tufRepo.SetSnapshot(snapshot)
|
|
|
|
|
|
r.tufRepo = tufRepo
|
|
|
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-func (r *NotaryRepository) saveMetadata() error {
|
|
|
+func (r *NotaryRepository) saveMetadata(ignoreSnapshot bool) error {
|
|
|
logrus.Debugf("Saving changes to Trusted Collection.")
|
|
|
|
|
|
- signedRoot, err := r.tufRepo.SignRoot(data.DefaultExpires("root"))
|
|
|
+ rootJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalRootRole)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- rootJSON, err := json.Marshal(signedRoot)
|
|
|
+ err = r.fileStore.SetMeta(data.CanonicalRootRole, rootJSON)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
@@ -476,27 +726,22 @@ func (r *NotaryRepository) saveMetadata() error {
|
|
|
targetsToSave[t] = targetsJSON
|
|
|
}
|
|
|
|
|
|
- signedSnapshot, err := r.tufRepo.SignSnapshot(data.DefaultExpires("snapshot"))
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
+ for role, blob := range targetsToSave {
|
|
|
+ parentDir := filepath.Dir(role)
|
|
|
+ os.MkdirAll(parentDir, 0755)
|
|
|
+ r.fileStore.SetMeta(role, blob)
|
|
|
}
|
|
|
- snapshotJSON, err := json.Marshal(signedSnapshot)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
+
|
|
|
+ if ignoreSnapshot {
|
|
|
+ return nil
|
|
|
}
|
|
|
|
|
|
- err = r.fileStore.SetMeta("root", rootJSON)
|
|
|
+ snapshotJSON, err := serializeCanonicalRole(r.tufRepo, data.CanonicalSnapshotRole)
|
|
|
if err != nil {
|
|
|
return err
|
|
|
}
|
|
|
|
|
|
- for role, blob := range targetsToSave {
|
|
|
- parentDir := filepath.Dir(role)
|
|
|
- os.MkdirAll(parentDir, 0755)
|
|
|
- r.fileStore.SetMeta(role, blob)
|
|
|
- }
|
|
|
-
|
|
|
- return r.fileStore.SetMeta("snapshot", snapshotJSON)
|
|
|
+ return r.fileStore.SetMeta(data.CanonicalSnapshotRole, snapshotJSON)
|
|
|
}
|
|
|
|
|
|
func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
|
@@ -515,11 +760,15 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
|
|
// the store and it doesn't know about the repo.
|
|
|
return nil, err
|
|
|
}
|
|
|
- rootJSON, err = r.fileStore.GetMeta("root", maxSize)
|
|
|
- if err != nil {
|
|
|
- // if cache didn't return a root, we cannot proceed
|
|
|
- return nil, store.ErrMetaNotFound{}
|
|
|
+ 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.
|
|
|
+ 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
|
|
@@ -530,7 +779,7 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
|
|
return nil, err
|
|
|
}
|
|
|
|
|
|
- err = r.KeyStoreManager.ValidateRoot(root, r.gun)
|
|
|
+ err = r.CertManager.ValidateRoot(root, r.gun)
|
|
|
if err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
@@ -555,21 +804,32 @@ func (r *NotaryRepository) bootstrapClient() (*tufclient.Client, error) {
|
|
|
), nil
|
|
|
}
|
|
|
|
|
|
-// RotateKeys removes all existing keys associated with role and adds
|
|
|
-// the keys specified by keyIDs to the role. These changes are staged
|
|
|
-// in a changelist until publish is called.
|
|
|
-func (r *NotaryRepository) RotateKeys() error {
|
|
|
- for _, role := range []string{"targets", "snapshot"} {
|
|
|
- key, err := r.CryptoService.Create(role, data.ECDSAKey)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
- err = r.rootFileKeyChange(role, changelist.ActionCreate, key)
|
|
|
- if err != nil {
|
|
|
- return err
|
|
|
- }
|
|
|
+// 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.
|
|
|
+func (r *NotaryRepository) RotateKey(role string, serverManagesKey bool) error {
|
|
|
+ if role == data.CanonicalRootRole || role == data.CanonicalTimestampRole {
|
|
|
+ return fmt.Errorf(
|
|
|
+ "notary does not currently support rotating the %s key", role)
|
|
|
}
|
|
|
- return nil
|
|
|
+ if serverManagesKey && role == data.CanonicalTargetsRole {
|
|
|
+ return ErrInvalidRemoteRole{Role: data.CanonicalTargetsRole}
|
|
|
+ }
|
|
|
+
|
|
|
+ var (
|
|
|
+ pubKey data.PublicKey
|
|
|
+ err error
|
|
|
+ )
|
|
|
+ if serverManagesKey {
|
|
|
+ pubKey, err = getRemoteKey(r.baseURL, r.gun, role, r.roundTrip)
|
|
|
+ } else {
|
|
|
+ pubKey, err = r.CryptoService.Create(role, data.ECDSAKey)
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ return r.rootFileKeyChange(role, changelist.ActionCreate, pubKey)
|
|
|
}
|
|
|
|
|
|
func (r *NotaryRepository) rootFileKeyChange(role, action string, key data.PublicKey) error {
|