github.com/hamo/docker@v1.11.1/api/client/trust.go (about)

     1  package client
     2  
     3  import (
     4  	"encoding/hex"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"net"
     9  	"net/http"
    10  	"net/url"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"sort"
    15  	"strconv"
    16  	"time"
    17  
    18  	"golang.org/x/net/context"
    19  
    20  	"github.com/Sirupsen/logrus"
    21  	"github.com/docker/distribution/digest"
    22  	"github.com/docker/distribution/registry/client/auth"
    23  	"github.com/docker/distribution/registry/client/transport"
    24  	"github.com/docker/docker/cliconfig"
    25  	"github.com/docker/docker/distribution"
    26  	"github.com/docker/docker/pkg/jsonmessage"
    27  	flag "github.com/docker/docker/pkg/mflag"
    28  	"github.com/docker/docker/reference"
    29  	"github.com/docker/docker/registry"
    30  	apiclient "github.com/docker/engine-api/client"
    31  	"github.com/docker/engine-api/types"
    32  	registrytypes "github.com/docker/engine-api/types/registry"
    33  	"github.com/docker/go-connections/tlsconfig"
    34  	"github.com/docker/notary/client"
    35  	"github.com/docker/notary/passphrase"
    36  	"github.com/docker/notary/trustmanager"
    37  	"github.com/docker/notary/tuf/data"
    38  	"github.com/docker/notary/tuf/signed"
    39  	"github.com/docker/notary/tuf/store"
    40  )
    41  
    42  var (
    43  	releasesRole = path.Join(data.CanonicalTargetsRole, "releases")
    44  	untrusted    bool
    45  )
    46  
    47  func addTrustedFlags(fs *flag.FlagSet, verify bool) {
    48  	var trusted bool
    49  	if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
    50  		if t, err := strconv.ParseBool(e); t || err != nil {
    51  			// treat any other value as true
    52  			trusted = true
    53  		}
    54  	}
    55  	message := "Skip image signing"
    56  	if verify {
    57  		message = "Skip image verification"
    58  	}
    59  	fs.BoolVar(&untrusted, []string{"-disable-content-trust"}, !trusted, message)
    60  }
    61  
    62  func isTrusted() bool {
    63  	return !untrusted
    64  }
    65  
    66  type target struct {
    67  	reference registry.Reference
    68  	digest    digest.Digest
    69  	size      int64
    70  }
    71  
    72  func (cli *DockerCli) trustDirectory() string {
    73  	return filepath.Join(cliconfig.ConfigDir(), "trust")
    74  }
    75  
    76  // certificateDirectory returns the directory containing
    77  // TLS certificates for the given server. An error is
    78  // returned if there was an error parsing the server string.
    79  func (cli *DockerCli) certificateDirectory(server string) (string, error) {
    80  	u, err := url.Parse(server)
    81  	if err != nil {
    82  		return "", err
    83  	}
    84  
    85  	return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil
    86  }
    87  
    88  func trustServer(index *registrytypes.IndexInfo) (string, error) {
    89  	if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" {
    90  		urlObj, err := url.Parse(s)
    91  		if err != nil || urlObj.Scheme != "https" {
    92  			return "", fmt.Errorf("valid https URL required for trust server, got %s", s)
    93  		}
    94  
    95  		return s, nil
    96  	}
    97  	if index.Official {
    98  		return registry.NotaryServer, nil
    99  	}
   100  	return "https://" + index.Name, nil
   101  }
   102  
   103  type simpleCredentialStore struct {
   104  	auth types.AuthConfig
   105  }
   106  
   107  func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
   108  	return scs.auth.Username, scs.auth.Password
   109  }
   110  
   111  func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string {
   112  	return scs.auth.IdentityToken
   113  }
   114  
   115  func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {
   116  }
   117  
   118  // getNotaryRepository returns a NotaryRepository which stores all the
   119  // information needed to operate on a notary repository.
   120  // It creates a HTTP transport providing authentication support.
   121  func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) {
   122  	server, err := trustServer(repoInfo.Index)
   123  	if err != nil {
   124  		return nil, err
   125  	}
   126  
   127  	var cfg = tlsconfig.ClientDefault
   128  	cfg.InsecureSkipVerify = !repoInfo.Index.Secure
   129  
   130  	// Get certificate base directory
   131  	certDir, err := cli.certificateDirectory(server)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  	logrus.Debugf("reading certificate directory: %s", certDir)
   136  
   137  	if err := registry.ReadCertsDirectory(&cfg, certDir); err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	base := &http.Transport{
   142  		Proxy: http.ProxyFromEnvironment,
   143  		Dial: (&net.Dialer{
   144  			Timeout:   30 * time.Second,
   145  			KeepAlive: 30 * time.Second,
   146  			DualStack: true,
   147  		}).Dial,
   148  		TLSHandshakeTimeout: 10 * time.Second,
   149  		TLSClientConfig:     &cfg,
   150  		DisableKeepAlives:   true,
   151  	}
   152  
   153  	// Skip configuration headers since request is not going to Docker daemon
   154  	modifiers := registry.DockerHeaders(clientUserAgent(), http.Header{})
   155  	authTransport := transport.NewTransport(base, modifiers...)
   156  	pingClient := &http.Client{
   157  		Transport: authTransport,
   158  		Timeout:   5 * time.Second,
   159  	}
   160  	endpointStr := server + "/v2/"
   161  	req, err := http.NewRequest("GET", endpointStr, nil)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	challengeManager := auth.NewSimpleChallengeManager()
   167  
   168  	resp, err := pingClient.Do(req)
   169  	if err != nil {
   170  		// Ignore error on ping to operate in offline mode
   171  		logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err)
   172  	} else {
   173  		defer resp.Body.Close()
   174  
   175  		// Add response to the challenge manager to parse out
   176  		// authentication header and register authentication method
   177  		if err := challengeManager.AddResponse(resp); err != nil {
   178  			return nil, err
   179  		}
   180  	}
   181  
   182  	creds := simpleCredentialStore{auth: authConfig}
   183  	tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...)
   184  	basicHandler := auth.NewBasicHandler(creds)
   185  	modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)))
   186  	tr := transport.NewTransport(base, modifiers...)
   187  
   188  	return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever())
   189  }
   190  
   191  func convertTarget(t client.Target) (target, error) {
   192  	h, ok := t.Hashes["sha256"]
   193  	if !ok {
   194  		return target{}, errors.New("no valid hash, expecting sha256")
   195  	}
   196  	return target{
   197  		reference: registry.ParseReference(t.Name),
   198  		digest:    digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
   199  		size:      t.Length,
   200  	}, nil
   201  }
   202  
   203  func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever {
   204  	aliasMap := map[string]string{
   205  		"root":     "root",
   206  		"snapshot": "repository",
   207  		"targets":  "repository",
   208  		"default":  "repository",
   209  	}
   210  	baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out, aliasMap)
   211  	env := map[string]string{
   212  		"root":     os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"),
   213  		"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
   214  		"targets":  os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
   215  		"default":  os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
   216  	}
   217  
   218  	// Backwards compatibility with old env names. We should remove this in 1.10
   219  	if env["root"] == "" {
   220  		if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE"); passphrase != "" {
   221  			env["root"] = passphrase
   222  			fmt.Fprintf(cli.err, "[DEPRECATED] The environment variable DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE has been deprecated and will be removed in v1.10. Please use DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE\n")
   223  		}
   224  	}
   225  	if env["snapshot"] == "" || env["targets"] == "" || env["default"] == "" {
   226  		if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE"); passphrase != "" {
   227  			env["snapshot"] = passphrase
   228  			env["targets"] = passphrase
   229  			env["default"] = passphrase
   230  			fmt.Fprintf(cli.err, "[DEPRECATED] The environment variable DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE has been deprecated and will be removed in v1.10. Please use DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE\n")
   231  		}
   232  	}
   233  
   234  	return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
   235  		if v := env[alias]; v != "" {
   236  			return v, numAttempts > 1, nil
   237  		}
   238  		// For non-root roles, we can also try the "default" alias if it is specified
   239  		if v := env["default"]; v != "" && alias != data.CanonicalRootRole {
   240  			return v, numAttempts > 1, nil
   241  		}
   242  		return baseRetriever(keyName, alias, createNew, numAttempts)
   243  	}
   244  }
   245  
   246  func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Canonical, error) {
   247  	repoInfo, err := registry.ParseRepositoryInfo(ref)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  
   252  	// Resolve the Auth config relevant for this server
   253  	authConfig := cli.resolveAuthConfig(repoInfo.Index)
   254  
   255  	notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull")
   256  	if err != nil {
   257  		fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
   258  		return nil, err
   259  	}
   260  
   261  	t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	// Only list tags in the top level targets role or the releases delegation role - ignore
   266  	// all other delegation roles
   267  	if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole {
   268  		return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag()))
   269  	}
   270  	r, err := convertTarget(t.Target)
   271  	if err != nil {
   272  		return nil, err
   273  
   274  	}
   275  
   276  	return reference.WithDigest(ref, r.digest)
   277  }
   278  
   279  func (cli *DockerCli) tagTrusted(trustedRef reference.Canonical, ref reference.NamedTagged) error {
   280  	fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String())
   281  
   282  	options := types.ImageTagOptions{
   283  		ImageID:        trustedRef.String(),
   284  		RepositoryName: trustedRef.Name(),
   285  		Tag:            ref.Tag(),
   286  		Force:          true,
   287  	}
   288  
   289  	return cli.client.ImageTag(context.Background(), options)
   290  }
   291  
   292  func notaryError(repoName string, err error) error {
   293  	switch err.(type) {
   294  	case *json.SyntaxError:
   295  		logrus.Debugf("Notary syntax error: %s", err)
   296  		return fmt.Errorf("Error: no trust data available for remote repository %s. Try running notary server and setting DOCKER_CONTENT_TRUST_SERVER to its HTTPS address?", repoName)
   297  	case signed.ErrExpired:
   298  		return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err)
   299  	case trustmanager.ErrKeyNotFound:
   300  		return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err)
   301  	case *net.OpError:
   302  		return fmt.Errorf("Error: error contacting notary server: %v", err)
   303  	case store.ErrMetaNotFound:
   304  		return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err)
   305  	case signed.ErrInvalidKeyType:
   306  		return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err)
   307  	case signed.ErrNoKeys:
   308  		return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err)
   309  	case signed.ErrLowVersion:
   310  		return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err)
   311  	case signed.ErrRoleThreshold:
   312  		return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err)
   313  	case client.ErrRepositoryNotExist:
   314  		return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err)
   315  	case signed.ErrInsufficientSignatures:
   316  		return fmt.Errorf("Error: could not produce valid signature for %s.  If Yubikey was used, was touch input provided?: %v", repoName, err)
   317  	}
   318  
   319  	return err
   320  }
   321  
   322  func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error {
   323  	var refs []target
   324  
   325  	notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull")
   326  	if err != nil {
   327  		fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
   328  		return err
   329  	}
   330  
   331  	if ref.String() == "" {
   332  		// List all targets
   333  		targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole)
   334  		if err != nil {
   335  			return notaryError(repoInfo.FullName(), err)
   336  		}
   337  		for _, tgt := range targets {
   338  			t, err := convertTarget(tgt.Target)
   339  			if err != nil {
   340  				fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name())
   341  				continue
   342  			}
   343  			// Only list tags in the top level targets role or the releases delegation role - ignore
   344  			// all other delegation roles
   345  			if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole {
   346  				continue
   347  			}
   348  			refs = append(refs, t)
   349  		}
   350  		if len(refs) == 0 {
   351  			return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName()))
   352  		}
   353  	} else {
   354  		t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole)
   355  		if err != nil {
   356  			return notaryError(repoInfo.FullName(), err)
   357  		}
   358  		// Only get the tag if it's in the top level targets role or the releases delegation role
   359  		// ignore it if it's in any other delegation roles
   360  		if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole {
   361  			return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String()))
   362  		}
   363  
   364  		logrus.Debugf("retrieving target for %s role\n", t.Role)
   365  		r, err := convertTarget(t.Target)
   366  		if err != nil {
   367  			return err
   368  
   369  		}
   370  		refs = append(refs, r)
   371  	}
   372  
   373  	for i, r := range refs {
   374  		displayTag := r.reference.String()
   375  		if displayTag != "" {
   376  			displayTag = ":" + displayTag
   377  		}
   378  		fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest)
   379  
   380  		if err := cli.imagePullPrivileged(authConfig, repoInfo.Name(), r.digest.String(), requestPrivilege); err != nil {
   381  			return err
   382  		}
   383  
   384  		// If reference is not trusted, tag by trusted reference
   385  		if !r.reference.HasDigest() {
   386  			tagged, err := reference.WithTag(repoInfo, r.reference.String())
   387  			if err != nil {
   388  				return err
   389  			}
   390  			trustedRef, err := reference.WithDigest(repoInfo, r.digest)
   391  			if err != nil {
   392  				return err
   393  			}
   394  			if err := cli.tagTrusted(trustedRef, tagged); err != nil {
   395  				return err
   396  			}
   397  		}
   398  	}
   399  	return nil
   400  }
   401  
   402  func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error {
   403  	responseBody, err := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, requestPrivilege)
   404  	if err != nil {
   405  		return err
   406  	}
   407  
   408  	defer responseBody.Close()
   409  
   410  	// If it is a trusted push we would like to find the target entry which match the
   411  	// tag provided in the function and then do an AddTarget later.
   412  	target := &client.Target{}
   413  	// Count the times of calling for handleTarget,
   414  	// if it is called more that once, that should be considered an error in a trusted push.
   415  	cnt := 0
   416  	handleTarget := func(aux *json.RawMessage) {
   417  		cnt++
   418  		if cnt > 1 {
   419  			// handleTarget should only be called one. This will be treated as an error.
   420  			return
   421  		}
   422  
   423  		var pushResult distribution.PushResult
   424  		err := json.Unmarshal(*aux, &pushResult)
   425  		if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil {
   426  			h, err := hex.DecodeString(pushResult.Digest.Hex())
   427  			if err != nil {
   428  				target = nil
   429  				return
   430  			}
   431  			target.Name = registry.ParseReference(pushResult.Tag).String()
   432  			target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h}
   433  			target.Length = int64(pushResult.Size)
   434  		}
   435  	}
   436  
   437  	// We want trust signatures to always take an explicit tag,
   438  	// otherwise it will act as an untrusted push.
   439  	if tag == "" {
   440  		if err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil); err != nil {
   441  			return err
   442  		}
   443  		fmt.Fprintln(cli.out, "No tag specified, skipping trust metadata push")
   444  		return nil
   445  	}
   446  
   447  	if err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, handleTarget); err != nil {
   448  		return err
   449  	}
   450  
   451  	if cnt > 1 {
   452  		return fmt.Errorf("internal error: only one call to handleTarget expected")
   453  	}
   454  
   455  	if target == nil {
   456  		fmt.Fprintln(cli.out, "No targets found, please provide a specific tag in order to sign it")
   457  		return nil
   458  	}
   459  
   460  	fmt.Fprintln(cli.out, "Signing and pushing trust metadata")
   461  
   462  	repo, err := cli.getNotaryRepository(repoInfo, authConfig, "push", "pull")
   463  	if err != nil {
   464  		fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err)
   465  		return err
   466  	}
   467  
   468  	// get the latest repository metadata so we can figure out which roles to sign
   469  	_, err = repo.Update(false)
   470  
   471  	switch err.(type) {
   472  	case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
   473  		keys := repo.CryptoService.ListKeys(data.CanonicalRootRole)
   474  		var rootKeyID string
   475  		// always select the first root key
   476  		if len(keys) > 0 {
   477  			sort.Strings(keys)
   478  			rootKeyID = keys[0]
   479  		} else {
   480  			rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey)
   481  			if err != nil {
   482  				return err
   483  			}
   484  			rootKeyID = rootPublicKey.ID()
   485  		}
   486  
   487  		// Initialize the notary repository with a remotely managed snapshot key
   488  		if err := repo.Initialize(rootKeyID, data.CanonicalSnapshotRole); err != nil {
   489  			return notaryError(repoInfo.FullName(), err)
   490  		}
   491  		fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName())
   492  		err = repo.AddTarget(target, data.CanonicalTargetsRole)
   493  	case nil:
   494  		// already initialized and we have successfully downloaded the latest metadata
   495  		err = cli.addTargetToAllSignableRoles(repo, target)
   496  	default:
   497  		return notaryError(repoInfo.FullName(), err)
   498  	}
   499  
   500  	if err == nil {
   501  		err = repo.Publish()
   502  	}
   503  
   504  	if err != nil {
   505  		fmt.Fprintf(cli.out, "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error())
   506  		return notaryError(repoInfo.FullName(), err)
   507  	}
   508  
   509  	fmt.Fprintf(cli.out, "Successfully signed %q:%s\n", repoInfo.FullName(), tag)
   510  	return nil
   511  }
   512  
   513  // Attempt to add the image target to all the top level delegation roles we can
   514  // (based on whether we have the signing key and whether the role's path allows
   515  // us to).
   516  // If there are no delegation roles, we add to the targets role.
   517  func (cli *DockerCli) addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error {
   518  	var signableRoles []string
   519  
   520  	// translate the full key names, which includes the GUN, into just the key IDs
   521  	allCanonicalKeyIDs := make(map[string]struct{})
   522  	for fullKeyID := range repo.CryptoService.ListAllKeys() {
   523  		allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{}
   524  	}
   525  
   526  	allDelegationRoles, err := repo.GetDelegationRoles()
   527  	if err != nil {
   528  		return err
   529  	}
   530  
   531  	// if there are no delegation roles, then just try to sign it into the targets role
   532  	if len(allDelegationRoles) == 0 {
   533  		return repo.AddTarget(target, data.CanonicalTargetsRole)
   534  	}
   535  
   536  	// there are delegation roles, find every delegation role we have a key for, and
   537  	// attempt to sign into into all those roles.
   538  	for _, delegationRole := range allDelegationRoles {
   539  		// We do not support signing any delegation role that isn't a direct child of the targets role.
   540  		// Also don't bother checking the keys if we can't add the target
   541  		// to this role due to path restrictions
   542  		if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) {
   543  			continue
   544  		}
   545  
   546  		for _, canonicalKeyID := range delegationRole.KeyIDs {
   547  			if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok {
   548  				signableRoles = append(signableRoles, delegationRole.Name)
   549  				break
   550  			}
   551  		}
   552  	}
   553  
   554  	if len(signableRoles) == 0 {
   555  		return fmt.Errorf("no valid signing keys for delegation roles")
   556  	}
   557  
   558  	return repo.AddTarget(target, signableRoles...)
   559  }