diff --git a/pkg/tuf/common.go b/pkg/tuf/common.go index 1f4b17a2..872869da 100644 --- a/pkg/tuf/common.go +++ b/pkg/tuf/common.go @@ -13,10 +13,12 @@ import ( "github.com/docker/distribution/reference" "github.com/docker/docker/registry" + "github.com/theupdateframework/notary/tuf/data" ) const ( - dockerConfigDir = ".docker" + dockerConfigDir = ".docker" + releasesRoleName = data.RoleName("targets/releases") ) func DefaultTrustDir() string { diff --git a/pkg/tuf/delegations.go b/pkg/tuf/delegations.go new file mode 100644 index 00000000..473fc13e --- /dev/null +++ b/pkg/tuf/delegations.go @@ -0,0 +1,25 @@ +package tuf + +import ( + "fmt" + + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/tuf/data" +) + +// Delegate all paths ("*") to targets/releases. +// https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/delegations.go +func delegateToReleases(repo client.Repository, publicKey data.PublicKey) error { + // How Notary v1 denotes "*"" + // https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/delegations.go#L367 + allPaths := []string{""} + publicKeys := []data.PublicKey{publicKey} + + // Add the delegation to the repository + err := repo.AddDelegation(releasesRoleName, publicKeys, allPaths) + if err != nil { + return fmt.Errorf("failed to create delegation: %v", err) + } + + return nil +} diff --git a/pkg/tuf/keys.go b/pkg/tuf/keys.go index 2edcc43d..9dbdc38d 100644 --- a/pkg/tuf/keys.go +++ b/pkg/tuf/keys.go @@ -24,9 +24,9 @@ import ( func getPassphraseRetriever() notary.PassRetriever { baseRetriever := passphrase.PromptRetriever() env := map[string]string{ - "root": os.Getenv("SIGNY_ROOT_PASSPHRASE"), - "targets": os.Getenv("SIGNY_TARGETS_PASSPHRASE"), - "releases": os.Getenv("SIGNY_RELEASES_PASSPHRASE"), + "root": os.Getenv("SIGNY_ROOT_PASSPHRASE"), + "targets": os.Getenv("SIGNY_TARGETS_PASSPHRASE"), + "targets/releases": os.Getenv("SIGNY_RELEASES_PASSPHRASE"), } return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { @@ -39,11 +39,12 @@ func getPassphraseRetriever() notary.PassRetriever { // Attempt to read a role key from a file, and return it as a data.PrivateKey // If key is for the Root role, it must be encrypted -func readKey(role data.RoleName, keyFilename string, retriever notary.PassRetriever) (data.PrivateKey, error) { +func readPrivateKey(role data.RoleName, keyFilename string, retriever notary.PassRetriever) (data.PrivateKey, error) { pemBytes, err := ioutil.ReadFile(keyFilename) if err != nil { return nil, fmt.Errorf("Error reading input root key file: %v", err) } + isEncrypted := true if err = cryptoservice.CheckRootKeyIsEncrypted(pemBytes); err != nil { if role == data.CanonicalRootRole { @@ -51,6 +52,7 @@ func readKey(role data.RoleName, keyFilename string, retriever notary.PassRetrie } isEncrypted = false } + var privKey data.PrivateKey if isEncrypted { privKey, _, err = trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", data.CanonicalRootRole.String()) @@ -71,7 +73,7 @@ func importRootKey(rootKey string, nRepo client.Repository, retriever notary.Pas var rootKeyList []string if rootKey != "" { - privKey, err := readKey(data.CanonicalRootRole, rootKey, retriever) + privKey, err := readPrivateKey(data.CanonicalRootRole, rootKey, retriever) if err != nil { return nil, err } @@ -89,57 +91,71 @@ func importRootKey(rootKey string, nRepo client.Repository, retriever notary.Pas // Chooses the first root key available, which is initialization specific // but should return the HW one first. rootKeyID := rootKeyList[0] - log.Debugf("Signy found root key, using: %s\n", rootKeyID) - + log.Debugf("found root key: %s\n", rootKeyID) return []string{rootKeyID}, nil } return []string{}, nil } +// Try to reuse a single targets/releases key across repositories. +func reuseReleasesKey(r client.Repository) (data.PublicKey, error) { + // Get all known targets keys. + cryptoService := r.GetCryptoService() + keyList := cryptoService.ListKeys(releasesRoleName) + + // Try to extract a single targets/releases key we can reuse. + switch len(keyList) { + case 0: + log.Debug("No %s key available, need to make one", releasesRoleName) + return cryptoService.Create(releasesRoleName, r.GetGUN(), data.ECDSAKey) + case 1: + log.Debug("Nothing to do, only one %s key available", releasesRoleName) + return cryptoService.GetKey(keyList[0]), nil + default: + return nil, fmt.Errorf("there is more than one %s keys", releasesRoleName) + } +} + // Try to reuse a single targets key across repositories. // FIXME: Unfortunately, short of forking Notary or sending a PR upstream, there isn't an easy way to prevent it // from automagically creating a new, local targets key per TUF metadata repository. We fix this here by undoing // more than one new, local targets key, and reusing any existing local targets key, just like the way Notary // reuses the root key. func reuseTargetsKey(r client.Repository) error { - var ( - err error - thisTargetsKeyID, thatTargetsKeyID string - ) - // Get all known targets keys. - targetsKeyList := r.GetCryptoService().ListKeys(data.CanonicalTargetsRole) + keyList := r.GetCryptoService().ListKeys(data.CanonicalTargetsRole) + // Try to extract a single targets key we can reuse. - switch len(targetsKeyList) { + switch len(keyList) { case 0: - err = fmt.Errorf("no targets key despite having initialized a repo") + return fmt.Errorf("no targets key despite having initialized a repo") case 1: log.Debug("Nothing to do, only one targets key available") + return nil case 2: // First, we publish current changes to repository in order to list roles. // FIXME: Find a find better way to list roles w/o publishing changes first. - publishErr := r.Publish() - if publishErr != nil { - err = publishErr - break + err := r.Publish() + if err != nil { + return err } // Get the current top-level roles. - roleWithSigs, listRolesErr := r.ListRoles() - if listRolesErr != nil { - err = listRolesErr - break + roleWithSigs, err := r.ListRoles() + if err != nil { + return err } // Get the current targets key. // NOTE: We do not delete it, in case the user wants to keep it. + var thisKeyID string for _, roleWithSig := range roleWithSigs { role := roleWithSig.Role if role.Name == data.CanonicalTargetsRole { if len(role.KeyIDs) == 1 { - thisTargetsKeyID = role.KeyIDs[0] - log.Debugf("This targets keyid: %s", thisTargetsKeyID) + thisKeyID = role.KeyIDs[0] + log.Debugf("This targets keyid: %s", thisKeyID) } else { return fmt.Errorf("this targets role has more than 1 key") } @@ -147,19 +163,19 @@ func reuseTargetsKey(r client.Repository) error { } // Get and reuse the other targets key. - for _, keyID := range targetsKeyList { - if keyID != thisTargetsKeyID { - thatTargetsKeyID = keyID + var thatKeyID string + for _, keyID := range keyList { + if keyID != thisKeyID { + thatKeyID = keyID break } } - log.Debugf("That targets keyID: %s", thatTargetsKeyID) - log.Debugf("Before rotating targets key from %s to %s", thisTargetsKeyID, thatTargetsKeyID) - err = r.RotateKey(data.CanonicalTargetsRole, false, []string{thatTargetsKeyID}) + log.Debugf("That targets keyID: %s", thatKeyID) + log.Debugf("Before rotating targets key from %s to %s", thisKeyID, thatKeyID) + err = r.RotateKey(data.CanonicalTargetsRole, false, []string{thatKeyID}) log.Debugf("After targets key rotation") + return err default: - err = fmt.Errorf("there are more than 2 targets keys") + return fmt.Errorf("there are more than two targets keys") } - - return err } diff --git a/pkg/tuf/sign.go b/pkg/tuf/sign.go index 9fce8292..d87b1960 100644 --- a/pkg/tuf/sign.go +++ b/pkg/tuf/sign.go @@ -9,15 +9,6 @@ import ( "github.com/theupdateframework/notary/tuf/data" ) -// clearChangelist clears the notary staging changelist -func clearChangeList(notaryRepo client.Repository) error { - cl, err := notaryRepo.GetChangelist() - if err != nil { - return err - } - return cl.Clear("") -} - // SignAndPublish signs an artifact, then publishes the metadata to a trust server func SignAndPublish(trustDir, trustServer, ref, file, tlscacert, rootKey, timeout string, custom *canonicaljson.RawMessage) (*client.Target, error) { if err := EnsureTrustDir(trustDir); err != nil { @@ -50,48 +41,74 @@ func SignAndPublish(trustDir, trustServer, ref, file, tlscacert, rootKey, timeou if err != nil { return nil, fmt.Errorf("cannot clear change list: %v", err) } - defer clearChangeList(repo) - if _, err = repo.ListTargets(); err != nil { + err = reuseKeys(repo, rootKey) + if err != nil { + return nil, fmt.Errorf("cannot reuse keys: %v", err) + } + + target, err := client.NewTarget(tag, file, custom) + if err != nil { + return nil, err + } + + // If roles is empty, we default to adding to targets + if err = repo.AddTarget(target, data.NewRoleList([]string{})...); err != nil { + return nil, err + } + + err = repo.Publish() + return target, err +} + +// reuse root and top-level targets keys +func reuseKeys(repo client.Repository, rootKey string) error { + if _, err := repo.ListTargets(); err != nil { switch err.(type) { case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: // Reuse root key. rootKeyIDs, err := importRootKey(rootKey, repo, getPassphraseRetriever()) if err != nil { - return nil, err + return err } // NOTE: 2nd variadic argument is to indicate that snapshot is managed remotely. // The impact of a timestamp + snapshot key compromise is not terrible: // https://docs.docker.com/notary/service_architecture/#threat-model if err = repo.Initialize(rootKeyIDs, data.CanonicalSnapshotRole); err != nil { - return nil, fmt.Errorf("cannot initialize repo: %v", err) + return fmt.Errorf("cannot initialize repo: %v", err) } // Reuse targets key. if err = reuseTargetsKey(repo); err != nil { - return nil, fmt.Errorf("cannot reuse targets keys: %v", err) + return fmt.Errorf("cannot reuse %s keys: %v", data.CanonicalTargetsRole) + } + + // Reuse targets/releases key. + releasesPublicKey, err := reuseReleasesKey(repo) + if err != nil { + return fmt.Errorf("cannot reuse %s keys: %v", releasesRoleName, err) + } + + // Delegate to targets/releases. + err = delegateToReleases(repo, releasesPublicKey) + if err != nil { + return fmt.Errorf("cannot delegate to %s: %v", releasesRoleName, err) } default: - return nil, fmt.Errorf("cannot list targets: %v", err) + return fmt.Errorf("cannot list targets: %v", err) } } + return nil +} - target, err := client.NewTarget(tag, file, custom) +// clearChangelist clears the notary staging changelist +func clearChangeList(notaryRepo client.Repository) error { + cl, err := notaryRepo.GetChangelist() if err != nil { - return nil, err - } - - // TODO - Radu M - // decide whether to allow actually passing roles as flags - - // If roles is empty, we default to adding to targets - if err = repo.AddTarget(target, data.NewRoleList([]string{})...); err != nil { - return nil, err + return err } - - err = repo.Publish() - return target, err + return cl.Clear("") }