diff --git a/pkg/tuf/delegations.go b/pkg/tuf/delegations.go new file mode 100644 index 00000000..63319662 --- /dev/null +++ b/pkg/tuf/delegations.go @@ -0,0 +1,58 @@ +package tuf + +import ( + "fmt" + "strings" + + "github.com/theupdateframework/notary/client" + "github.com/theupdateframework/notary/tuf/data" + "github.com/theupdateframework/notary/tuf/utils" +) + +// delegationAdd creates a new delegation by adding a public key from a certificate to a specific role in a GUN +// https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/delegations.go +func delegateToReleases(repo client.Repository, releasesKeyID string) error { + role := data.RoleName("targets/releases") + // How Notary v1 denotes "*"" + // https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/delegations.go#L367 + allPaths := []string{""} + + pubKeys, err := ingestPublicKeys(args) + if err != nil { + return err + } + + // Add the delegation to the repository + err = repo.AddDelegation(role, pubKeys, allPaths) + if err != nil { + return fmt.Errorf("failed to create delegation: %v", err) + } + + // Make keyID slice for better CLI print + pubKeyIDs := []string{} + for _, pubKey := range pubKeys { + pubKeyID, err := utils.CanonicalKeyID(pubKey) + if err != nil { + return err + } + pubKeyIDs = append(pubKeyIDs, pubKeyID) + } + + fmt.Println("") + addingItems := "" + if len(pubKeyIDs) > 0 { + addingItems = addingItems + fmt.Sprintf("with keys %s, ", pubKeyIDs) + } + if d.paths != nil || d.allPaths { + addingItems = addingItems + fmt.Sprintf( + "with paths [%s], ", + strings.Join(prettyPaths(d.paths), "\n"), + ) + } + fmt.Printf( + "Addition of delegation role %s %sto repository \"%s\" staged for next publish.\n", + role, addingItems, gun) + fmt.Println("") + + return maybeAutoPublish(cmd, d.autoPublish, gun, config, d.retriever) +} diff --git a/pkg/tuf/keys.go b/pkg/tuf/keys.go index 2edcc43d..8b5f1edd 100644 --- a/pkg/tuf/keys.go +++ b/pkg/tuf/keys.go @@ -39,7 +39,7 @@ 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) @@ -64,6 +64,32 @@ func readKey(role data.RoleName, keyFilename string, retriever notary.PassRetrie return privKey, nil } +// Attempt to read a role key from a file, and return it as a data.PrivateKey +func readPublicKey(args []string) ([]data.PublicKey, error) { + pubKeys := []data.PublicKey{} + if len(args) > 2 { + pubKeyPaths := args[2:] + for _, pubKeyPath := range pubKeyPaths { + // Read public key bytes from PEM file + pubKeyBytes, err := ioutil.ReadFile(pubKeyPath) + if err != nil { + if os.IsNotExist(err) { + return nil, fmt.Errorf("file for public key does not exist: %s", pubKeyPath) + } + return nil, fmt.Errorf("unable to read public key from file: %s", pubKeyPath) + } + + // Parse PEM bytes into type PublicKey + pubKey, err := utils.ParsePEMPublicKey(pubKeyBytes) + if err != nil { + return nil, fmt.Errorf("unable to parse valid public key certificate from PEM file %s: %v", pubKeyPath, err) + } + pubKeys = append(pubKeys, pubKey) + } + } + return pubKeys, nil +} + // importRootKey imports the root key from path then adds the key to repo // returns key ids // https://github.com/theupdateframework/notary/blob/f255ae779066dc28ae4aee196061e58bb38a2b49/cmd/notary/tuf.go#L413 @@ -71,7 +97,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 } @@ -97,25 +123,25 @@ func importRootKey(rootKey string, nRepo client.Repository, retriever notary.Pas return []string{}, nil } -// Try to reuse a single targets key across repositories. +// Try to reuse a single key for the given rolename 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 { +func reuseKey(r client.Repository, rolename data.RoleName) (string, error) { var ( - err error - thisTargetsKeyID, thatTargetsKeyID string + err error + thisKeyID, thatKeyID string ) - // Get all known targets keys. - targetsKeyList := r.GetCryptoService().ListKeys(data.CanonicalTargetsRole) - // Try to extract a single targets key we can reuse. - switch len(targetsKeyList) { + // Get all known keys for this rolename. + keyList := r.GetCryptoService().ListKeys(rolename) + // Try to extract a single key we can reuse. + switch len(keyList) { case 0: - err = fmt.Errorf("no targets key despite having initialized a repo") + err = fmt.Errorf("no %s key despite having initialized a repo", rolename) case 1: - log.Debug("Nothing to do, only one targets key available") + log.Debug("Nothing to do, only one %s key available", rolename) 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. @@ -132,34 +158,34 @@ func reuseTargetsKey(r client.Repository) error { break } - // Get the current targets key. + // Get the current key for the given rolename. // NOTE: We do not delete it, in case the user wants to keep it. for _, roleWithSig := range roleWithSigs { role := roleWithSig.Role - if role.Name == data.CanonicalTargetsRole { + if role.Name == rolename { if len(role.KeyIDs) == 1 { - thisTargetsKeyID = role.KeyIDs[0] - log.Debugf("This targets keyid: %s", thisTargetsKeyID) + thisKeyID = role.KeyIDs[0] + log.Debugf("This %s keyid: %s", rolename, thisKeyID) } else { - return fmt.Errorf("this targets role has more than 1 key") + return thatKeyID, fmt.Errorf("this %s role has more than 1 key", rolename) } } } - // Get and reuse the other targets key. - for _, keyID := range targetsKeyList { - if keyID != thisTargetsKeyID { - thatTargetsKeyID = keyID + // Get and reuse the other key for the given rolename. + 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("After targets key rotation") + log.Debugf("That %s keyID: %s", rolename, thatKeyID) + log.Debugf("Before rotating %s key from %s to %s", rolename, thisKeyID, thatKeyID) + err = r.RotateKey(rolename, false, []string{thatKeyID}) + log.Debugf("After %s key rotation", rolename) default: - err = fmt.Errorf("there are more than 2 targets keys") + err = fmt.Errorf("there are more than 2 %s keys", rolename) } - return err + return thatKeyID, err } diff --git a/pkg/tuf/sign.go b/pkg/tuf/sign.go index 9fce8292..28d70a32 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,76 @@ 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) + if _, err := reuseKey(repo, data.CanonicalTargetsRole); err != nil { + return fmt.Errorf("cannot reuse %s keys: %v", data.CanonicalTargetsRole) + } + + // Reuse targets/releases key. + releasesRoleName := data.RoleName("targets/releases") + // FIXME: logic is faulty right now, because targets/releases will not exist by default. + releasesKeyID, err := reuseKey(repo, releasesRoleName) + if err != nil { + return fmt.Errorf("cannot reuse %s keys: %v", releasesRoleName, err) + } + + // Delegate to targets/releases. + err = delegateToReleases(repo, releasesKeyID) + 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("") }