github.com/ncdc/docker@v0.10.1-0.20160129113957-6c6729ef5b74/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 func (cli *DockerCli) getNotaryRepository(repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig) (*client.NotaryRepository, error) { 111 server, err := trustServer(repoInfo.Index) 112 if err != nil { 113 return nil, err 114 } 115 116 var cfg = tlsconfig.ClientDefault 117 cfg.InsecureSkipVerify = !repoInfo.Index.Secure 118 119 // Get certificate base directory 120 certDir, err := cli.certificateDirectory(server) 121 if err != nil { 122 return nil, err 123 } 124 logrus.Debugf("reading certificate directory: %s", certDir) 125 126 if err := registry.ReadCertsDirectory(&cfg, certDir); err != nil { 127 return nil, err 128 } 129 130 base := &http.Transport{ 131 Proxy: http.ProxyFromEnvironment, 132 Dial: (&net.Dialer{ 133 Timeout: 30 * time.Second, 134 KeepAlive: 30 * time.Second, 135 DualStack: true, 136 }).Dial, 137 TLSHandshakeTimeout: 10 * time.Second, 138 TLSClientConfig: &cfg, 139 DisableKeepAlives: true, 140 } 141 142 // Skip configuration headers since request is not going to Docker daemon 143 modifiers := registry.DockerHeaders(dockerversion.DockerUserAgent(), http.Header{}) 144 authTransport := transport.NewTransport(base, modifiers...) 145 pingClient := &http.Client{ 146 Transport: authTransport, 147 Timeout: 5 * time.Second, 148 } 149 endpointStr := server + "/v2/" 150 req, err := http.NewRequest("GET", endpointStr, nil) 151 if err != nil { 152 return nil, err 153 } 154 155 challengeManager := auth.NewSimpleChallengeManager() 156 157 resp, err := pingClient.Do(req) 158 if err != nil { 159 // Ignore error on ping to operate in offline mode 160 logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err) 161 } else { 162 defer resp.Body.Close() 163 164 // Add response to the challenge manager to parse out 165 // authentication header and register authentication method 166 if err := challengeManager.AddResponse(resp); err != nil { 167 return nil, err 168 } 169 } 170 171 creds := simpleCredentialStore{auth: authConfig} 172 tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), "push", "pull") 173 basicHandler := auth.NewBasicHandler(creds) 174 modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))) 175 tr := transport.NewTransport(base, modifiers...) 176 177 return client.NewNotaryRepository(cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever()) 178 } 179 180 func convertTarget(t client.Target) (target, error) { 181 h, ok := t.Hashes["sha256"] 182 if !ok { 183 return target{}, errors.New("no valid hash, expecting sha256") 184 } 185 return target{ 186 reference: registry.ParseReference(t.Name), 187 digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)), 188 size: t.Length, 189 }, nil 190 } 191 192 func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever { 193 aliasMap := map[string]string{ 194 "root": "root", 195 "snapshot": "repository", 196 "targets": "repository", 197 "targets/releases": "repository", 198 } 199 baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.out, aliasMap) 200 env := map[string]string{ 201 "root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"), 202 "snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 203 "targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 204 "targets/releases": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"), 205 } 206 207 // Backwards compatibility with old env names. We should remove this in 1.10 208 if env["root"] == "" { 209 if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_OFFLINE_PASSPHRASE"); passphrase != "" { 210 env["root"] = passphrase 211 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") 212 } 213 } 214 if env["snapshot"] == "" || env["targets"] == "" || env["targets/releases"] == "" { 215 if passphrase := os.Getenv("DOCKER_CONTENT_TRUST_TAGGING_PASSPHRASE"); passphrase != "" { 216 env["snapshot"] = passphrase 217 env["targets"] = passphrase 218 env["targets/releases"] = passphrase 219 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") 220 } 221 } 222 223 return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { 224 if v := env[alias]; v != "" { 225 return v, numAttempts > 1, nil 226 } 227 return baseRetriever(keyName, alias, createNew, numAttempts) 228 } 229 } 230 231 func (cli *DockerCli) trustedReference(ref reference.NamedTagged) (reference.Canonical, error) { 232 repoInfo, err := registry.ParseRepositoryInfo(ref) 233 if err != nil { 234 return nil, err 235 } 236 237 // Resolve the Auth config relevant for this server 238 authConfig := registry.ResolveAuthConfig(cli.configFile.AuthConfigs, repoInfo.Index) 239 240 notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig) 241 if err != nil { 242 fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) 243 return nil, err 244 } 245 246 t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole) 247 if err != nil { 248 return nil, err 249 } 250 r, err := convertTarget(t.Target) 251 if err != nil { 252 return nil, err 253 254 } 255 256 return reference.WithDigest(ref, r.digest) 257 } 258 259 func (cli *DockerCli) tagTrusted(trustedRef reference.Canonical, ref reference.NamedTagged) error { 260 fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String()) 261 262 options := types.ImageTagOptions{ 263 ImageID: trustedRef.String(), 264 RepositoryName: trustedRef.Name(), 265 Tag: ref.Tag(), 266 Force: true, 267 } 268 269 return cli.client.ImageTag(options) 270 } 271 272 func notaryError(repoName string, err error) error { 273 switch err.(type) { 274 case *json.SyntaxError: 275 logrus.Debugf("Notary syntax error: %s", err) 276 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) 277 case signed.ErrExpired: 278 return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err) 279 case trustmanager.ErrKeyNotFound: 280 return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err) 281 case *net.OpError: 282 return fmt.Errorf("Error: error contacting notary server: %v", err) 283 case store.ErrMetaNotFound: 284 return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err) 285 case signed.ErrInvalidKeyType: 286 return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err) 287 case signed.ErrNoKeys: 288 return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err) 289 case signed.ErrLowVersion: 290 return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err) 291 case signed.ErrRoleThreshold: 292 return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err) 293 case client.ErrRepositoryNotExist: 294 return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err) 295 case signed.ErrInsufficientSignatures: 296 return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err) 297 } 298 299 return err 300 } 301 302 func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error { 303 var refs []target 304 305 notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig) 306 if err != nil { 307 fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err) 308 return err 309 } 310 311 if ref.String() == "" { 312 // List all targets 313 targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole) 314 if err != nil { 315 return notaryError(repoInfo.FullName(), err) 316 } 317 for _, tgt := range targets { 318 t, err := convertTarget(tgt.Target) 319 if err != nil { 320 fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name()) 321 continue 322 } 323 refs = append(refs, t) 324 } 325 } else { 326 t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole) 327 if err != nil { 328 return notaryError(repoInfo.FullName(), err) 329 } 330 r, err := convertTarget(t.Target) 331 if err != nil { 332 return err 333 334 } 335 refs = append(refs, r) 336 } 337 338 for i, r := range refs { 339 displayTag := r.reference.String() 340 if displayTag != "" { 341 displayTag = ":" + displayTag 342 } 343 fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest) 344 345 if err := cli.imagePullPrivileged(authConfig, repoInfo.Name(), r.digest.String(), requestPrivilege); err != nil { 346 return err 347 } 348 349 // If reference is not trusted, tag by trusted reference 350 if !r.reference.HasDigest() { 351 tagged, err := reference.WithTag(repoInfo, r.reference.String()) 352 if err != nil { 353 return err 354 } 355 trustedRef, err := reference.WithDigest(repoInfo, r.digest) 356 if err != nil { 357 return err 358 } 359 if err := cli.tagTrusted(trustedRef, tagged); err != nil { 360 return err 361 } 362 } 363 } 364 return nil 365 } 366 367 func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error { 368 responseBody, err := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, requestPrivilege) 369 if err != nil { 370 return err 371 } 372 373 defer responseBody.Close() 374 375 targets := []target{} 376 handleTarget := func(aux *json.RawMessage) { 377 var pushResult distribution.PushResult 378 err := json.Unmarshal(*aux, &pushResult) 379 if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil { 380 targets = append(targets, target{ 381 reference: registry.ParseReference(pushResult.Tag), 382 digest: pushResult.Digest, 383 size: int64(pushResult.Size), 384 }) 385 } 386 } 387 388 err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, handleTarget) 389 if err != nil { 390 return err 391 } 392 393 if tag == "" { 394 fmt.Fprintf(cli.out, "No tag specified, skipping trust metadata push\n") 395 return nil 396 } 397 if len(targets) == 0 { 398 fmt.Fprintf(cli.out, "No targets found, skipping trust metadata push\n") 399 return nil 400 } 401 402 fmt.Fprintf(cli.out, "Signing and pushing trust metadata\n") 403 404 repo, err := cli.getNotaryRepository(repoInfo, authConfig) 405 if err != nil { 406 fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err) 407 return err 408 } 409 410 for _, target := range targets { 411 h, err := hex.DecodeString(target.digest.Hex()) 412 if err != nil { 413 return err 414 } 415 t := &client.Target{ 416 Name: target.reference.String(), 417 Hashes: data.Hashes{ 418 string(target.digest.Algorithm()): h, 419 }, 420 Length: int64(target.size), 421 } 422 if err := repo.AddTarget(t, releasesRole); err != nil { 423 return err 424 } 425 } 426 427 err = repo.Publish() 428 if _, ok := err.(client.ErrRepoNotInitialized); !ok { 429 return notaryError(repoInfo.FullName(), err) 430 } 431 432 keys := repo.CryptoService.ListKeys(data.CanonicalRootRole) 433 434 var rootKeyID string 435 // always select the first root key 436 if len(keys) > 0 { 437 sort.Strings(keys) 438 rootKeyID = keys[0] 439 } else { 440 rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, data.ECDSAKey) 441 if err != nil { 442 return err 443 } 444 rootKeyID = rootPublicKey.ID() 445 } 446 447 if err := repo.Initialize(rootKeyID); err != nil { 448 return notaryError(repoInfo.FullName(), err) 449 } 450 fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName()) 451 452 return notaryError(repoInfo.FullName(), repo.Publish()) 453 }