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 }