github.1git.de/docker/cli@v26.1.3+incompatible/cli/trust/trust.go (about)

     1  package trust
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"io"
     7  	"net"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"time"
    14  
    15  	"github.com/distribution/reference"
    16  	"github.com/docker/cli/cli/config"
    17  	"github.com/docker/distribution/registry/client/auth"
    18  	"github.com/docker/distribution/registry/client/auth/challenge"
    19  	"github.com/docker/distribution/registry/client/transport"
    20  	registrytypes "github.com/docker/docker/api/types/registry"
    21  	"github.com/docker/docker/registry"
    22  	"github.com/docker/go-connections/tlsconfig"
    23  	"github.com/opencontainers/go-digest"
    24  	"github.com/pkg/errors"
    25  	"github.com/sirupsen/logrus"
    26  	"github.com/theupdateframework/notary"
    27  	"github.com/theupdateframework/notary/client"
    28  	"github.com/theupdateframework/notary/passphrase"
    29  	"github.com/theupdateframework/notary/storage"
    30  	"github.com/theupdateframework/notary/trustmanager"
    31  	"github.com/theupdateframework/notary/trustpinning"
    32  	"github.com/theupdateframework/notary/tuf/data"
    33  	"github.com/theupdateframework/notary/tuf/signed"
    34  )
    35  
    36  var (
    37  	// ReleasesRole is the role named "releases"
    38  	ReleasesRole = data.RoleName(path.Join(data.CanonicalTargetsRole.String(), "releases"))
    39  	// ActionsPullOnly defines the actions for read-only interactions with a Notary Repository
    40  	ActionsPullOnly = []string{"pull"}
    41  	// ActionsPushAndPull defines the actions for read-write interactions with a Notary Repository
    42  	ActionsPushAndPull = []string{"pull", "push"}
    43  	// NotaryServer is the endpoint serving the Notary trust server
    44  	NotaryServer = "https://notary.docker.io"
    45  )
    46  
    47  // GetTrustDirectory returns the base trust directory name
    48  func GetTrustDirectory() string {
    49  	return filepath.Join(config.Dir(), "trust")
    50  }
    51  
    52  // certificateDirectory returns the directory containing
    53  // TLS certificates for the given server. An error is
    54  // returned if there was an error parsing the server string.
    55  func certificateDirectory(server string) (string, error) {
    56  	u, err := url.Parse(server)
    57  	if err != nil {
    58  		return "", err
    59  	}
    60  
    61  	return filepath.Join(config.Dir(), "tls", u.Host), nil
    62  }
    63  
    64  // Server returns the base URL for the trust server.
    65  func Server(index *registrytypes.IndexInfo) (string, error) {
    66  	if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" {
    67  		urlObj, err := url.Parse(s)
    68  		if err != nil || urlObj.Scheme != "https" {
    69  			return "", errors.Errorf("valid https URL required for trust server, got %s", s)
    70  		}
    71  
    72  		return s, nil
    73  	}
    74  	if index.Official {
    75  		return NotaryServer, nil
    76  	}
    77  	return "https://" + index.Name, nil
    78  }
    79  
    80  type simpleCredentialStore struct {
    81  	auth registrytypes.AuthConfig
    82  }
    83  
    84  func (scs simpleCredentialStore) Basic(*url.URL) (string, string) {
    85  	return scs.auth.Username, scs.auth.Password
    86  }
    87  
    88  func (scs simpleCredentialStore) RefreshToken(*url.URL, string) string {
    89  	return scs.auth.IdentityToken
    90  }
    91  
    92  func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {}
    93  
    94  // GetNotaryRepository returns a NotaryRepository which stores all the
    95  // information needed to operate on a notary repository.
    96  // It creates an HTTP transport providing authentication support.
    97  func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo *registry.RepositoryInfo, authConfig *registrytypes.AuthConfig, actions ...string) (client.Repository, error) {
    98  	server, err := Server(repoInfo.Index)
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  
   103  	cfg := tlsconfig.ClientDefault()
   104  	cfg.InsecureSkipVerify = !repoInfo.Index.Secure
   105  
   106  	// Get certificate base directory
   107  	certDir, err := certificateDirectory(server)
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	logrus.Debugf("reading certificate directory: %s", certDir)
   112  
   113  	if err := registry.ReadCertsDirectory(cfg, certDir); err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	base := &http.Transport{
   118  		Proxy: http.ProxyFromEnvironment,
   119  		Dial: (&net.Dialer{
   120  			Timeout:   30 * time.Second,
   121  			KeepAlive: 30 * time.Second,
   122  			DualStack: true,
   123  		}).Dial,
   124  		TLSHandshakeTimeout: 10 * time.Second,
   125  		TLSClientConfig:     cfg,
   126  		DisableKeepAlives:   true,
   127  	}
   128  
   129  	// Skip configuration headers since request is not going to Docker daemon
   130  	modifiers := registry.Headers(userAgent, http.Header{})
   131  	authTransport := transport.NewTransport(base, modifiers...)
   132  	pingClient := &http.Client{
   133  		Transport: authTransport,
   134  		Timeout:   5 * time.Second,
   135  	}
   136  	endpointStr := server + "/v2/"
   137  	req, err := http.NewRequest(http.MethodGet, endpointStr, nil)
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	challengeManager := challenge.NewSimpleManager()
   143  
   144  	resp, err := pingClient.Do(req)
   145  	if err != nil {
   146  		// Ignore error on ping to operate in offline mode
   147  		logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err)
   148  	} else {
   149  		defer resp.Body.Close()
   150  
   151  		// Add response to the challenge manager to parse out
   152  		// authentication header and register authentication method
   153  		if err := challengeManager.AddResponse(resp); err != nil {
   154  			return nil, err
   155  		}
   156  	}
   157  
   158  	scope := auth.RepositoryScope{
   159  		Repository: repoInfo.Name.Name(),
   160  		Actions:    actions,
   161  		Class:      repoInfo.Class, // TODO(thaJeztah): Class is no longer needed for plugins and can likely be removed; see https://github.com/docker/cli/pull/4114#discussion_r1145430825
   162  	}
   163  	creds := simpleCredentialStore{auth: *authConfig}
   164  	tokenHandlerOptions := auth.TokenHandlerOptions{
   165  		Transport:   authTransport,
   166  		Credentials: creds,
   167  		Scopes:      []auth.Scope{scope},
   168  		ClientID:    registry.AuthClientID,
   169  	}
   170  	tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
   171  	basicHandler := auth.NewBasicHandler(creds)
   172  	modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
   173  	tr := transport.NewTransport(base, modifiers...)
   174  
   175  	return client.NewFileCachedRepository(
   176  		GetTrustDirectory(),
   177  		data.GUN(repoInfo.Name.Name()),
   178  		server,
   179  		tr,
   180  		GetPassphraseRetriever(in, out),
   181  		trustpinning.TrustPinConfig{})
   182  }
   183  
   184  // GetPassphraseRetriever returns a passphrase retriever that utilizes Content Trust env vars
   185  func GetPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever {
   186  	aliasMap := map[string]string{
   187  		"root":     "root",
   188  		"snapshot": "repository",
   189  		"targets":  "repository",
   190  		"default":  "repository",
   191  	}
   192  	baseRetriever := passphrase.PromptRetrieverWithInOut(in, out, aliasMap)
   193  	env := map[string]string{
   194  		"root":     os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"),
   195  		"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
   196  		"targets":  os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
   197  		"default":  os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
   198  	}
   199  
   200  	return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
   201  		if v := env[alias]; v != "" {
   202  			return v, numAttempts > 1, nil
   203  		}
   204  		// For non-root roles, we can also try the "default" alias if it is specified
   205  		if v := env["default"]; v != "" && alias != data.CanonicalRootRole.String() {
   206  			return v, numAttempts > 1, nil
   207  		}
   208  		return baseRetriever(keyName, alias, createNew, numAttempts)
   209  	}
   210  }
   211  
   212  // NotaryError formats an error message received from the notary service
   213  func NotaryError(repoName string, err error) error {
   214  	switch err.(type) {
   215  	case *json.SyntaxError:
   216  		logrus.Debugf("Notary syntax error: %s", err)
   217  		return errors.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)
   218  	case signed.ErrExpired:
   219  		return errors.Errorf("Error: remote repository %s out-of-date: %v", repoName, err)
   220  	case trustmanager.ErrKeyNotFound:
   221  		return errors.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err)
   222  	case storage.NetworkError:
   223  		return errors.Errorf("Error: error contacting notary server: %v", err)
   224  	case storage.ErrMetaNotFound:
   225  		return errors.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err)
   226  	case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType:
   227  		return errors.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err)
   228  	case signed.ErrNoKeys:
   229  		return errors.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err)
   230  	case signed.ErrLowVersion:
   231  		return errors.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err)
   232  	case signed.ErrRoleThreshold:
   233  		return errors.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err)
   234  	case client.ErrRepositoryNotExist:
   235  		return errors.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err)
   236  	case signed.ErrInsufficientSignatures:
   237  		return errors.Errorf("Error: could not produce valid signature for %s.  If Yubikey was used, was touch input provided?: %v", repoName, err)
   238  	}
   239  
   240  	return err
   241  }
   242  
   243  // GetSignableRoles returns a list of roles for which we have valid signing
   244  // keys, given a notary repository and a target
   245  func GetSignableRoles(repo client.Repository, target *client.Target) ([]data.RoleName, error) {
   246  	var signableRoles []data.RoleName
   247  
   248  	// translate the full key names, which includes the GUN, into just the key IDs
   249  	allCanonicalKeyIDs := make(map[string]struct{})
   250  	for fullKeyID := range repo.GetCryptoService().ListAllKeys() {
   251  		allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{}
   252  	}
   253  
   254  	allDelegationRoles, err := repo.GetDelegationRoles()
   255  	if err != nil {
   256  		return signableRoles, err
   257  	}
   258  
   259  	// if there are no delegation roles, then just try to sign it into the targets role
   260  	if len(allDelegationRoles) == 0 {
   261  		signableRoles = append(signableRoles, data.CanonicalTargetsRole)
   262  		return signableRoles, nil
   263  	}
   264  
   265  	// there are delegation roles, find every delegation role we have a key for,
   266  	// and attempt to sign in to all those roles.
   267  	for _, delegationRole := range allDelegationRoles {
   268  		// We do not support signing any delegation role that isn't a direct child of the targets role.
   269  		// Also don't bother checking the keys if we can't add the target
   270  		// to this role due to path restrictions
   271  		if path.Dir(delegationRole.Name.String()) != data.CanonicalTargetsRole.String() || !delegationRole.CheckPaths(target.Name) {
   272  			continue
   273  		}
   274  
   275  		for _, canonicalKeyID := range delegationRole.KeyIDs {
   276  			if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok {
   277  				signableRoles = append(signableRoles, delegationRole.Name)
   278  				break
   279  			}
   280  		}
   281  	}
   282  
   283  	if len(signableRoles) == 0 {
   284  		return signableRoles, errors.Errorf("no valid signing keys for delegation roles")
   285  	}
   286  
   287  	return signableRoles, nil
   288  }
   289  
   290  // ImageRefAndAuth contains all reference information and the auth config for an image request
   291  type ImageRefAndAuth struct {
   292  	original   string
   293  	authConfig *registrytypes.AuthConfig
   294  	reference  reference.Named
   295  	repoInfo   *registry.RepositoryInfo
   296  	tag        string
   297  	digest     digest.Digest
   298  }
   299  
   300  // GetImageReferencesAndAuth retrieves the necessary reference and auth information for an image name
   301  // as an ImageRefAndAuth struct
   302  func GetImageReferencesAndAuth(ctx context.Context,
   303  	authResolver func(ctx context.Context, index *registrytypes.IndexInfo) registrytypes.AuthConfig,
   304  	imgName string,
   305  ) (ImageRefAndAuth, error) {
   306  	ref, err := reference.ParseNormalizedNamed(imgName)
   307  	if err != nil {
   308  		return ImageRefAndAuth{}, err
   309  	}
   310  
   311  	// Resolve the Repository name from fqn to RepositoryInfo
   312  	repoInfo, err := registry.ParseRepositoryInfo(ref)
   313  	if err != nil {
   314  		return ImageRefAndAuth{}, err
   315  	}
   316  
   317  	authConfig := authResolver(ctx, repoInfo.Index)
   318  	return ImageRefAndAuth{
   319  		original:   imgName,
   320  		authConfig: &authConfig,
   321  		reference:  ref,
   322  		repoInfo:   repoInfo,
   323  		tag:        getTag(ref),
   324  		digest:     getDigest(ref),
   325  	}, nil
   326  }
   327  
   328  func getTag(ref reference.Named) string {
   329  	switch x := ref.(type) {
   330  	case reference.Canonical, reference.Digested:
   331  		return ""
   332  	case reference.NamedTagged:
   333  		return x.Tag()
   334  	default:
   335  		return ""
   336  	}
   337  }
   338  
   339  func getDigest(ref reference.Named) digest.Digest {
   340  	switch x := ref.(type) {
   341  	case reference.Canonical:
   342  		return x.Digest()
   343  	case reference.Digested:
   344  		return x.Digest()
   345  	default:
   346  		return digest.Digest("")
   347  	}
   348  }
   349  
   350  // AuthConfig returns the auth information (username, etc) for a given ImageRefAndAuth
   351  func (imgRefAuth *ImageRefAndAuth) AuthConfig() *registrytypes.AuthConfig {
   352  	return imgRefAuth.authConfig
   353  }
   354  
   355  // Reference returns the Image reference for a given ImageRefAndAuth
   356  func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named {
   357  	return imgRefAuth.reference
   358  }
   359  
   360  // RepoInfo returns the repository information for a given ImageRefAndAuth
   361  func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo {
   362  	return imgRefAuth.repoInfo
   363  }
   364  
   365  // Tag returns the Image tag for a given ImageRefAndAuth
   366  func (imgRefAuth *ImageRefAndAuth) Tag() string {
   367  	return imgRefAuth.tag
   368  }
   369  
   370  // Digest returns the Image digest for a given ImageRefAndAuth
   371  func (imgRefAuth *ImageRefAndAuth) Digest() digest.Digest {
   372  	return imgRefAuth.digest
   373  }
   374  
   375  // Name returns the image name used to initialize the ImageRefAndAuth
   376  func (imgRefAuth *ImageRefAndAuth) Name() string {
   377  	return imgRefAuth.original
   378  }