1package trust
2
3import (
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
37var (
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
49func 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.
56func 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.
66func 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
81type simpleCredentialStore struct {
82	auth types.AuthConfig
83}
84
85func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
86	return scs.auth.Username, scs.auth.Password
87}
88
89func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string {
90	return scs.auth.IdentityToken
91}
92
93func (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.
99func 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
187func 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
215func 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
247func 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
294type 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
305func 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
337func 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
348func 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
360func (imgRefAuth *ImageRefAndAuth) AuthConfig() *types.AuthConfig {
361	return imgRefAuth.authConfig
362}
363
364// Reference returns the Image reference for a given ImageRefAndAuth
365func (imgRefAuth *ImageRefAndAuth) Reference() reference.Named {
366	return imgRefAuth.reference
367}
368
369// RepoInfo returns the repository information for a given ImageRefAndAuth
370func (imgRefAuth *ImageRefAndAuth) RepoInfo() *registry.RepositoryInfo {
371	return imgRefAuth.repoInfo
372}
373
374// Tag returns the Image tag for a given ImageRefAndAuth
375func (imgRefAuth *ImageRefAndAuth) Tag() string {
376	return imgRefAuth.tag
377}
378
379// Digest returns the Image digest for a given ImageRefAndAuth
380func (imgRefAuth *ImageRefAndAuth) Digest() digest.Digest {
381	return imgRefAuth.digest
382}
383
384// Name returns the image name used to initialize the ImageRefAndAuth
385func (imgRefAuth *ImageRefAndAuth) Name() string {
386	return imgRefAuth.original
387
388}
389