github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/command/trust/signer_add.go (about)

     1  package trust
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path"
    10  	"regexp"
    11  	"strings"
    12  
    13  	"github.com/docker/cli/cli"
    14  	"github.com/docker/cli/cli/command"
    15  	"github.com/docker/cli/cli/command/image"
    16  	"github.com/docker/cli/cli/trust"
    17  	"github.com/docker/cli/opts"
    18  	"github.com/pkg/errors"
    19  	"github.com/spf13/cobra"
    20  	"github.com/theupdateframework/notary/client"
    21  	"github.com/theupdateframework/notary/tuf/data"
    22  	tufutils "github.com/theupdateframework/notary/tuf/utils"
    23  )
    24  
    25  type signerAddOptions struct {
    26  	keys   opts.ListOpts
    27  	signer string
    28  	repos  []string
    29  }
    30  
    31  func newSignerAddCommand(dockerCli command.Cli) *cobra.Command {
    32  	var options signerAddOptions
    33  	cmd := &cobra.Command{
    34  		Use:   "add OPTIONS NAME REPOSITORY [REPOSITORY...] ",
    35  		Short: "Add a signer",
    36  		Args:  cli.RequiresMinArgs(2),
    37  		RunE: func(cmd *cobra.Command, args []string) error {
    38  			options.signer = args[0]
    39  			options.repos = args[1:]
    40  			return addSigner(dockerCli, options)
    41  		},
    42  	}
    43  	flags := cmd.Flags()
    44  	options.keys = opts.NewListOpts(nil)
    45  	flags.Var(&options.keys, "key", "Path to the signer's public key file")
    46  	return cmd
    47  }
    48  
    49  var validSignerName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
    50  
    51  func addSigner(cli command.Cli, options signerAddOptions) error {
    52  	signerName := options.signer
    53  	if !validSignerName(signerName) {
    54  		return fmt.Errorf("signer name \"%s\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character", signerName)
    55  	}
    56  	if signerName == "releases" {
    57  		return fmt.Errorf("releases is a reserved keyword, please use a different signer name")
    58  	}
    59  
    60  	if options.keys.Len() == 0 {
    61  		return fmt.Errorf("path to a public key must be provided using the `--key` flag")
    62  	}
    63  	signerPubKeys, err := ingestPublicKeys(options.keys.GetAll())
    64  	if err != nil {
    65  		return err
    66  	}
    67  	var errRepos []string
    68  	for _, repoName := range options.repos {
    69  		fmt.Fprintf(cli.Out(), "Adding signer \"%s\" to %s...\n", signerName, repoName)
    70  		if err := addSignerToRepo(cli, signerName, repoName, signerPubKeys); err != nil {
    71  			fmt.Fprintln(cli.Err(), err.Error()+"\n")
    72  			errRepos = append(errRepos, repoName)
    73  		} else {
    74  			fmt.Fprintf(cli.Out(), "Successfully added signer: %s to %s\n\n", signerName, repoName)
    75  		}
    76  	}
    77  	if len(errRepos) > 0 {
    78  		return fmt.Errorf("Failed to add signer to: %s", strings.Join(errRepos, ", "))
    79  	}
    80  	return nil
    81  }
    82  
    83  func addSignerToRepo(cli command.Cli, signerName string, repoName string, signerPubKeys []data.PublicKey) error {
    84  	ctx := context.Background()
    85  	imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, nil, image.AuthResolver(cli), repoName)
    86  	if err != nil {
    87  		return err
    88  	}
    89  
    90  	notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPushAndPull)
    91  	if err != nil {
    92  		return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
    93  	}
    94  
    95  	if _, err = notaryRepo.ListTargets(); err != nil {
    96  		switch err.(type) {
    97  		case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
    98  			fmt.Fprintf(cli.Out(), "Initializing signed repository for %s...\n", repoName)
    99  			if err := getOrGenerateRootKeyAndInitRepo(notaryRepo); err != nil {
   100  				return trust.NotaryError(repoName, err)
   101  			}
   102  			fmt.Fprintf(cli.Out(), "Successfully initialized %q\n", repoName)
   103  		default:
   104  			return trust.NotaryError(repoName, err)
   105  		}
   106  	}
   107  
   108  	newSignerRoleName := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), signerName))
   109  
   110  	if err := addStagedSigner(notaryRepo, newSignerRoleName, signerPubKeys); err != nil {
   111  		return errors.Wrapf(err, "could not add signer to repo: %s", strings.TrimPrefix(newSignerRoleName.String(), "targets/"))
   112  	}
   113  
   114  	return notaryRepo.Publish()
   115  }
   116  
   117  func ingestPublicKeys(pubKeyPaths []string) ([]data.PublicKey, error) {
   118  	pubKeys := []data.PublicKey{}
   119  	for _, pubKeyPath := range pubKeyPaths {
   120  		// Read public key bytes from PEM file, limit to 1 KiB
   121  		pubKeyFile, err := os.OpenFile(pubKeyPath, os.O_RDONLY, 0666)
   122  		if err != nil {
   123  			return nil, errors.Wrap(err, "unable to read public key from file")
   124  		}
   125  		defer pubKeyFile.Close()
   126  		// limit to
   127  		l := io.LimitReader(pubKeyFile, 1<<20)
   128  		pubKeyBytes, err := ioutil.ReadAll(l)
   129  		if err != nil {
   130  			return nil, errors.Wrap(err, "unable to read public key from file")
   131  		}
   132  
   133  		// Parse PEM bytes into type PublicKey
   134  		pubKey, err := tufutils.ParsePEMPublicKey(pubKeyBytes)
   135  		if err != nil {
   136  			return nil, errors.Wrapf(err, "could not parse public key from file: %s", pubKeyPath)
   137  		}
   138  		pubKeys = append(pubKeys, pubKey)
   139  	}
   140  	return pubKeys, nil
   141  }