Merge pull request #29469 from aaronlehmann/swarm-content-trust-1.13

[1.13] Backport "Content trust for swarm services"
This commit is contained in:
Victor Vieux 2016-12-16 01:00:27 -08:00 committed by GitHub
commit c9a557765e
11 changed files with 496 additions and 337 deletions

View file

@ -55,16 +55,6 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
fmt.Fprintf(dockerCli.Out(), "Using default tag: %s\n", reference.DefaultTag)
}
var tag string
switch x := distributionRef.(type) {
case reference.Canonical:
tag = x.Digest().String()
case reference.NamedTagged:
tag = x.Tag()
}
registryRef := registry.ParseReference(tag)
// Resolve the Repository name from fqn to RepositoryInfo
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
if err != nil {
@ -76,9 +66,10 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
authConfig := command.ResolveAuthConfig(ctx, dockerCli, repoInfo.Index)
requestPrivilege := command.RegistryAuthenticationPrivilegedFunc(dockerCli, repoInfo.Index, "pull")
if command.IsTrusted() && !registryRef.HasDigest() {
// Check if tag is digest
err = trustedPull(ctx, dockerCli, repoInfo, registryRef, authConfig, requestPrivilege)
// Check if reference has a digest
_, isCanonical := distributionRef.(reference.Canonical)
if command.IsTrusted() && !isCanonical {
err = trustedPull(ctx, dockerCli, repoInfo, distributionRef, authConfig, requestPrivilege)
} else {
err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all)
}

View file

@ -6,47 +6,26 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"time"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/digest"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/cli/trust"
"github.com/docker/docker/distribution"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/go-connections/tlsconfig"
"github.com/docker/notary"
"github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/storage"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
)
var (
releasesRole = path.Join(data.CanonicalTargetsRole, "releases")
)
type target struct {
reference registry.Reference
name string
digest digest.Digest
size int64
}
@ -81,7 +60,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry
target = nil
return
}
target.Name = registry.ParseReference(pushResult.Tag).String()
target.Name = pushResult.Tag
target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h}
target.Length = int64(pushResult.Size)
}
@ -93,11 +72,9 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry
return errors.New("cannot push a digest reference")
case reference.NamedTagged:
tag = x.Tag()
}
default:
// We want trust signatures to always take an explicit tag,
// otherwise it will act as an untrusted push.
if tag == "" {
if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil); err != nil {
return err
}
@ -120,7 +97,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry
fmt.Fprintln(cli.Out(), "Signing and pushing trust metadata")
repo, err := GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull")
repo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull")
if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to notary repository: %s\n", err)
return err
@ -147,7 +124,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry
// Initialize the notary repository with a remotely managed snapshot key
if err := repo.Initialize([]string{rootKeyID}, data.CanonicalSnapshotRole); err != nil {
return notaryError(repoInfo.FullName(), err)
return trust.NotaryError(repoInfo.FullName(), err)
}
fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.FullName())
err = repo.AddTarget(target, data.CanonicalTargetsRole)
@ -155,7 +132,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry
// already initialized and we have successfully downloaded the latest metadata
err = addTargetToAllSignableRoles(repo, target)
default:
return notaryError(repoInfo.FullName(), err)
return trust.NotaryError(repoInfo.FullName(), err)
}
if err == nil {
@ -164,7 +141,7 @@ func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry
if err != nil {
fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error())
return notaryError(repoInfo.FullName(), err)
return trust.NotaryError(repoInfo.FullName(), err)
}
fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.FullName(), tag)
@ -234,20 +211,20 @@ func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig
}
// trustedPull handles content trust pulling of an image
func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
var refs []target
notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull")
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
return err
}
if ref.String() == "" {
if tagged, isTagged := ref.(reference.NamedTagged); !isTagged {
// List all targets
targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole)
targets, err := notaryRepo.ListTargets(trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return notaryError(repoInfo.FullName(), err)
return trust.NotaryError(repoInfo.FullName(), err)
}
for _, tgt := range targets {
t, err := convertTarget(tgt.Target)
@ -257,23 +234,23 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry
}
// Only list tags in the top level targets role or the releases delegation role - ignore
// all other delegation roles
if tgt.Role != releasesRole && tgt.Role != data.CanonicalTargetsRole {
if tgt.Role != trust.ReleasesRole && tgt.Role != data.CanonicalTargetsRole {
continue
}
refs = append(refs, t)
}
if len(refs) == 0 {
return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName()))
return trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName()))
}
} else {
t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole)
t, err := notaryRepo.GetTargetByName(tagged.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return notaryError(repoInfo.FullName(), err)
return trust.NotaryError(repoInfo.FullName(), err)
}
// Only get the tag if it's in the top level targets role or the releases delegation role
// ignore it if it's in any other delegation roles
if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole {
return notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String()))
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", tagged.Tag()))
}
logrus.Debugf("retrieving target for %s role\n", t.Role)
@ -286,7 +263,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry
}
for i, r := range refs {
displayTag := r.reference.String()
displayTag := r.name
if displayTag != "" {
displayTag = ":" + displayTag
}
@ -300,9 +277,7 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry
return err
}
// If reference is not trusted, tag by trusted reference
if !r.reference.HasDigest() {
tagged, err := reference.WithTag(repoInfo, r.reference.String())
tagged, err := reference.WithTag(repoInfo, r.name)
if err != nil {
return err
}
@ -314,7 +289,6 @@ func trustedPull(ctx context.Context, cli *command.DockerCli, repoInfo *registry
return err
}
}
}
return nil
}
@ -340,159 +314,6 @@ func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig
return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil)
}
func trustDirectory() string {
return filepath.Join(cliconfig.ConfigDir(), "trust")
}
// certificateDirectory returns the directory containing
// TLS certificates for the given server. An error is
// returned if there was an error parsing the server string.
func certificateDirectory(server string) (string, error) {
u, err := url.Parse(server)
if err != nil {
return "", err
}
return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil
}
func trustServer(index *registrytypes.IndexInfo) (string, error) {
if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" {
urlObj, err := url.Parse(s)
if err != nil || urlObj.Scheme != "https" {
return "", fmt.Errorf("valid https URL required for trust server, got %s", s)
}
return s, nil
}
if index.Official {
return registry.NotaryServer, nil
}
return "https://" + index.Name, nil
}
type simpleCredentialStore struct {
auth types.AuthConfig
}
func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
return scs.auth.Username, scs.auth.Password
}
func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string {
return scs.auth.IdentityToken
}
func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {
}
// GetNotaryRepository returns a NotaryRepository which stores all the
// information needed to operate on a notary repository.
// It creates an HTTP transport providing authentication support.
// TODO: move this too
func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) {
server, err := trustServer(repoInfo.Index)
if err != nil {
return nil, err
}
var cfg = tlsconfig.ClientDefault()
cfg.InsecureSkipVerify = !repoInfo.Index.Secure
// Get certificate base directory
certDir, err := certificateDirectory(server)
if err != nil {
return nil, err
}
logrus.Debugf("reading certificate directory: %s", certDir)
if err := registry.ReadCertsDirectory(cfg, certDir); err != nil {
return nil, err
}
base := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: cfg,
DisableKeepAlives: true,
}
// Skip configuration headers since request is not going to Docker daemon
modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{})
authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{
Transport: authTransport,
Timeout: 5 * time.Second,
}
endpointStr := server + "/v2/"
req, err := http.NewRequest("GET", endpointStr, nil)
if err != nil {
return nil, err
}
challengeManager := challenge.NewSimpleManager()
resp, err := pingClient.Do(req)
if err != nil {
// Ignore error on ping to operate in offline mode
logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err)
} else {
defer resp.Body.Close()
// Add response to the challenge manager to parse out
// authentication header and register authentication method
if err := challengeManager.AddResponse(resp); err != nil {
return nil, err
}
}
creds := simpleCredentialStore{auth: authConfig}
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...)
basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)))
tr := transport.NewTransport(base, modifiers...)
return client.NewNotaryRepository(
trustDirectory(),
repoInfo.FullName(),
server,
tr,
getPassphraseRetriever(streams),
trustpinning.TrustPinConfig{})
}
func getPassphraseRetriever(streams command.Streams) notary.PassRetriever {
aliasMap := map[string]string{
"root": "root",
"snapshot": "repository",
"targets": "repository",
"default": "repository",
}
baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap)
env := map[string]string{
"root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"),
"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
"targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
"default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
}
return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
if v := env[alias]; v != "" {
return v, numAttempts > 1, nil
}
// For non-root roles, we can also try the "default" alias if it is specified
if v := env["default"]; v != "" && alias != data.CanonicalRootRole {
return v, numAttempts > 1, nil
}
return baseRetriever(keyName, alias, createNew, numAttempts)
}
}
// TrustedReference returns the canonical trusted reference for an image reference
func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (reference.Canonical, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
@ -503,20 +324,20 @@ func TrustedReference(ctx context.Context, cli *command.DockerCli, ref reference
// Resolve the Auth config relevant for this server
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
notaryRepo, err := GetNotaryRepository(cli, repoInfo, authConfig, "pull")
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to trust repository: %s\n", err)
return nil, err
}
t, err := notaryRepo.GetTargetByName(ref.Tag(), releasesRole, data.CanonicalTargetsRole)
t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, err
}
// Only list tags in the top level targets role or the releases delegation role - ignore
// all other delegation roles
if t.Role != releasesRole && t.Role != data.CanonicalTargetsRole {
return nil, notaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag()))
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.Tag()))
}
r, err := convertTarget(t.Target)
if err != nil {
@ -533,7 +354,7 @@ func convertTarget(t client.Target) (target, error) {
return target{}, errors.New("no valid hash, expecting sha256")
}
return target{
reference: registry.ParseReference(t.Name),
name: t.Name,
digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
size: t.Length,
}, nil
@ -545,34 +366,3 @@ func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef referenc
return cli.Client().ImageTag(ctx, trustedRef.String(), ref.String())
}
// notaryError formats an error message received from the notary service
func notaryError(repoName string, err error) error {
switch err.(type) {
case *json.SyntaxError:
logrus.Debugf("Notary syntax error: %s", err)
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)
case signed.ErrExpired:
return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err)
case trustmanager.ErrKeyNotFound:
return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err)
case storage.NetworkError:
return fmt.Errorf("Error: error contacting notary server: %v", err)
case storage.ErrMetaNotFound:
return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err)
case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType:
return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err)
case signed.ErrNoKeys:
return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err)
case signed.ErrLowVersion:
return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err)
case signed.ErrRoleThreshold:
return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err)
case client.ErrRepositoryNotExist:
return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err)
case signed.ErrInsufficientSignatures:
return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err)
}
return err
}

View file

@ -5,6 +5,7 @@ import (
"testing"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/cli/trust"
"github.com/docker/docker/registry"
)
@ -19,7 +20,7 @@ func TestENVTrustServer(t *testing.T) {
if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "https://notary-test.com:5000"); err != nil {
t.Fatal("Failed to set ENV variable")
}
output, err := trustServer(indexInfo)
output, err := trust.Server(indexInfo)
expectedStr := "https://notary-test.com:5000"
if err != nil || output != expectedStr {
t.Fatalf("Expected server to be %s, got %s", expectedStr, output)
@ -32,7 +33,7 @@ func TestHTTPENVTrustServer(t *testing.T) {
if err := os.Setenv("DOCKER_CONTENT_TRUST_SERVER", "http://notary-test.com:5000"); err != nil {
t.Fatal("Failed to set ENV variable")
}
_, err := trustServer(indexInfo)
_, err := trust.Server(indexInfo)
if err == nil {
t.Fatal("Expected error with invalid scheme")
}
@ -40,7 +41,7 @@ func TestHTTPENVTrustServer(t *testing.T) {
func TestOfficialTrustServer(t *testing.T) {
indexInfo := &registrytypes.IndexInfo{Name: "testserver", Official: true}
output, err := trustServer(indexInfo)
output, err := trust.Server(indexInfo)
if err != nil || output != registry.NotaryServer {
t.Fatalf("Expected server to be %s, got %s", registry.NotaryServer, output)
}
@ -48,7 +49,7 @@ func TestOfficialTrustServer(t *testing.T) {
func TestNonOfficialTrustServer(t *testing.T) {
indexInfo := &registrytypes.IndexInfo{Name: "testserver", Official: false}
output, err := trustServer(indexInfo)
output, err := trust.Server(indexInfo)
expectedStr := "https://" + indexInfo.Name
if err != nil || output != expectedStr {
t.Fatalf("Expected server to be %s, got %s", expectedStr, output)

View file

@ -72,6 +72,10 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
ctx := context.Background()
if err := resolveServiceImageDigest(dockerCli, &service); err != nil {
return err
}
// only send auth if flag was set
if opts.registryAuth {
// Retrieve encoded auth token from the image reference

View file

@ -0,0 +1,96 @@
package service
import (
"encoding/hex"
"fmt"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/digest"
distreference "github.com/docker/distribution/reference"
"github.com/docker/docker/api/types/swarm"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/trust"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/notary/tuf/data"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
func resolveServiceImageDigest(dockerCli *command.DockerCli, service *swarm.ServiceSpec) error {
if !command.IsTrusted() {
// Digests are resolved by the daemon when not using content
// trust.
return nil
}
image := service.TaskTemplate.ContainerSpec.Image
// We only attempt to resolve the digest if the reference
// could be parsed as a digest reference. Specifying an image ID
// is valid but not resolvable. There is no warning message for
// an image ID because it's valid to use one.
if _, err := digest.ParseDigest(image); err == nil {
return nil
}
ref, err := reference.ParseNamed(image)
if err != nil {
return fmt.Errorf("Could not parse image reference %s", service.TaskTemplate.ContainerSpec.Image)
}
if _, ok := ref.(reference.Canonical); !ok {
ref = reference.WithDefaultTag(ref)
taggedRef, ok := ref.(reference.NamedTagged)
if !ok {
// This should never happen because a reference either
// has a digest, or WithDefaultTag would give it a tag.
return errors.New("Failed to resolve image digest using content trust: reference is missing a tag")
}
resolvedImage, err := trustedResolveDigest(context.Background(), dockerCli, taggedRef)
if err != nil {
return fmt.Errorf("Failed to resolve image digest using content trust: %v", err)
}
logrus.Debugf("resolved image tag to %s using content trust", resolvedImage.String())
service.TaskTemplate.ContainerSpec.Image = resolvedImage.String()
}
return nil
}
func trustedResolveDigest(ctx context.Context, cli *command.DockerCli, ref reference.NamedTagged) (distreference.Canonical, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return nil, err
}
authConfig := command.ResolveAuthConfig(ctx, cli, repoInfo.Index)
notaryRepo, err := trust.GetNotaryRepository(cli, repoInfo, authConfig, "pull")
if err != nil {
return nil, errors.Wrap(err, "error establishing connection to trust repository")
}
t, err := notaryRepo.GetTargetByName(ref.Tag(), trust.ReleasesRole, data.CanonicalTargetsRole)
if err != nil {
return nil, trust.NotaryError(repoInfo.FullName(), err)
}
// Only get the tag if it's in the top level targets role or the releases delegation role
// ignore it if it's in any other delegation roles
if t.Role != trust.ReleasesRole && t.Role != data.CanonicalTargetsRole {
return nil, trust.NotaryError(repoInfo.FullName(), fmt.Errorf("No trust data for %s", ref.String()))
}
logrus.Debugf("retrieving target for %s role\n", t.Role)
h, ok := t.Hashes["sha256"]
if !ok {
return nil, errors.New("no valid hash, expecting sha256")
}
dgst := digest.NewDigestFromHex("sha256", hex.EncodeToString(h))
// Using distribution reference package to make sure that adding a
// digest does not erase the tag. When the two reference packages
// are unified, this will no longer be an issue.
return distreference.WithDigest(ref, dgst)
}

View file

@ -103,6 +103,12 @@ func runUpdate(dockerCli *command.DockerCli, flags *pflag.FlagSet, serviceID str
return err
}
if flags.Changed("image") {
if err := resolveServiceImageDigest(dockerCli, spec); err != nil {
return err
}
}
updatedSecrets, err := getUpdatedSecrets(apiClient, flags, spec.TaskTemplate.ContainerSpec.Secrets)
if err != nil {
return err

221
cli/trust/trust.go Normal file
View file

@ -0,0 +1,221 @@
package trust
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"time"
"github.com/Sirupsen/logrus"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/registry"
"github.com/docker/go-connections/tlsconfig"
"github.com/docker/notary"
"github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/storage"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
)
var (
// ReleasesRole is the role named "releases"
ReleasesRole = path.Join(data.CanonicalTargetsRole, "releases")
)
func trustDirectory() string {
return filepath.Join(cliconfig.ConfigDir(), "trust")
}
// certificateDirectory returns the directory containing
// TLS certificates for the given server. An error is
// returned if there was an error parsing the server string.
func certificateDirectory(server string) (string, error) {
u, err := url.Parse(server)
if err != nil {
return "", err
}
return filepath.Join(cliconfig.ConfigDir(), "tls", u.Host), nil
}
// Server returns the base URL for the trust server.
func Server(index *registrytypes.IndexInfo) (string, error) {
if s := os.Getenv("DOCKER_CONTENT_TRUST_SERVER"); s != "" {
urlObj, err := url.Parse(s)
if err != nil || urlObj.Scheme != "https" {
return "", fmt.Errorf("valid https URL required for trust server, got %s", s)
}
return s, nil
}
if index.Official {
return registry.NotaryServer, nil
}
return "https://" + index.Name, nil
}
type simpleCredentialStore struct {
auth types.AuthConfig
}
func (scs simpleCredentialStore) Basic(u *url.URL) (string, string) {
return scs.auth.Username, scs.auth.Password
}
func (scs simpleCredentialStore) RefreshToken(u *url.URL, service string) string {
return scs.auth.IdentityToken
}
func (scs simpleCredentialStore) SetRefreshToken(*url.URL, string, string) {
}
// GetNotaryRepository returns a NotaryRepository which stores all the
// information needed to operate on a notary repository.
// It creates an HTTP transport providing authentication support.
func GetNotaryRepository(streams command.Streams, repoInfo *registry.RepositoryInfo, authConfig types.AuthConfig, actions ...string) (*client.NotaryRepository, error) {
server, err := Server(repoInfo.Index)
if err != nil {
return nil, err
}
var cfg = tlsconfig.ClientDefault()
cfg.InsecureSkipVerify = !repoInfo.Index.Secure
// Get certificate base directory
certDir, err := certificateDirectory(server)
if err != nil {
return nil, err
}
logrus.Debugf("reading certificate directory: %s", certDir)
if err := registry.ReadCertsDirectory(cfg, certDir); err != nil {
return nil, err
}
base := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: cfg,
DisableKeepAlives: true,
}
// Skip configuration headers since request is not going to Docker daemon
modifiers := registry.DockerHeaders(command.UserAgent(), http.Header{})
authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{
Transport: authTransport,
Timeout: 5 * time.Second,
}
endpointStr := server + "/v2/"
req, err := http.NewRequest("GET", endpointStr, nil)
if err != nil {
return nil, err
}
challengeManager := challenge.NewSimpleManager()
resp, err := pingClient.Do(req)
if err != nil {
// Ignore error on ping to operate in offline mode
logrus.Debugf("Error pinging notary server %q: %s", endpointStr, err)
} else {
defer resp.Body.Close()
// Add response to the challenge manager to parse out
// authentication header and register authentication method
if err := challengeManager.AddResponse(resp); err != nil {
return nil, err
}
}
creds := simpleCredentialStore{auth: authConfig}
tokenHandler := auth.NewTokenHandler(authTransport, creds, repoInfo.FullName(), actions...)
basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, transport.RequestModifier(auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler)))
tr := transport.NewTransport(base, modifiers...)
return client.NewNotaryRepository(
trustDirectory(),
repoInfo.FullName(),
server,
tr,
getPassphraseRetriever(streams),
trustpinning.TrustPinConfig{})
}
func getPassphraseRetriever(streams command.Streams) notary.PassRetriever {
aliasMap := map[string]string{
"root": "root",
"snapshot": "repository",
"targets": "repository",
"default": "repository",
}
baseRetriever := passphrase.PromptRetrieverWithInOut(streams.In(), streams.Out(), aliasMap)
env := map[string]string{
"root": os.Getenv("DOCKER_CONTENT_TRUST_ROOT_PASSPHRASE"),
"snapshot": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
"targets": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
"default": os.Getenv("DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE"),
}
return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) {
if v := env[alias]; v != "" {
return v, numAttempts > 1, nil
}
// For non-root roles, we can also try the "default" alias if it is specified
if v := env["default"]; v != "" && alias != data.CanonicalRootRole {
return v, numAttempts > 1, nil
}
return baseRetriever(keyName, alias, createNew, numAttempts)
}
}
// NotaryError formats an error message received from the notary service
func NotaryError(repoName string, err error) error {
switch err.(type) {
case *json.SyntaxError:
logrus.Debugf("Notary syntax error: %s", err)
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)
case signed.ErrExpired:
return fmt.Errorf("Error: remote repository %s out-of-date: %v", repoName, err)
case trustmanager.ErrKeyNotFound:
return fmt.Errorf("Error: signing keys for remote repository %s not found: %v", repoName, err)
case storage.NetworkError:
return fmt.Errorf("Error: error contacting notary server: %v", err)
case storage.ErrMetaNotFound:
return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err)
case trustpinning.ErrRootRotationFail, trustpinning.ErrValidationFail, signed.ErrInvalidKeyType:
return fmt.Errorf("Warning: potential malicious behavior - trust data mismatch for remote repository %s: %v", repoName, err)
case signed.ErrNoKeys:
return fmt.Errorf("Error: could not find signing keys for remote repository %s, or could not decrypt signing key: %v", repoName, err)
case signed.ErrLowVersion:
return fmt.Errorf("Warning: potential malicious behavior - trust data version is lower than expected for remote repository %s: %v", repoName, err)
case signed.ErrRoleThreshold:
return fmt.Errorf("Warning: potential malicious behavior - trust data has insufficient signatures for remote repository %s: %v", repoName, err)
case client.ErrRepositoryNotExist:
return fmt.Errorf("Error: remote trust data does not exist for %s: %v", repoName, err)
case signed.ErrInsufficientSignatures:
return fmt.Errorf("Error: could not produce valid signature for %s. If Yubikey was used, was touch input provided?: %v", repoName, err)
}
return err
}

View file

@ -1111,9 +1111,11 @@ func (c *Cluster) CreateService(s types.ServiceSpec, encodedAuth string) (*apity
if err != nil {
logrus.Warnf("unable to pin image %s to digest: %s", ctnr.Image, err.Error())
resp.Warnings = append(resp.Warnings, fmt.Sprintf("unable to pin image %s to digest: %s", ctnr.Image, err.Error()))
} else {
} else if ctnr.Image != digestImage {
logrus.Debugf("pinning image %s by digest: %s", ctnr.Image, digestImage)
ctnr.Image = digestImage
} else {
logrus.Debugf("creating service using supplied digest reference %s", ctnr.Image)
}
}
@ -1223,6 +1225,8 @@ func (c *Cluster) UpdateService(serviceIDOrName string, version uint64, spec typ
} else if newCtnr.Image != digestImage {
logrus.Debugf("pinning image %s by digest: %s", newCtnr.Image, digestImage)
newCtnr.Image = digestImage
} else {
logrus.Debugf("updating service using supplied digest reference %s", newCtnr.Image)
}
}

View file

@ -348,3 +348,36 @@ func (s *DockerTrustSuite) TearDownTest(c *check.C) {
os.RemoveAll(filepath.Join(cliconfig.ConfigDir(), "trust"))
s.ds.TearDownTest(c)
}
func init() {
ds := &DockerSuite{}
check.Suite(&DockerTrustedSwarmSuite{
trustSuite: DockerTrustSuite{
ds: ds,
},
swarmSuite: DockerSwarmSuite{
ds: ds,
},
})
}
type DockerTrustedSwarmSuite struct {
swarmSuite DockerSwarmSuite
trustSuite DockerTrustSuite
reg *testRegistryV2
not *testNotary
}
func (s *DockerTrustedSwarmSuite) SetUpTest(c *check.C) {
s.swarmSuite.SetUpTest(c)
s.trustSuite.SetUpTest(c)
}
func (s *DockerTrustedSwarmSuite) TearDownTest(c *check.C) {
s.trustSuite.TearDownTest(c)
s.swarmSuite.TearDownTest(c)
}
func (s *DockerTrustedSwarmSuite) OnTimeout(c *check.C) {
s.swarmSuite.OnTimeout(c)
}

View file

@ -1085,3 +1085,84 @@ func (s *DockerSwarmSuite) TestSwarmNetworkIPAMOptions(c *check.C) {
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(strings.TrimSpace(out), checker.Equals, "map[foo:bar]")
}
func (s *DockerTrustedSwarmSuite) TestTrustedServiceCreate(c *check.C) {
d := s.swarmSuite.AddDaemon(c, true, true)
// Attempt creating a service from an image that is known to notary.
repoName := s.trustSuite.setupTrustedImage(c, "trusted-pull")
name := "trusted"
serviceCmd := d.command("-D", "service", "create", "--name", name, repoName, "top")
s.trustSuite.trustedCmd(serviceCmd)
out, _, err := runCommandWithOutput(serviceCmd)
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(out, checker.Contains, "resolved image tag to", check.Commentf(out))
out, err = d.Cmd("service", "inspect", "--pretty", name)
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(out, checker.Contains, repoName+"@", check.Commentf(out))
// Try trusted service create on an untrusted tag.
repoName = fmt.Sprintf("%v/untrustedservicecreate/createtest:latest", privateRegistryURL)
// tag the image and upload it to the private registry
dockerCmd(c, "tag", "busybox", repoName)
dockerCmd(c, "push", repoName)
dockerCmd(c, "rmi", repoName)
name = "untrusted"
serviceCmd = d.command("service", "create", "--name", name, repoName, "top")
s.trustSuite.trustedCmd(serviceCmd)
out, _, err = runCommandWithOutput(serviceCmd)
c.Assert(err, check.NotNil, check.Commentf(out))
c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out))
out, err = d.Cmd("service", "inspect", "--pretty", name)
c.Assert(err, checker.NotNil, check.Commentf(out))
}
func (s *DockerTrustedSwarmSuite) TestTrustedServiceUpdate(c *check.C) {
d := s.swarmSuite.AddDaemon(c, true, true)
// Attempt creating a service from an image that is known to notary.
repoName := s.trustSuite.setupTrustedImage(c, "trusted-pull")
name := "myservice"
// Create a service without content trust
_, err := d.Cmd("service", "create", "--name", name, repoName, "top")
c.Assert(err, checker.IsNil)
out, err := d.Cmd("service", "inspect", "--pretty", name)
c.Assert(err, checker.IsNil, check.Commentf(out))
// Daemon won't insert the digest because this is disabled by
// DOCKER_SERVICE_PREFER_OFFLINE_IMAGE.
c.Assert(out, check.Not(checker.Contains), repoName+"@", check.Commentf(out))
serviceCmd := d.command("-D", "service", "update", "--image", repoName, name)
s.trustSuite.trustedCmd(serviceCmd)
out, _, err = runCommandWithOutput(serviceCmd)
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(out, checker.Contains, "resolved image tag to", check.Commentf(out))
out, err = d.Cmd("service", "inspect", "--pretty", name)
c.Assert(err, checker.IsNil, check.Commentf(out))
c.Assert(out, checker.Contains, repoName+"@", check.Commentf(out))
// Try trusted service update on an untrusted tag.
repoName = fmt.Sprintf("%v/untrustedservicecreate/createtest:latest", privateRegistryURL)
// tag the image and upload it to the private registry
dockerCmd(c, "tag", "busybox", repoName)
dockerCmd(c, "push", repoName)
dockerCmd(c, "rmi", repoName)
serviceCmd = d.command("service", "update", "--image", repoName, name)
s.trustSuite.trustedCmd(serviceCmd)
out, _, err = runCommandWithOutput(serviceCmd)
c.Assert(err, check.NotNil, check.Commentf(out))
c.Assert(string(out), checker.Contains, "Error: remote trust data does not exist", check.Commentf(out))
}

View file

@ -1,68 +0,0 @@
package registry
import (
"strings"
"github.com/docker/distribution/digest"
)
// Reference represents a tag or digest within a repository
type Reference interface {
// HasDigest returns whether the reference has a verifiable
// content addressable reference which may be considered secure.
HasDigest() bool
// ImageName returns an image name for the given repository
ImageName(string) string
// Returns a string representation of the reference
String() string
}
type tagReference struct {
tag string
}
func (tr tagReference) HasDigest() bool {
return false
}
func (tr tagReference) ImageName(repo string) string {
return repo + ":" + tr.tag
}
func (tr tagReference) String() string {
return tr.tag
}
type digestReference struct {
digest digest.Digest
}
func (dr digestReference) HasDigest() bool {
return true
}
func (dr digestReference) ImageName(repo string) string {
return repo + "@" + dr.String()
}
func (dr digestReference) String() string {
return dr.digest.String()
}
// ParseReference parses a reference into either a digest or tag reference
func ParseReference(ref string) Reference {
if strings.Contains(ref, ":") {
dgst, err := digest.ParseDigest(ref)
if err == nil {
return digestReference{digest: dgst}
}
}
return tagReference{tag: ref}
}
// DigestReference creates a digest reference using a digest
func DigestReference(dgst digest.Digest) Reference {
return digestReference{digest: dgst}
}