This replaces the visitAll recursive function with a test that verifies that the option is set for all commands and subcommands, so that it doesn't have to be modified at runtime. We currently still have to loop over all functions for the setValidateArgs call, but that can be looked at separately. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
140 lines
4.5 KiB
Go
140 lines
4.5 KiB
Go
package trust
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/docker/cli/cli"
|
|
"github.com/docker/cli/cli/command"
|
|
"github.com/docker/cli/cli/trust"
|
|
"github.com/docker/cli/internal/lazyregexp"
|
|
"github.com/docker/cli/opts"
|
|
"github.com/spf13/cobra"
|
|
"github.com/theupdateframework/notary/client"
|
|
"github.com/theupdateframework/notary/tuf/data"
|
|
tufutils "github.com/theupdateframework/notary/tuf/utils"
|
|
)
|
|
|
|
type signerAddOptions struct {
|
|
keys opts.ListOpts
|
|
signer string
|
|
repos []string
|
|
}
|
|
|
|
func newSignerAddCommand(dockerCLI command.Cli) *cobra.Command {
|
|
var options signerAddOptions
|
|
cmd := &cobra.Command{
|
|
Use: "add OPTIONS NAME REPOSITORY [REPOSITORY...] ",
|
|
Short: "Add a signer",
|
|
Args: cli.RequiresMinArgs(2),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
options.signer = args[0]
|
|
options.repos = args[1:]
|
|
return addSigner(cmd.Context(), dockerCLI, options)
|
|
},
|
|
DisableFlagsInUseLine: true,
|
|
}
|
|
flags := cmd.Flags()
|
|
options.keys = opts.NewListOpts(nil)
|
|
flags.Var(&options.keys, "key", "Path to the signer's public key file")
|
|
return cmd
|
|
}
|
|
|
|
var validSignerName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
|
|
|
|
func addSigner(ctx context.Context, dockerCLI command.Cli, options signerAddOptions) error {
|
|
signerName := options.signer
|
|
if !validSignerName(signerName) {
|
|
return fmt.Errorf("signer name \"%s\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character", signerName)
|
|
}
|
|
if signerName == "releases" {
|
|
return errors.New("releases is a reserved keyword, use a different signer name")
|
|
}
|
|
|
|
if options.keys.Len() == 0 {
|
|
return errors.New("path to a public key must be provided using the `--key` flag")
|
|
}
|
|
signerPubKeys, err := ingestPublicKeys(options.keys.GetSlice())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var errRepos []string
|
|
for _, repoName := range options.repos {
|
|
_, _ = fmt.Fprintf(dockerCLI.Out(), "Adding signer \"%s\" to %s...\n", signerName, repoName)
|
|
if err := addSignerToRepo(ctx, dockerCLI, signerName, repoName, signerPubKeys); err != nil {
|
|
_, _ = fmt.Fprintln(dockerCLI.Err(), err.Error()+"\n")
|
|
errRepos = append(errRepos, repoName)
|
|
} else {
|
|
_, _ = fmt.Fprintf(dockerCLI.Out(), "Successfully added signer: %s to %s\n\n", signerName, repoName)
|
|
}
|
|
}
|
|
if len(errRepos) > 0 {
|
|
return fmt.Errorf("failed to add signer to: %s", strings.Join(errRepos, ", "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func addSignerToRepo(ctx context.Context, dockerCLI command.Cli, signerName string, repoName string, signerPubKeys []data.PublicKey) error {
|
|
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, authResolver(dockerCLI), repoName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
notaryRepo, err := newNotaryClient(dockerCLI, imgRefAndAuth, trust.ActionsPushAndPull)
|
|
if err != nil {
|
|
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
|
|
}
|
|
|
|
if _, err = notaryRepo.ListTargets(); err != nil {
|
|
switch err.(type) {
|
|
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
|
|
_, _ = fmt.Fprintf(dockerCLI.Out(), "Initializing signed repository for %s...\n", repoName)
|
|
if err := getOrGenerateRootKeyAndInitRepo(notaryRepo); err != nil {
|
|
return trust.NotaryError(repoName, err)
|
|
}
|
|
_, _ = fmt.Fprintf(dockerCLI.Out(), "Successfully initialized %q\n", repoName)
|
|
default:
|
|
return trust.NotaryError(repoName, err)
|
|
}
|
|
}
|
|
|
|
newSignerRoleName := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), signerName))
|
|
|
|
if err := addStagedSigner(notaryRepo, newSignerRoleName, signerPubKeys); err != nil {
|
|
return fmt.Errorf("could not add signer to repo: %s: %w", strings.TrimPrefix(newSignerRoleName.String(), "targets/"), err)
|
|
}
|
|
|
|
return notaryRepo.Publish()
|
|
}
|
|
|
|
func ingestPublicKeys(pubKeyPaths []string) ([]data.PublicKey, error) {
|
|
pubKeys := []data.PublicKey{}
|
|
for _, pubKeyPath := range pubKeyPaths {
|
|
// Read public key bytes from PEM file, limit to 1 KiB
|
|
pubKeyFile, err := os.OpenFile(pubKeyPath, os.O_RDONLY, 0o666)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read public key from file: %w", err)
|
|
}
|
|
defer pubKeyFile.Close()
|
|
// limit to
|
|
l := io.LimitReader(pubKeyFile, 1<<20)
|
|
pubKeyBytes, err := io.ReadAll(l)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to read public key from file: %w", err)
|
|
}
|
|
|
|
// Parse PEM bytes into type PublicKey
|
|
pubKey, err := tufutils.ParsePEMPublicKey(pubKeyBytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("could not parse public key from file: %s: %w", pubKeyPath, err)
|
|
}
|
|
pubKeys = append(pubKeys, pubKey)
|
|
}
|
|
return pubKeys, nil
|
|
}
|