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