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