github.com/zhuohuang-hust/src-cbuild@v0.0.0-20230105071821-c7aab3e7c840/cli/command/image/trust.go (about) 1 package image 2 3 import ( 4 "encoding/hex" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "net" 10 "net/http" 11 "net/url" 12 "os" 13 "path" 14 "path/filepath" 15 "sort" 16 "time" 17 18 "golang.org/x/net/context" 19 20 "github.com/Sirupsen/logrus" 21 "github.com/docker/distribution/digest" 22 "github.com/docker/distribution/registry/client/auth" 23 "github.com/docker/distribution/registry/client/auth/challenge" 24 "github.com/docker/distribution/registry/client/transport" 25 "github.com/docker/docker/api/types" 26 registrytypes "github.com/docker/docker/api/types/registry" 27 "github.com/docker/docker/cli/command" 28 "github.com/docker/docker/cliconfig" 29 "github.com/docker/docker/distribution" 30 "github.com/docker/docker/pkg/jsonmessage" 31 "github.com/docker/docker/reference" 32 "github.com/docker/docker/registry" 33 "github.com/docker/go-connections/tlsconfig" 34 "github.com/docker/notary" 35 "github.com/docker/notary/client" 36 "github.com/docker/notary/passphrase" 37 "github.com/docker/notary/storage" 38 "github.com/docker/notary/trustmanager" 39 "github.com/docker/notary/trustpinning" 40 "github.com/docker/notary/tuf/data" 41 "github.com/docker/notary/tuf/signed" 42 ) 43 44 var ( 45 releasesRole = path.Join(data.CanonicalTargetsRole, "releases") 46 ) 47 48 type target struct { 49 name string 50 digest digest.Digest 51 size int64 52 } 53 54 // trustedPush handles content trust pushing of an image 55 func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { 56 responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege) 57 if err != nil { 58 return err 59 } 60 61 defer responseBody.Close() 62 63 // If it is a trusted push we would like to find the target entry which match the 64 // tag provided in the function and then do an AddTarget later. 65 target := &client.Target{} 66 // Count the times of calling for handleTarget, 67 // if it is called more that once, that should be considered an error in a trusted push. 68 cnt := 0 69 handleTarget := func(aux *json.RawMessage) { 70 cnt++ 71 if cnt > 1 { 72 // handleTarget should only be called one. This will be treated as an error. 73 return 74 } 75 76 var pushResult distribution.PushResult 77 err := json.Unmarshal(*aux, &pushResult) 78 if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { 79 h, err := hex.DecodeString(pushResult.Digest.Hex()) 80 if err != nil { 81 target = nil 82 return 83 } 84 target.Name = pushResult.Tag 85 target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} 86 target.Length = int64(pushResult.Size) 87 } 88 } 89 90 var tag string 91 switch x := ref.(type) { 92 case reference.Canonical: 93 return errors.New("cannot push a digest reference") 94 case reference.NamedTagged: 95 tag = x.Tag() 96 default: 97 // We want trust signatures to always take an explicit tag, 98 // otherwise it will act as an untrusted push. 99 if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil { 100 return err 101 } 102 fmt.Fprintln(cli.Out(), "No tag specified, skipping trust metadata push") 103 return nil 104 } 105 106 if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil { 107 return err 108 } 109 110 if cnt > 1 { 111 return fmt.Errorf("internal error: only one call to handleTarget expected") 112 } 113 114 if target == nil { 115 fmt.Fprintln(cli.Out(), "No targets found, please provide a specific tag in order to sign it") 116 return nil 117 } 118 119 fmt.Fprintln(cli.Out(), "Signing and pushing trust metadata") 120 121 repo, err := GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull") 122 if err != nil { 123 fmt.Fprintf(cli.Out(), "Error establishing connection to notary repository: %s\n", err) 124 return err 125 } 126 127 // get the latest repository metadata so we can figure out which roles to sign 128 err = repo.Update(false) 129 130 switch err.(type) { 131 case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist: 132 keys := repo.CryptoService.ListKeys(data.CanonicalRootRole) 133 var rootKeyID string 134 // always select the first root key 135 if len(keys) > 0 { 136 sort.Strings(keys) 137 rootKeyID = keys[0] 138 } else { 139 rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) 140 if err != nil { 141 return err 142 } 143 rootKeyID = rootPublicKey.ID() 144 } 145 146 // Initialize the notary repository with a remotely managed snapshot key 147 if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil { 148 return notaryError(repoInfo.FullName(), err) 149 } 150 fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.FullName()) 151 err = repo.AddTarget(target, data.CanonicalTargetsRole) 152 case nil: 153 // already initialized and we have successfully downloaded the latest metadata 154 err = addTargetToAllSignableRoles(repo, target) 155 default: 156 return notaryError(repoInfo.FullName(), err) 157 } 158 159 if err == nil { 160 err = repo.Publish() 161 } 162 163 if err != nil { 164 fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) 165 return notaryError(repoInfo.FullName(), err) 166 } 167 168 fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.FullName(), tag) 169 return nil 170 } 171 172 // Attempt to add the image target to all the top level delegation roles we can 173 // (based on whether we have the signing key and whether the role's path allows 174 // us to). 175 // If there are no delegation roles, we add to the targets role. 176 func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error { 177 var signableRoles []string 178 179 // translate the full key names, which includes the GUN, into just the key IDs 180 allCanonicalKeyIDs := make(map[string]struct{}) 181 for fullKeyID := range repo.CryptoService.ListAllKeys() { 182 allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{} 183 } 184 185 allDelegationRoles, err := repo.GetDelegationRoles() 186 if err != nil { 187 return err 188 } 189 190 // if there are no delegation roles, then just try to sign it into the targets role 191 if len(allDelegationRoles) == 0 { 192 return repo.AddTarget(target, data.CanonicalTargetsRole) 193 } 194 195 // there are delegation roles, find every delegation role we have a key for, and 196 // attempt to sign into into all those roles. 197 for _, delegationRole := range allDelegationRoles { 198 // We do not support signing any delegation role that isn't a direct child of the targets role. 199 // Also don't bother checking the keys if we can't add the target 200 // to this role due to path restrictions 201 if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) { 202 continue 203 } 204 205 for _, canonicalKeyID := range delegationRole.KeyIDs { 206 if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok { 207 signableRoles = append(signableRoles, delegationRole.Name) 208 break 209 } 210 } 211 } 212 213 if len(signableRoles) == 0 { 214 return fmt.Errorf("no valid signing keys for delegation roles") 215 } 216 217 return repo.AddTarget(target, signableRoles...) 218 } 219 220 // imagePushPrivileged push the image 221 func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { 222 encodedAuth, err := command.EncodeAuthToBase64(authConfig) 223 if err != nil { 224 return nil, err 225 } 226 options := types.ImagePushOptions{ 227 RegistryAuth: encodedAuth, 228 PrivilegeFunc: requestPrivilege, 229 } 230 231 return cli.Client().ImagePush(ctx, ref, options) 232 } 233 234 // trustedPull handles content trust pulling of an image 235 func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { 236 var refs []target 237 238 notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") 239 if err != nil { 240 fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) 241 return err 242 } 243 244 if tagged, isTagged := ref.(reference.NamedTagged); !isTagged { 245 // List all targets 246 targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) 247 if err != nil { 248 return notaryError(repoInfo.FullName(), err) 249 } 250 for _, tgt := range targets { 251 t, err := convertTarget(tgt.Target) 252 if err != nil { 253 fmt.Fprintf(cli.Out(), "Skipping target for %q\n", repoInfo.Name()) 254 continue 255 } 256 // Only list tags in the top level targets role or the releases delegation role - ignore 257 // all other delegation roles 258 if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole { 259 continue 260 } 261 refs = append(refs, t) 262 } 263 if len(refs) == 0 { 264 return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName())) 265 } 266 } else { 267 t, err := notaryRepo.GetTargetByName(tagged.Tag(), releasesRole, data.CanonicalTargetsRole) 268 if err != nil { 269 return notaryError(repoInfo.FullName(), err) 270 } 271 // Only get the tag if it's in the top level targets role or the releases delegation role 272 // ignore it if it's in any other delegation roles 273 if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { 274 return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag())) 275 } 276 277 logrus.Debugf("retrieving target for %s role\n", t.Role) 278 r, err := convertTarget(t.Target) 279 if err != nil { 280 return err 281 282 } 283 refs = append(refs, r) 284 } 285 286 for i, r := range refs { 287 displayTag := r.name 288 if displayTag != "" { 289 displayTag = ":" + displayTag 290 } 291 fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) 292 293 ref, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) 294 if err != nil { 295 return err 296 } 297 if err := imagePullPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege, false); err != nil { 298 return err 299 } 300 301 tagged, err := reference.WithTag(repoInfo, r.name) 302 if err != nil { 303 return err 304 } 305 trustedRef, err := reference.WithDigest(reference.TrimNamed(repoInfo), r.digest) 306 if err != nil { 307 return err 308 } 309 if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { 310 return err 311 } 312 } 313 return nil 314 } 315 316 // imagePullPrivileged pulls the image and displays it to the output 317 func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { 318 319 encodedAuth, err := command.EncodeAuthToBase64(authConfig) 320 if err != nil { 321 return err 322 } 323 options := types.ImagePullOptions{ 324 RegistryAuth: encodedAuth, 325 PrivilegeFunc: requestPrivilege, 326 All: all, 327 } 328 329 responseBody, err := cli.Client().ImagePull(ctx, ref, options) 330 if err != nil { 331 return err 332 } 333 defer responseBody.Close() 334 335 return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) 336 } 337 338 func trustDirectory() string { 339 return filepath.Join(cliconfig.ConfigDir(), "trust") 340 } 341 342 // certificateDirectory returns the directory containing 343 // TLS certificates for the given server. An error is 344 // returned if there was an error parsing the server string. 345 func certificateDirectory(server string) (string, error) { 346 u, err := url.Parse(server) 347 if err != nil { 348 return "", err 349 } 350 351 return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil 352 } 353 354 func trustServer(index *registrytypes.IndexInfo) (string, error) { 355 if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { 356 urlObj, err := url.Parse(s) 357 if err != nil || urlObj.Scheme != "https" { 358 return "", fmt.Errorf("valid https URL required for trust server, got %s", s) 359 } 360 361 return s, nil 362 } 363 if index.Official { 364 return registry.NotaryServer, nil 365 } 366 return "https://" + index.Name, nil 367 } 368 369 type simpleCredentialStore struct { 370 auth types.AuthConfig 371 } 372 373 func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { 374 return scs.auth.Username, scs.auth.Password 375 } 376 377 func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { 378 return scs.auth.IdentityToken 379 } 380 381 func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { 382 } 383 384 // GetNotaryRepository returns a NotaryRepository which stores all the 385 // information needed to operate on a notary repository. 386 // It creates an HTTP transport providing authentication support. 387 // TODO: move this too 388 func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { 389 server, err := trustServer(repoInfo.Index) 390 if err != nil { 391 return nil, err 392 } 393 394 var cfg = tlsconfig.ClientDefault() 395 cfg.InsecureSkipVerify = !repoInfo.Index.Secure 396 397 // Get certificate base directory 398 certDir, err := certificateDirectory(server) 399 if err != nil { 400 return nil, err 401 } 402 logrus.Debugf("reading certificate directory: %s", certDir) 403 404 if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { 405 return nil, err 406 } 407 408 base := &http.Transport{ 409 Proxy: http.ProxyFromEnvironment, 410 Dial: (&net.Dialer{ 411 Timeout: 30 * time.Second, 412 KeepAlive: 30 * time.Second, 413 DualStack: true, 414 }).Dial, 415 TLSHandshakeTimeout: 10 * time.Second, 416 TLSClientConfig: cfg, 417 DisableKeepAlives: true, 418 } 419 420 // Skip configuration headers since request is not going to Docker daemon 421 modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{}) 422 authTransport := transport.NewTransport(base, modifiers...) 423 pingClient := &http.Client{ 424 Transport: authTransport, 425 Timeout: 5 * time.Second, 426 } 427 endpointStr := server + "/v2/" 428 req, err := http.NewRequest("GET", endpointStr, nil) 429 if err != nil { 430 return nil, err 431 } 432 433 challengeManager := challenge.NewSimpleManager() 434 435 resp, err := pingClient.Do(req) 436 if err != nil { 437 // Ignore error on ping to operate in offline mode 438 logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) 439 } else { 440 defer resp.Body.Close() 441 442 // Add response to the challenge manager to parse out 443 // authentication header and register authentication method 444 if err := challengeManager.AddResponse(resp); err != nil { 445 return nil, err 446 } 447 } 448 449 creds := simpleCredentialStore{auth: authConfig} 450 tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) 451 basicHandler := auth.NewBasicHandler(creds) 452 modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) 453 tr := transport.NewTransport(base, modifiers...) 454 455 return client.NewNotaryRepository( 456 trustDirectory(), 457 repoInfo.FullName(), 458 server, 459 tr, 460 getPassphraseRetriever(streams), 461 trustpinning.TrustPinConfig{}) 462 } 463 464 func getPassphraseRetriever(streams command.Streams) notary.PassRetriever { 465 aliasMap := map[string]string{ 466 "root": "root", 467 "snapshot": "repository", 468 "targets": "repository", 469 "default": "repository", 470 } 471 baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) 472 env := map[string]string{ 473 "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), 474 "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 475 "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 476 "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 477 } 478 479 return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { 480 if v := env[alias]; v != "" { 481 return v, numAttempts > 1, nil 482 } 483 // For non-root roles, we can also try the "default" alias if it is specified 484 if v := env["default"]; v != "" && alias != data.CanonicalRootRole { 485 return v, numAttempts > 1, nil 486 } 487 return baseRetriever(keyName, alias, createNew, numAttempts) 488 } 489 } 490 491 // TrustedReference returns the canonical trusted reference for an image reference 492 func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { 493 repoInfo, err := registry.ParseRepositoryInfo(ref) 494 if err != nil { 495 return nil, err 496 } 497 498 // Resolve the Auth config relevant for this server 499 authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) 500 501 notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") 502 if err != nil { 503 fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) 504 return nil, err 505 } 506 507 t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) 508 if err != nil { 509 return nil, err 510 } 511 // Only list tags in the top level targets role or the releases delegation role - ignore 512 // all other delegation roles 513 if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { 514 return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) 515 } 516 r, err := convertTarget(t.Target) 517 if err != nil { 518 return nil, err 519 520 } 521 522 return reference.WithDigest(reference.TrimNamed(ref), r.digest) 523 } 524 525 func convertTarget(t client.Target) (target, error) { 526 h, ok := t.Hashes["sha256"] 527 if !ok { 528 return target{}, errors.New("no valid hash, expecting sha256") 529 } 530 return target{ 531 name: t.Name, 532 digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), 533 size: t.Length, 534 }, nil 535 } 536 537 // TagTrusted tags a trusted ref 538 func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef reference.Canonical, ref reference.NamedTagged) error { 539 fmt.Fprintf(cli.Out(), "Tagging %s as %s\n", trustedRef.String(), ref.String()) 540 541 return cli.Client().ImageTag(ctx, trustedRef.String(), ref.String()) 542 } 543 544 // notaryError formats an error message received from the notary service 545 func notaryError(repoName string, err error) error { 546 switch err.(type) { 547 case *json.SyntaxError: 548 logrus.Debugf("Notary syntax error: %s", err) 549 return fmt.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) 550 case signed.ErrExpired: 551 return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) 552 case trustmanager.ErrKeyNotFound: 553 return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) 554 case storage.NetworkError: 555 return fmt.Errorf("Error: error contacting notary server: %v", err) 556 case storage.ErrMetaNotFound: 557 return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) 558 case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType: 559 return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) 560 case signed.ErrNoKeys: 561 return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) 562 case signed.ErrLowVersion: 563 return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) 564 case signed.ErrRoleThreshold: 565 return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) 566 case client.ErrRepositoryNotExist: 567 return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) 568 case signed.ErrInsufficientSignatures: 569 return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) 570 } 571 572 return err 573 }