github.com/jcarley/cli@v0.0.0-20180201210820-966d90434c30/lib/images/tuf.go (about) 1 package images 2 3 import ( 4 "context" 5 "encoding/base64" 6 "encoding/hex" 7 "encoding/json" 8 "fmt" 9 "io/ioutil" 10 "net" 11 "net/http" 12 "net/url" 13 "os" 14 "path" 15 "runtime" 16 "strings" 17 "time" 18 19 "github.com/Sirupsen/logrus" 20 "github.com/daticahealth/cli/lib/prompts" 21 "github.com/daticahealth/cli/models" 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 "github.com/docker/docker/api/types/filters" 27 dockerClient "github.com/docker/docker/client" 28 "github.com/docker/docker/pkg/jsonmessage" 29 "github.com/docker/go-connections/tlsconfig" 30 "github.com/docker/notary" 31 notaryClient "github.com/docker/notary/client" 32 "github.com/docker/notary/client/changelist" 33 "github.com/docker/notary/cryptoservice" 34 "github.com/docker/notary/passphrase" 35 "github.com/docker/notary/trustmanager" 36 "github.com/docker/notary/trustpinning" 37 "github.com/docker/notary/tuf/data" 38 "github.com/olekukonko/tablewriter" 39 digest "github.com/opencontainers/go-digest" 40 ) 41 42 var registries = map[string]string{ 43 "sbox05": "registry-sbox05.datica.com", 44 "default": "registry.datica.com", 45 } 46 47 var notaryServers = map[string]string{ 48 "sbox05": "https://notary-sandbox.datica.com", 49 "default": "https://notary.datica.com", 50 } 51 52 // Errors for image handling 53 const ( 54 InvalidImageName = "Invalid image name" 55 IncorrectNamespace = "Incorrect namespace for your environment" 56 IncorrectRegistryOrNamespace = "Incorrect registry or namespace for your environment" 57 MissingTrustData = "does not have trust data for" 58 ImageDoesNotExist = "No such image" 59 CancelingPush = "Canceling push request" 60 61 CanonicalTargetsRole = "targets" 62 ) 63 64 // Constants for image handling 65 const ( 66 DefaultTag = "latest" 67 trustPath = ".docker/trust" 68 ) 69 70 // Target contains metadata about a target 71 type Target struct { 72 Name string 73 Digest digest.Digest 74 Size int64 75 Role string 76 } 77 78 // AuxData contains metadata about the content that was pushed 79 type AuxData struct { 80 Tag string `json:"Tag"` 81 Digest string `json:"Digest"` 82 Size int64 `json:"Size"` 83 } 84 85 // Push parses a given name into registry/namespace/image:tag and attempts to push it to the remote registry 86 func (d *SImages) Push(name string, user *models.User, env *models.Environment, ip prompts.IPrompts) (*models.Image, error) { 87 ctx := context.Background() 88 dockerCli, err := dockerClient.NewEnvClient() 89 if err != nil { 90 return nil, err 91 } 92 defer dockerCli.Close() 93 dockerCli.NegotiateAPIVersion(ctx) 94 95 repositoryName, tag, err := d.GetGloballyUniqueNamespace(name, env, true) 96 if err != nil { 97 return nil, err 98 } 99 100 if tag == "" { 101 tag = DefaultTag 102 } 103 fullImageName := strings.Join([]string{repositoryName, tag}, ":") 104 if fullImageName != name { 105 if err = dockerCli.ImageTag(ctx, name, fullImageName); err != nil { 106 if !strings.Contains(err.Error(), ImageDoesNotExist) { 107 return nil, err 108 } 109 110 // Check if the fully formatted repo name exists, and ask if user wants to push that instead 111 if !localImageExists(ctx, fullImageName, dockerCli) { 112 return nil, err 113 } else if yesNo := ip.YesNo(err.Error(), fmt.Sprintf("Would you like to push %s instead? (y/n) ", fullImageName)); yesNo != nil { 114 return nil, fmt.Errorf(CancelingPush) 115 } 116 } else { 117 logrus.Printf("Pushing image %s to %s\n", name, fullImageName) 118 } 119 } else { 120 logrus.Printf("Pushing image %s", fullImageName) 121 } 122 123 resp, err := dockerCli.ImagePush(ctx, fullImageName, types.ImagePushOptions{RegistryAuth: dockerAuth(user)}) 124 if err != nil { 125 return nil, err 126 } 127 defer resp.Close() 128 129 var digest models.ContentDigest 130 if err = jsonmessage.DisplayJSONMessagesStream(resp, os.Stdout, os.Stdout.Fd(), true, 131 func(aux *json.RawMessage) { 132 var auxData AuxData 133 if data, jsonErr := aux.MarshalJSON(); jsonErr == nil { 134 json.Unmarshal(data, &auxData) 135 hashParts := strings.Split(auxData.Digest, ":") 136 digest.HashType = hashParts[0] 137 digest.Hash = hashParts[1] 138 digest.Size = auxData.Size 139 } 140 }); err != nil { 141 return nil, err 142 } 143 144 return &models.Image{ 145 Name: repositoryName, 146 Tag: tag, 147 Digest: &digest, 148 }, nil 149 } 150 151 // Pull parses a name into registry/namespace/image:tag and attempts to retrieve it from the remote registry 152 func (d *SImages) Pull(name string, target *Target, user *models.User, env *models.Environment) error { 153 ctx := context.Background() 154 dockerCli, err := dockerClient.NewEnvClient() 155 if err != nil { 156 return err 157 } 158 defer dockerCli.Close() 159 dockerCli.NegotiateAPIVersion(ctx) 160 161 ref := strings.Join([]string{name, string(target.Digest)}, "@") 162 resp, err := dockerCli.ImagePull(ctx, ref, types.ImagePullOptions{RegistryAuth: dockerAuth(user)}) 163 if err != nil { 164 return err 165 } 166 defer resp.Close() 167 return jsonmessage.DisplayJSONMessagesStream(resp, os.Stdout, os.Stdout.Fd(), true, nil) 168 } 169 170 // InitNotaryRepo intializes a notary repository 171 func (d *SImages) InitNotaryRepo(repo notaryClient.Repository, rootKeyPath string) error { 172 rootTrustDir := fmt.Sprintf("%s/%s", userHomeDir(), trustPath) 173 if err := os.MkdirAll(rootTrustDir, 0700); err != nil { 174 return err 175 } 176 177 rootKeyIDs, err := getRootKey(rootKeyPath, repo, getPassphraseRetriever()) 178 if err != nil { 179 return err 180 } 181 if err = repo.Initialize(rootKeyIDs); err != nil { 182 return err 183 } 184 return nil 185 } 186 187 // AddTargetHash adds the given content hash to a notary repo and sends a signing request to the server 188 func (d *SImages) AddTargetHash(repo notaryClient.Repository, digest *models.ContentDigest, tag string, publish bool) error { 189 targetHash := data.Hashes{} 190 sha256, err := hex.DecodeString(digest.Hash) 191 if err != nil { 192 return err 193 } 194 targetHash[digest.HashType] = sha256 195 196 // var targetCustom *canonicalJson.RawMessage 197 target := ¬aryClient.Target{Name: tag, Hashes: targetHash, Length: digest.Size} 198 if err = repo.AddTarget(target, data.CanonicalTargetsRole); err != nil { 199 return err 200 } 201 if publish { 202 return d.Publish(repo) 203 } 204 return nil 205 } 206 207 // ListTargets intializes a notary repository 208 func (d *SImages) ListTargets(repo notaryClient.Repository, roles ...string) ([]*Target, error) { 209 targets, err := repo.ListTargets(data.NewRoleList(roles)...) 210 if err != nil { 211 return nil, err 212 } 213 var ts []*Target 214 for _, t := range targets { 215 t1, err := convertTarget(t) 216 if err != nil { 217 return nil, err 218 } 219 ts = append(ts, t1) 220 } 221 return ts, nil 222 } 223 224 // LookupTarget searches for a specific target in a repository by tag name 225 func (d *SImages) LookupTarget(repo notaryClient.Repository, tag string) (*Target, error) { 226 target, err := repo.GetTargetByName(tag) 227 if err != nil { 228 return nil, err 229 } 230 role := string(target.Role) 231 if role != path.Join(CanonicalTargetsRole, "releases") && role != CanonicalTargetsRole { 232 return nil, fmt.Errorf("no canonical target found for %s", tag) 233 } 234 t, err := convertTarget(target) 235 if err != nil { 236 return nil, err 237 } 238 return t, nil 239 } 240 241 // DeleteTargets deletes the signed targets for a list of tags 242 func (d *SImages) DeleteTargets(repo notaryClient.Repository, tags []string, publish bool) error { 243 //TODO: Check if a target is associated with a deployed release before allowing it to be unsigned 244 for _, tag := range tags { 245 if err := repo.RemoveTarget(tag, "targets"); err != nil { 246 return err 247 } 248 } 249 250 if publish { 251 return d.Publish(repo) 252 } 253 return nil 254 } 255 256 // PrintChangelist prints out the users unpublished changes in a formatted table 257 func (d *SImages) PrintChangelist(changes []changelist.Change) { 258 data := [][]string{[]string{"#", "Action", "Scope", "Type", "Target"}, []string{"-", "------", "-----", "----", "------"}} 259 data = append(data) 260 for i, c := range changes { 261 data = append(data, []string{fmt.Sprintf("%d\n", i), c.Action(), c.Scope().String(), c.Type(), c.Path()}) 262 } 263 264 logrus.Println() 265 table := tablewriter.NewWriter(logrus.StandardLogger().Out) 266 table.SetBorder(false) 267 table.SetRowLine(false) 268 table.SetHeaderLine(false) 269 table.SetAlignment(1) 270 table.SetCenterSeparator("") 271 table.SetColumnSeparator("") 272 table.SetRowSeparator("") 273 table.AppendBulk(data) 274 table.Render() 275 logrus.Println() 276 } 277 278 // CheckChangelist prompts the user if they have unpublished changes to let them clear 279 // undesired changes before publishing, or verify that the changes should be published 280 func (d *SImages) CheckChangelist(repo notaryClient.Repository, ip prompts.IPrompts) error { 281 if changelist, err := repo.GetChangelist(); err == nil { 282 changes := changelist.List() 283 if len(changes) > 0 { 284 logrus.Println("The following unpublished changes were found in your local trust repository:") 285 d.PrintChangelist(changes) 286 publishWarning := "These changes will be published along with your current request." 287 if err = ip.YesNo(publishWarning, "Would you like to proceed? (y/n) "); err != nil { 288 logrus.Println("Use the `images targets reset <image>` command to clear undesired changes") 289 return err 290 } 291 } 292 } 293 return nil 294 } 295 296 // GetNotaryRepository returns a pointer to the notary repository for an image 297 func (d *SImages) GetNotaryRepository(pod, imageName string, user *models.User) notaryClient.Repository { 298 notaryServer := getServer(pod, notaryServers) 299 transport, err := getTransport(imageName, notaryServer, user, readWrite) 300 if err != nil { 301 logrus.Fatalln(err) 302 } 303 rootTrustDir := fmt.Sprintf("%s/%s", userHomeDir(), trustPath) 304 repo, err := notaryClient.NewFileCachedRepository( 305 rootTrustDir, 306 data.GUN(imageName), 307 notaryServer, 308 transport, 309 getPassphraseRetriever(), 310 trustpinning.TrustPinConfig{}, 311 ) 312 if err != nil { 313 logrus.Fatalln(err) 314 } 315 return repo 316 } 317 318 // GetGloballyUniqueNamespace returns the fully formatted name for an image <registry>/<namespace>/<image> and a tag if present 319 func (d *SImages) GetGloballyUniqueNamespace(name string, env *models.Environment, includeRegistry bool) (string, string, error) { 320 var repositoryName string 321 var image string 322 var tag string 323 324 imageParts := strings.Split(strings.TrimPrefix(name, "/"), ":") 325 switch len(imageParts) { 326 case 1: 327 image = imageParts[0] 328 case 2: 329 image = imageParts[0] 330 tag = imageParts[1] 331 default: 332 return "", "", fmt.Errorf(InvalidImageName) 333 } 334 335 repoParts := strings.Split(image, "/") 336 registry := getServer(env.Pod, registries) 337 338 var repo string 339 switch len(repoParts) { 340 case 1: 341 repo = repoParts[0] 342 case 2: 343 if repoParts[0] != env.Namespace { 344 if repoParts[0] != registry { 345 return "", "", fmt.Errorf(IncorrectNamespace) 346 } 347 //Allow users to pull public images 348 return image, tag, nil 349 } 350 repo = repoParts[1] 351 case 3: 352 if repoParts[0] != registry || repoParts[1] != env.Namespace { 353 return "", "", fmt.Errorf(IncorrectRegistryOrNamespace) 354 } 355 repo = repoParts[2] 356 default: 357 return "", "", fmt.Errorf(InvalidImageName) 358 } 359 if includeRegistry { 360 repositoryName = fmt.Sprintf("%s/%s/%s", registry, env.Namespace, repo) 361 } else { 362 repositoryName = fmt.Sprintf("%s/%s", env.Namespace, repo) 363 } 364 return repositoryName, tag, nil 365 } 366 367 // Publish publishes changes to a repo 368 func (d *SImages) Publish(repo notaryClient.Repository) error { 369 if err := repo.Publish(); err != nil { 370 return err 371 } 372 return nil 373 } 374 375 // dockerAuth returns a sessionized auth string for registry and notary requests 376 func dockerAuth(user *models.User) string { 377 authConfig := types.AuthConfig{ 378 Username: user.Email, 379 Password: fmt.Sprintf("SessionToken=%s", user.SessionToken), 380 } 381 encodedJSON, err := json.Marshal(authConfig) 382 if err != nil { 383 logrus.Fatal(err) 384 } 385 return base64.URLEncoding.EncodeToString(encodedJSON) 386 } 387 388 func localImageExists(ctx context.Context, fullImageName string, dockerCli *dockerClient.Client) bool { 389 imageList, err := dockerCli.ImageList(ctx, types.ImageListOptions{All: true, Filters: filters.NewArgs(filters.Arg("reference", fullImageName))}) 390 if err != nil { 391 return false 392 } 393 return len(imageList) > 0 394 } 395 396 func getServer(pod string, serverMap map[string]string) string { 397 if server, ok := serverMap[pod]; ok { 398 return server 399 } 400 return serverMap["default"] 401 } 402 403 func getPassphraseRetriever() notary.PassRetriever { 404 baseRetriever := passphrase.PromptRetriever() 405 env := map[string]string{ 406 "root": os.Getenv("NOTARY_ROOT_PASSPHRASE"), 407 "targets": os.Getenv("NOTARY_TARGETS_PASSPHRASE"), 408 "snapshot": os.Getenv("NOTARY_SNAPSHOT_PASSPHRASE"), 409 "delegation": os.Getenv("NOTARY_DELEGATION_PASSPHRASE"), 410 } 411 412 return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { 413 if v := env[alias]; v != "" { 414 return v, numAttempts > 1, nil 415 } 416 // For delegation roles, we can also try the "delegation" alias if it is specified 417 // Note that we don't check if the role name is for a delegation to allow for names like "user" 418 // since delegation keys can be shared across repositories 419 // This cannot be a base role or imported key, though. 420 if v := env["delegation"]; !data.IsBaseRole(data.RoleName(alias)) && v != "" { 421 return v, numAttempts > 1, nil 422 } 423 return baseRetriever(keyName, alias, createNew, numAttempts) 424 } 425 } 426 427 func getRootKey(rootKeyPath string, repo notaryClient.Repository, retriever notary.PassRetriever) ([]string, error) { 428 var rootKeyList []string 429 cryptoService := repo.GetCryptoService() 430 if rootKeyPath != "" { 431 privKey, err := readRootKey(rootKeyPath, retriever) 432 if err != nil { 433 return nil, err 434 } 435 err = cryptoService.AddKey(data.CanonicalRootRole, "", privKey) 436 if err != nil { 437 return nil, fmt.Errorf("Error importing key: %v", err) 438 } 439 rootKeyList = []string{privKey.ID()} 440 } else { 441 rootKeyList = cryptoService.ListKeys(data.CanonicalRootRole) 442 } 443 444 if len(rootKeyList) < 1 { 445 logrus.Println("No root keys found. Generating a new root key...") 446 rootPublicKey, err := cryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey) 447 if err != nil { 448 return nil, err 449 } 450 rootKeyList = []string{rootPublicKey.ID()} 451 } else { 452 // Chooses the first root key available, which is initialization specific 453 // but should return the HW one first. 454 logrus.Printf("Root key found, using: %s\n", rootKeyList[0]) 455 rootKeyList = rootKeyList[0:1] 456 } 457 458 return rootKeyList, nil 459 } 460 461 // Attempt to read a role key from a file, and return it as a data.PrivateKey 462 // Root key must be encrypted 463 func readRootKey(rootKeyPath string, retriever notary.PassRetriever) (data.PrivateKey, error) { 464 keyFile, err := os.Open(rootKeyPath) 465 if err != nil { 466 return nil, fmt.Errorf("Opening file to import as a root key: %v", err) 467 } 468 defer keyFile.Close() 469 470 pemBytes, err := ioutil.ReadAll(keyFile) 471 if err != nil { 472 return nil, fmt.Errorf("Error reading input root key file: %v", err) 473 } 474 if err = cryptoservice.CheckRootKeyIsEncrypted(pemBytes); err != nil { 475 return nil, err 476 } 477 478 privKey, _, err := trustmanager.GetPasswdDecryptBytes(retriever, pemBytes, "", data.CanonicalRootRole.String()) 479 if err != nil { 480 return nil, err 481 } 482 483 return privKey, nil 484 } 485 486 type httpAccess int 487 488 const ( 489 readOnly httpAccess = iota 490 readWrite 491 admin 492 ) 493 494 func getTransport(gun, notaryServer string, user *models.User, permission httpAccess) (http.RoundTripper, error) { 495 tlsConfig, err := tlsconfig.Client(tlsconfig.Options{ 496 CAFile: "", 497 InsecureSkipVerify: false, 498 CertFile: "", 499 KeyFile: "", 500 }) 501 if err != nil { 502 return nil, fmt.Errorf("unable to configure TLS: %s", err.Error()) 503 } 504 505 base := &http.Transport{ 506 Proxy: http.ProxyFromEnvironment, 507 Dial: (&net.Dialer{ 508 Timeout: 30 * time.Second, 509 KeepAlive: 30 * time.Second, 510 DualStack: true, 511 }).Dial, 512 TLSHandshakeTimeout: 10 * time.Second, 513 TLSClientConfig: tlsConfig, 514 DisableKeepAlives: true, 515 } 516 return tokenAuth(notaryServer, base, gun, user, permission) 517 } 518 519 func tokenAuth(trustServerURL string, baseTransport *http.Transport, gun string, user *models.User, permission httpAccess) (http.RoundTripper, error) { 520 authTransport := transport.NewTransport(baseTransport) 521 pingClient := &http.Client{ 522 Transport: authTransport, 523 Timeout: 5 * time.Second, 524 } 525 endpoint, err := url.Parse(trustServerURL) 526 if err != nil { 527 return nil, fmt.Errorf("Could not parse remote trust server url (%s): %s", trustServerURL, err.Error()) 528 } 529 if endpoint.Scheme == "" { 530 return nil, fmt.Errorf("Trust server url has to be in the form of http(s)://URL:PORT. Got: %s", trustServerURL) 531 } 532 subPath, err := url.Parse("v2/") 533 if err != nil { 534 return nil, fmt.Errorf("Failed to parse v2 subpath. This error should not have been reached. Please report it as an issue at https://github.com/docker/notary/issues: %s", err.Error()) 535 } 536 endpoint = endpoint.ResolveReference(subPath) 537 req, err := http.NewRequest("GET", endpoint.String(), nil) 538 if err != nil { 539 return nil, err 540 } 541 resp, err := pingClient.Do(req) 542 if err != nil { 543 logrus.Errorf("could not reach %s: %s", trustServerURL, err.Error()) 544 logrus.Info("continuing in offline mode") 545 return nil, nil 546 } 547 // non-nil err means we must close body 548 defer resp.Body.Close() 549 if (resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices) && 550 resp.StatusCode != http.StatusUnauthorized { 551 // If we didn't get a 2XX range or 401 status code, we're not talking to a notary server. 552 // The http client should be configured to handle redirects so at this point, 3XX is 553 // not a valid status code. 554 logrus.Errorf("could not reach %s: %d", trustServerURL, resp.StatusCode) 555 logrus.Info("continuing in offline mode") 556 return nil, nil 557 } 558 559 // challengeManager := auth.NewSimpleChallengeManager() 560 challengeManager := challenge.NewSimpleManager() 561 if err := challengeManager.AddResponse(resp); err != nil { 562 return nil, err 563 } 564 565 // ps := passwordStore{anonymous: permission == readOnly} 566 creds := credentials{ 567 username: user.Email, 568 sessionToken: user.SessionToken, 569 } 570 571 var actions []string 572 switch permission { 573 case admin: 574 actions = []string{"*"} 575 case readWrite: 576 actions = []string{"push", "pull"} 577 case readOnly: 578 actions = []string{"pull"} 579 default: 580 return nil, fmt.Errorf("Invalid permission requested for token authentication of gun %s", gun) 581 } 582 583 tokenHandler := auth.NewTokenHandler(authTransport, creds, gun, actions...) 584 basicHandler := auth.NewBasicHandler(creds) 585 586 modifier := auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler) 587 588 if permission != readOnly { 589 return newAuthRoundTripper(transport.NewTransport(baseTransport, modifier)), nil 590 } 591 592 // Try to authenticate read only repositories using basic username/password authentication 593 return newAuthRoundTripper(transport.NewTransport(baseTransport, modifier), 594 transport.NewTransport(baseTransport, auth.NewAuthorizer(challengeManager, auth.NewTokenHandler(authTransport, creds, gun, actions...)))), nil 595 } 596 597 // authRoundTripper tries to authenticate the requests via multiple HTTP transactions (until first succeed) 598 type authRoundTripper struct { 599 trippers []http.RoundTripper 600 } 601 602 func newAuthRoundTripper(trippers ...http.RoundTripper) http.RoundTripper { 603 return &authRoundTripper{trippers: trippers} 604 } 605 606 func (a *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 607 var resp *http.Response 608 // Try all run all transactions 609 for _, t := range a.trippers { 610 var err error 611 resp, err = t.RoundTrip(req) 612 // Reject on error 613 if err != nil { 614 return resp, err 615 } 616 617 // Stop when request is authorized/unknown error 618 if resp.StatusCode != http.StatusUnauthorized { 619 return resp, nil 620 } 621 } 622 623 // Return the last response 624 return resp, nil 625 } 626 627 type credentials struct { 628 username string 629 sessionToken string 630 refreshToken string 631 } 632 633 func (c credentials) Basic(url *url.URL) (string, string) { 634 return c.username, fmt.Sprintf("SessionToken=%s", c.sessionToken) 635 } 636 637 func (c credentials) RefreshToken(url *url.URL, service string) string { 638 return c.refreshToken 639 } 640 641 func (c credentials) SetRefreshToken(realm *url.URL, service, token string) { 642 c.refreshToken = token 643 } 644 645 func userHomeDir() string { 646 env := "HOME" 647 if runtime.GOOS == "windows" { 648 env = "USERPROFILE" 649 } else if runtime.GOOS == "plan9" { 650 env = "home" 651 } 652 return os.Getenv(env) 653 } 654 655 func convertTarget(t *notaryClient.TargetWithRole) (*Target, error) { 656 h, ok := t.Hashes["sha256"] 657 if !ok { 658 return nil, fmt.Errorf("no valid hash, expecting sha256") 659 } 660 return &Target{ 661 Name: t.Name, 662 Digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), 663 Size: t.Length, 664 Role: string(t.Role), 665 }, nil 666 }