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