github.com/vieux/docker@v0.6.3-0.20161004191708-e097c2a938c7/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/client" 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/docker/notary/tuf/signed" 39 "github.com/docker/notary/tuf/store" 40 ) 41 42 var ( 43 releasesRole = path.Join(data.CanonicalTargetsRole, "releases") 44 ) 45 46 type target struct { 47 reference registry.Reference 48 digest digest.Digest 49 size int64 50 } 51 52 // trustedPush handles content trust pushing of an image 53 func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error { 54 responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege) 55 if err != nil { 56 return err 57 } 58 59 defer responseBody.Close() 60 61 // If it is a trusted push we would like to find the target entry which match the 62 // tag provided in the function and then do an AddTarget later. 63 target := &client.Target{} 64 // Count the times of calling for handleTarget, 65 // if it is called more that once, that should be considered an error in a trusted push. 66 cnt := 0 67 handleTarget := func(aux *json.RawMessage) { 68 cnt++ 69 if cnt > 1 { 70 // handleTarget should only be called one. This will be treated as an error. 71 return 72 } 73 74 var pushResult distribution.PushResult 75 err := json.Unmarshal(*aux, &pushResult) 76 if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { 77 h, err := hex.DecodeString(pushResult.Digest.Hex()) 78 if err != nil { 79 target = nil 80 return 81 } 82 target.Name = registry.ParseReference(pushResult.Tag).String() 83 target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} 84 target.Length = int64(pushResult.Size) 85 } 86 } 87 88 var tag string 89 switch x := ref.(type) { 90 case reference.Canonical: 91 return errors.New("cannot push a digest reference") 92 case reference.NamedTagged: 93 tag = x.Tag() 94 } 95 96 // We want trust signatures to always take an explicit tag, 97 // otherwise it will act as an untrusted push. 98 if tag == "" { 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(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 registry.Reference, 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 ref.String() == "" { 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(ref.String(), 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", ref.String())) 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.reference.String() 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(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 // If reference is not trusted, tag by trusted reference 302 if !r.reference.HasDigest() { 303 tagged, err := reference.WithTag(repoInfo, r.reference.String()) 304 if err != nil { 305 return err 306 } 307 trustedRef, err := reference.WithDigest(repoInfo, r.digest) 308 if err != nil { 309 return err 310 } 311 if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil { 312 return err 313 } 314 } 315 } 316 return nil 317 } 318 319 // imagePullPrivileged pulls the image and displays it to the output 320 func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { 321 322 encodedAuth, err := command.EncodeAuthToBase64(authConfig) 323 if err != nil { 324 return err 325 } 326 options := types.ImagePullOptions{ 327 RegistryAuth: encodedAuth, 328 PrivilegeFunc: requestPrivilege, 329 All: all, 330 } 331 332 responseBody, err := cli.Client().ImagePull(ctx, ref, options) 333 if err != nil { 334 return err 335 } 336 defer responseBody.Close() 337 338 return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil) 339 } 340 341 func trustDirectory() string { 342 return filepath.Join(cliconfig.ConfigDir(), "trust") 343 } 344 345 // certificateDirectory returns the directory containing 346 // TLS certificates for the given server. An error is 347 // returned if there was an error parsing the server string. 348 func certificateDirectory(server string) (string, error) { 349 u, err := url.Parse(server) 350 if err != nil { 351 return "", err 352 } 353 354 return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil 355 } 356 357 func trustServer(index *registrytypes.IndexInfo) (string, error) { 358 if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { 359 urlObj, err := url.Parse(s) 360 if err != nil || urlObj.Scheme != "https" { 361 return "", fmt.Errorf("valid https URL required for trust server, got %s", s) 362 } 363 364 return s, nil 365 } 366 if index.Official { 367 return registry.NotaryServer, nil 368 } 369 return "https://" + index.Name, nil 370 } 371 372 type simpleCredentialStore struct { 373 auth types.AuthConfig 374 } 375 376 func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { 377 return scs.auth.Username, scs.auth.Password 378 } 379 380 func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string { 381 return scs.auth.IdentityToken 382 } 383 384 func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) { 385 } 386 387 // GetNotaryRepository returns a NotaryRepository which stores all the 388 // information needed to operate on a notary repository. 389 // It creates an HTTP transport providing authentication support. 390 // TODO: move this too 391 func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { 392 server, err := trustServer(repoInfo.Index) 393 if err != nil { 394 return nil, err 395 } 396 397 var cfg = tlsconfig.ClientDefault() 398 cfg.InsecureSkipVerify = !repoInfo.Index.Secure 399 400 // Get certificate base directory 401 certDir, err := certificateDirectory(server) 402 if err != nil { 403 return nil, err 404 } 405 logrus.Debugf("reading certificate directory: %s", certDir) 406 407 if err := registry.ReadCertsDirectory(cfg, certDir); err != nil { 408 return nil, err 409 } 410 411 base := &http.Transport{ 412 Proxy: http.ProxyFromEnvironment, 413 Dial: (&net.Dialer{ 414 Timeout: 30 * time.Second, 415 KeepAlive: 30 * time.Second, 416 DualStack: true, 417 }).Dial, 418 TLSHandshakeTimeout: 10 * time.Second, 419 TLSClientConfig: cfg, 420 DisableKeepAlives: true, 421 } 422 423 // Skip configuration headers since request is not going to Docker daemon 424 modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{}) 425 authTransport := transport.NewTransport(base, modifiers...) 426 pingClient := &http.Client{ 427 Transport: authTransport, 428 Timeout: 5 * time.Second, 429 } 430 endpointStr := server + "/v2/" 431 req, err := http.NewRequest("GET", endpointStr, nil) 432 if err != nil { 433 return nil, err 434 } 435 436 challengeManager := auth.NewSimpleChallengeManager() 437 438 resp, err := pingClient.Do(req) 439 if err != nil { 440 // Ignore error on ping to operate in offline mode 441 logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) 442 } else { 443 defer resp.Body.Close() 444 445 // Add response to the challenge manager to parse out 446 // authentication header and register authentication method 447 if err := challengeManager.AddResponse(resp); err != nil { 448 return nil, err 449 } 450 } 451 452 creds := simpleCredentialStore{auth: authConfig} 453 tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) 454 basicHandler := auth.NewBasicHandler(creds) 455 modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) 456 tr := transport.NewTransport(base, modifiers...) 457 458 return client.NewNotaryRepository( 459 trustDirectory(), 460 repoInfo.FullName(), 461 server, 462 tr, 463 getPassphraseRetriever(streams), 464 trustpinning.TrustPinConfig{}) 465 } 466 467 func getPassphraseRetriever(streams command.Streams) passphrase.Retriever { 468 aliasMap := map[string]string{ 469 "root": "root", 470 "snapshot": "repository", 471 "targets": "repository", 472 "default": "repository", 473 } 474 baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap) 475 env := map[string]string{ 476 "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), 477 "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 478 "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 479 "default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 480 } 481 482 return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { 483 if v := env[alias]; v != "" { 484 return v, numAttempts > 1, nil 485 } 486 // For non-root roles, we can also try the "default" alias if it is specified 487 if v := env["default"]; v != "" && alias != data.CanonicalRootRole { 488 return v, numAttempts > 1, nil 489 } 490 return baseRetriever(keyName, alias, createNew, numAttempts) 491 } 492 } 493 494 // TrustedReference returns the canonical trusted reference for an image reference 495 func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) { 496 repoInfo, err := registry.ParseRepositoryInfo(ref) 497 if err != nil { 498 return nil, err 499 } 500 501 // Resolve the Auth config relevant for this server 502 authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index) 503 504 notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull") 505 if err != nil { 506 fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err) 507 return nil, err 508 } 509 510 t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) 511 if err != nil { 512 return nil, err 513 } 514 // Only list tags in the top level targets role or the releases delegation role - ignore 515 // all other delegation roles 516 if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole { 517 return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag())) 518 } 519 r, err := convertTarget(t.Target) 520 if err != nil { 521 return nil, err 522 523 } 524 525 return reference.WithDigest(ref, r.digest) 526 } 527 528 func convertTarget(t client.Target) (target, error) { 529 h, ok := t.Hashes["sha256"] 530 if !ok { 531 return target{}, errors.New("no valid hash, expecting sha256") 532 } 533 return target{ 534 reference: registry.ParseReference(t.Name), 535 digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), 536 size: t.Length, 537 }, nil 538 } 539 540 // TagTrusted tags a trusted ref 541 func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef reference.Canonical, ref reference.NamedTagged) error { 542 fmt.Fprintf(cli.Out(), "Tagging %s as %s\n", trustedRef.String(), ref.String()) 543 544 return cli.Client().ImageTag(ctx, trustedRef.String(), ref.String()) 545 } 546 547 // notaryError formats an error message received from the notary service 548 func notaryError(repoName string, err error) error { 549 switch err.(type) { 550 case *json.SyntaxError: 551 logrus.Debugf("Notary syntax error: %s", err) 552 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) 553 case signed.ErrExpired: 554 return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) 555 case trustmanager.ErrKeyNotFound: 556 return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) 557 case *net.OpError: 558 return fmt.Errorf("Error: error contacting notary server: %v", err) 559 case store.ErrMetaNotFound: 560 return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) 561 case signed.ErrInvalidKeyType: 562 return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) 563 case signed.ErrNoKeys: 564 return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) 565 case signed.ErrLowVersion: 566 return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) 567 case signed.ErrRoleThreshold: 568 return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) 569 case client.ErrRepositoryNotExist: 570 return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) 571 case signed.ErrInsufficientSignatures: 572 return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) 573 } 574 575 return err 576 }