github.com/hustcat/docker@v1.3.3-0.20160314103604-901c67a8eeab/api/client/trust.go (about) 1 package client 2 3 import ( 4 "encoding/hex" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net" 9 "net/http" 10 "net/url" 11 "os" 12 "path" 13 "path/filepath" 14 "sort" 15 "strconv" 16 "time" 17 18 "github.com/Sirupsen/logrus" 19 "github.com/docker/distribution/digest" 20 "github.com/docker/distribution/registry/client/auth" 21 "github.com/docker/distribution/registry/client/transport" 22 "github.com/docker/docker/cliconfig" 23 "github.com/docker/docker/distribution" 24 "github.com/docker/docker/dockerversion" 25 "github.com/docker/docker/pkg/jsonmessage" 26 flag "github.com/docker/docker/pkg/mflag" 27 "github.com/docker/docker/reference" 28 "github.com/docker/docker/registry" 29 apiclient "github.com/docker/engine-api/client" 30 "github.com/docker/engine-api/types" 31 registrytypes "github.com/docker/engine-api/types/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/tuf/data" 37 "github.com/docker/notary/tuf/signed" 38 "github.com/docker/notary/tuf/store" 39 ) 40 41 var ( 42 releasesRole = path.Join(data.CanonicalTargetsRole, "releases") 43 untrusted bool 44 ) 45 46 func addTrustedFlags(fs *flag.FlagSet, verify bool) { 47 var trusted bool 48 if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" { 49 if t, err := strconv.ParseBool(e); t || err != nil { 50 // treat any other value as true 51 trusted = true 52 } 53 } 54 message := "Skip image signing" 55 if verify { 56 message = "Skip image verification" 57 } 58 fs.BoolVar(&untrusted, []string{"-disable-content-trust"}, !trusted, message) 59 } 60 61 func isTrusted() bool { 62 return !untrusted 63 } 64 65 type target struct { 66 reference registry.Reference 67 digest digest.Digest 68 size int64 69 } 70 71 func (cli *DockerCli) trustDirectory() string { 72 return filepath.Join(cliconfig.ConfigDir(), "trust") 73 } 74 75 // certificateDirectory returns the directory containing 76 // TLS certificates for the given server. An error is 77 // returned if there was an error parsing the server string. 78 func (cli *DockerCli) certificateDirectory(server string) (string, error) { 79 u, err := url.Parse(server) 80 if err != nil { 81 return "", err 82 } 83 84 return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil 85 } 86 87 func trustServer(index *registrytypes.IndexInfo) (string, error) { 88 if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" { 89 urlObj, err := url.Parse(s) 90 if err != nil || urlObj.Scheme != "https" { 91 return "", fmt.Errorf("valid https URL required for trust server, got %s", s) 92 } 93 94 return s, nil 95 } 96 if index.Official { 97 return registry.NotaryServer, nil 98 } 99 return "https://" + index.Name, nil 100 } 101 102 type simpleCredentialStore struct { 103 auth types.AuthConfig 104 } 105 106 func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) { 107 return scs.auth.Username, scs.auth.Password 108 } 109 110 // getNotaryRepository returns a NotaryRepository which stores all the 111 // information needed to operate on a notary repository. 112 // It creates a HTTP transport providing authentication support. 113 func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) { 114 server, err := trustServer(repoInfo.Index) 115 if err != nil { 116 return nil, err 117 } 118 119 var cfg = tlsconfig.ClientDefault 120 cfg.InsecureSkipVerify = !repoInfo.Index.Secure 121 122 // Get certificate base directory 123 certDir, err := cli.certificateDirectory(server) 124 if err != nil { 125 return nil, err 126 } 127 logrus.Debugf("reading certificate directory: %s", certDir) 128 129 if err := registry.ReadCertsDirectory(&cfg, certDir); err != nil { 130 return nil, err 131 } 132 133 base := &http.Transport{ 134 Proxy: http.ProxyFromEnvironment, 135 Dial: (&net.Dialer{ 136 Timeout: 30 * time.Second, 137 KeepAlive: 30 * time.Second, 138 DualStack: true, 139 }).Dial, 140 TLSHandshakeTimeout: 10 * time.Second, 141 TLSClientConfig: &cfg, 142 DisableKeepAlives: true, 143 } 144 145 // Skip configuration headers since request is not going to Docker daemon 146 modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), http.Header{}) 147 authTransport := transport.NewTransport(base, modifiers...) 148 pingClient := &http.Client{ 149 Transport: authTransport, 150 Timeout: 5 * time.Second, 151 } 152 endpointStr := server + "/v2/" 153 req, err := http.NewRequest("GET", endpointStr, nil) 154 if err != nil { 155 return nil, err 156 } 157 158 challengeManager := auth.NewSimpleChallengeManager() 159 160 resp, err := pingClient.Do(req) 161 if err != nil { 162 // Ignore error on ping to operate in offline mode 163 logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) 164 } else { 165 defer resp.Body.Close() 166 167 // Add response to the challenge manager to parse out 168 // authentication header and register authentication method 169 if err := challengeManager.AddResponse(resp); err != nil { 170 return nil, err 171 } 172 } 173 174 creds := simpleCredentialStore{auth: authConfig} 175 tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...) 176 basicHandler := auth.NewBasicHandler(creds) 177 modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) 178 tr := transport.NewTransport(base, modifiers...) 179 180 return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever()) 181 } 182 183 func convertTarget(t client.Target) (target, error) { 184 h, ok := t.Hashes["sha256"] 185 if !ok { 186 return target{}, errors.New("no valid hash, expecting sha256") 187 } 188 return target{ 189 reference: registry.ParseReference(t.Name), 190 digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), 191 size: t.Length, 192 }, nil 193 } 194 195 func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever { 196 aliasMap := map[string]string{ 197 "root": "root", 198 "snapshot": "repository", 199 "targets": "repository", 200 "targets/releases": "repository", 201 } 202 baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out, aliasMap) 203 env := map[string]string{ 204 "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), 205 "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 206 "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 207 "targets/releases": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 208 } 209 210 // Backwards compatibility with old env names. We should remove this in 1.10 211 if env["root"] == "" { 212 if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE"); passphrase != "" { 213 env["root"] = passphrase 214 fmt.Fprintf(cli.err, "[DEPRECATED] The environment variable DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE has been deprecated and will be removed in v1.10. Please use DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE\n") 215 } 216 } 217 if env["snapshot"] == "" || env["targets"] == "" || env["targets/releases"] == "" { 218 if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE"); passphrase != "" { 219 env["snapshot"] = passphrase 220 env["targets"] = passphrase 221 env["targets/releases"] = passphrase 222 fmt.Fprintf(cli.err, "[DEPRECATED] The environment variable DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE has been deprecated and will be removed in v1.10. Please use DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE\n") 223 } 224 } 225 226 return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { 227 if v := env[alias]; v != "" { 228 return v, numAttempts > 1, nil 229 } 230 return baseRetriever(keyName, alias, createNew, numAttempts) 231 } 232 } 233 234 func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Canonical, error) { 235 repoInfo, err := registry.ParseRepositoryInfo(ref) 236 if err != nil { 237 return nil, err 238 } 239 240 // Resolve the Auth config relevant for this server 241 authConfig := cli.resolveAuthConfig(repoInfo.Index) 242 243 notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig) 244 if err != nil { 245 fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) 246 return nil, err 247 } 248 249 t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) 250 if err != nil { 251 return nil, err 252 } 253 r, err := convertTarget(t.Target) 254 if err != nil { 255 return nil, err 256 257 } 258 259 return reference.WithDigest(ref, r.digest) 260 } 261 262 func (cli *DockerCli) tagTrusted(trustedRef reference.Canonical, ref reference.NamedTagged) error { 263 fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String()) 264 265 options := types.ImageTagOptions{ 266 ImageID: trustedRef.String(), 267 RepositoryName: trustedRef.Name(), 268 Tag: ref.Tag(), 269 Force: true, 270 } 271 272 return cli.client.ImageTag(options) 273 } 274 275 func notaryError(repoName string, err error) error { 276 switch err.(type) { 277 case *json.SyntaxError: 278 logrus.Debugf("Notary syntax error: %s", err) 279 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) 280 case signed.ErrExpired: 281 return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) 282 case trustmanager.ErrKeyNotFound: 283 return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) 284 case *net.OpError: 285 return fmt.Errorf("Error: error contacting notary server: %v", err) 286 case store.ErrMetaNotFound: 287 return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) 288 case signed.ErrInvalidKeyType: 289 return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) 290 case signed.ErrNoKeys: 291 return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) 292 case signed.ErrLowVersion: 293 return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) 294 case signed.ErrRoleThreshold: 295 return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) 296 case client.ErrRepositoryNotExist: 297 return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) 298 case signed.ErrInsufficientSignatures: 299 return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) 300 } 301 302 return err 303 } 304 305 func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error { 306 var refs []target 307 308 notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull") 309 if err != nil { 310 fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) 311 return err 312 } 313 314 if ref.String() == "" { 315 // List all targets 316 targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) 317 if err != nil { 318 return notaryError(repoInfo.FullName(), err) 319 } 320 for _, tgt := range targets { 321 t, err := convertTarget(tgt.Target) 322 if err != nil { 323 fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name()) 324 continue 325 } 326 refs = append(refs, t) 327 } 328 } else { 329 t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole) 330 if err != nil { 331 return notaryError(repoInfo.FullName(), err) 332 } 333 r, err := convertTarget(t.Target) 334 if err != nil { 335 return err 336 337 } 338 refs = append(refs, r) 339 } 340 341 for i, r := range refs { 342 displayTag := r.reference.String() 343 if displayTag != "" { 344 displayTag = ":" + displayTag 345 } 346 fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) 347 348 if err := cli.imagePullPrivileged(authConfig, repoInfo.Name(), r.digest.String(), requestPrivilege); err != nil { 349 return err 350 } 351 352 // If reference is not trusted, tag by trusted reference 353 if !r.reference.HasDigest() { 354 tagged, err := reference.WithTag(repoInfo, r.reference.String()) 355 if err != nil { 356 return err 357 } 358 trustedRef, err := reference.WithDigest(repoInfo, r.digest) 359 if err != nil { 360 return err 361 } 362 if err := cli.tagTrusted(trustedRef, tagged); err != nil { 363 return err 364 } 365 } 366 } 367 return nil 368 } 369 370 func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error { 371 responseBody, err := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, requestPrivilege) 372 if err != nil { 373 return err 374 } 375 376 defer responseBody.Close() 377 378 // If it is a trusted push we would like to find the target entry which match the 379 // tag provided in the function and then do an AddTarget later. 380 target := &client.Target{} 381 // Count the times of calling for handleTarget, 382 // if it is called more that once, that should be considered an error in a trusted push. 383 cnt := 0 384 handleTarget := func(aux *json.RawMessage) { 385 cnt++ 386 if cnt > 1 { 387 // handleTarget should only be called one. This will be treated as an error. 388 return 389 } 390 391 var pushResult distribution.PushResult 392 err := json.Unmarshal(*aux, &pushResult) 393 if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { 394 h, err := hex.DecodeString(pushResult.Digest.Hex()) 395 if err != nil { 396 target = nil 397 return 398 } 399 target.Name = registry.ParseReference(pushResult.Tag).String() 400 target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h} 401 target.Length = int64(pushResult.Size) 402 } 403 } 404 405 // We want trust signatures to always take an explicit tag, 406 // otherwise it will act as an untrusted push. 407 if tag == "" { 408 if err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil); err != nil { 409 return err 410 } 411 fmt.Fprintln(cli.out, "No tag specified, skipping trust metadata push") 412 return nil 413 } 414 415 if err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, handleTarget); err != nil { 416 return err 417 } 418 419 if cnt > 1 { 420 return fmt.Errorf("internal error: only one call to handleTarget expected") 421 } 422 423 if target == nil { 424 fmt.Fprintln(cli.out, "No targets found, please provide a specific tag in order to sign it") 425 return nil 426 } 427 428 fmt.Fprintln(cli.out, "Signing and pushing trust metadata") 429 430 repo, err := cli.getNotaryRepository(repoInfo, authConfig, "push", "pull") 431 if err != nil { 432 fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err) 433 return err 434 } 435 436 if err := repo.AddTarget(target, releasesRole); err != nil { 437 return err 438 } 439 440 err = repo.Publish() 441 if err == nil { 442 fmt.Fprintf(cli.out, "Successfully signed %q:%s\n", repoInfo.FullName(), tag) 443 return nil 444 } else if _, ok := err.(client.ErrRepoNotInitialized); !ok { 445 fmt.Fprintf(cli.out, "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error()) 446 return notaryError(repoInfo.FullName(), err) 447 } 448 449 keys := repo.CryptoService.ListKeys(data.CanonicalRootRole) 450 451 var rootKeyID string 452 // always select the first root key 453 if len(keys) > 0 { 454 sort.Strings(keys) 455 rootKeyID = keys[0] 456 } else { 457 rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, data.ECDSAKey) 458 if err != nil { 459 return err 460 } 461 rootKeyID = rootPublicKey.ID() 462 } 463 464 // Initialize the notary repository with a remotely managed snapshot key 465 if err := repo.Initialize(rootKeyID, data.CanonicalSnapshotRole); err != nil { 466 return notaryError(repoInfo.FullName(), err) 467 } 468 fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName()) 469 470 return notaryError(repoInfo.FullName(), repo.Publish()) 471 }