Move image trust related cli methods into the image package.

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2016-08-29 14:45:29 -04:00
parent 89bc5d54ca
commit e7c9694d76
9 changed files with 604 additions and 580 deletions

View File

@ -21,6 +21,13 @@ import (
"github.com/docker/go-connections/tlsconfig"
)
// Streams is an interface which exposes the standard input and output streams
type Streams interface {
In() *InStream
Out() *OutStream
Err() io.Writer
}
// DockerCli represents the docker command line client.
// Instances of the client can be returned from NewDockerCli.
type DockerCli struct {
@ -105,7 +112,7 @@ func NewAPIClientFromFlags(opts *cliflags.CommonOptions, configFile *configfile.
if customHeaders == nil {
customHeaders = map[string]string{}
}
customHeaders["User-Agent"] = clientUserAgent()
customHeaders["User-Agent"] = UserAgent()
verStr := api.DefaultVersion
if tmpStr := os.Getenv("DOCKER_API_VERSION"); tmpStr != "" {
@ -159,6 +166,7 @@ func newHTTPClient(host string, tlsOptions *tlsconfig.Options) (*http.Client, er
}, nil
}
func clientUserAgent() string {
// UserAgent returns the user agent string used for making API requests
func UserAgent() string {
return "Docker-Client/" + dockerversion.Version + " (" + runtime.GOOS + ")"
}

View File

@ -9,6 +9,7 @@ import (
"github.com/docker/docker/cli"
"github.com/docker/docker/cli/command"
"github.com/docker/docker/cli/command/image"
"github.com/docker/docker/pkg/jsonmessage"
// FIXME migrate to docker/distribution/reference
"github.com/docker/docker/api/types"
@ -169,7 +170,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *
if ref, ok := ref.(reference.NamedTagged); ok && command.IsTrusted() {
var err error
trustedRef, err = dockerCli.TrustedReference(ctx, ref)
trustedRef, err = image.TrustedReference(ctx, dockerCli, ref)
if err != nil {
return nil, err
}
@ -190,7 +191,7 @@ func createContainer(ctx context.Context, dockerCli *command.DockerCli, config *
return nil, err
}
if ref, ok := ref.(reference.NamedTagged); ok && trustedRef != nil {
if err := dockerCli.TagTrusted(ctx, trustedRef, ref); err != nil {
if err := image.TagTrusted(ctx, dockerCli, trustedRef, ref); err != nil {
return nil, err
}
}

View File

@ -12,14 +12,9 @@ import (
"golang.org/x/net/context"
)
type streams interface {
In() *command.InStream
Out() *command.OutStream
}
// holdHijackedConnection handles copying input to and output from streams to the
// connection
func holdHijackedConnection(ctx context.Context, streams streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
func holdHijackedConnection(ctx context.Context, streams command.Streams, tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
var (
err error
restoreOnce sync.Once
@ -100,14 +95,14 @@ func holdHijackedConnection(ctx context.Context, streams streams, tty bool, inpu
return nil
}
func setRawTerminal(streams streams) error {
func setRawTerminal(streams command.Streams) error {
if err := streams.In().SetRawTerminal(); err != nil {
return err
}
return streams.Out().SetRawTerminal()
}
func restoreTerminal(streams streams, in io.Closer) error {
func restoreTerminal(streams command.Streams, in io.Closer) error {
streams.In().RestoreTerminal()
streams.Out().RestoreTerminal()
// WARNING: DO NOT REMOVE THE OS CHECK !!!

View File

@ -220,9 +220,12 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
var resolvedTags []*resolvedTag
if command.IsTrusted() {
translator := func(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) {
return TrustedReference(ctx, dockerCli, ref)
}
// Wrap the tar archive to replace the Dockerfile entry with the rewritten
// Dockerfile which uses trusted pulls.
buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, dockerCli.TrustedReference, &resolvedTags)
buildCtx = replaceDockerfileTarWrapper(ctx, buildCtx, relDockerfile, translator, &resolvedTags)
}
// Setup an upload progress bar
@ -323,7 +326,7 @@ func runBuild(dockerCli *command.DockerCli, options buildOptions) error {
// Since the build was successful, now we must tag any of the resolved
// images from the above Dockerfile rewrite.
for _, resolved := range resolvedTags {
if err := dockerCli.TagTrusted(ctx, resolved.digestRef, resolved.tagRef); err != nil {
if err := TagTrusted(ctx, dockerCli, resolved.digestRef, resolved.tagRef); err != nil {
return err
}
}

View File

@ -78,9 +78,9 @@ func runPull(dockerCli *command.DockerCli, opts pullOptions) error {
if command.IsTrusted() && !registryRef.HasDigest() {
// Check if tag is digest
err = dockerCli.TrustedPull(ctx, repoInfo, registryRef, authConfig, requestPrivilege)
err = trustedPull(ctx, dockerCli, repoInfo, registryRef, authConfig, requestPrivilege)
} else {
err = dockerCli.ImagePullPrivileged(ctx, authConfig, distributionRef.String(), requestPrivilege, opts.all)
err = imagePullPrivileged(ctx, dockerCli, authConfig, distributionRef.String(), requestPrivilege, opts.all)
}
if err != nil {
if strings.Contains(err.Error(), "target is a plugin") {

View File

@ -48,10 +48,10 @@ func runPush(dockerCli *command.DockerCli, remote string) error {
requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "push")
if command.IsTrusted() {
return dockerCli.TrustedPush(ctx, repoInfo, ref, authConfig, requestPrivilege)
return trustedPush(ctx, dockerCli, repoInfo, ref, authConfig, requestPrivilege)
}
responseBody, err := dockerCli.ImagePushPrivileged(ctx, authConfig, ref.String(), requestPrivilege)
responseBody, err := imagePushPrivileged(ctx, dockerCli, authConfig, ref.String(), requestPrivilege)
if err != nil {
return err
}

576
cli/command/image/trust.go Normal file
View File

@ -0,0 +1,576 @@
package image
import (
"encoding/hex"
"encoding/json"
"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/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/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/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
)
var (
releasesRole = path.Join(data.CanonicalTargetsRole, "releases")
)
type target struct {
reference registry.Reference
digest digest.Digest
size int64
}
// trustedPush handles content trust pushing of an image
func trustedPush(ctx context.Context, cli *command.DockerCli, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
responseBody, err := imagePushPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege)
if err != nil {
return err
}
defer responseBody.Close()
// If it is a trusted push we would like to find the target entry which match the
// tag provided in the function and then do an AddTarget later.
target := &client.Target{}
// Count the times of calling for handleTarget,
// if it is called more that once, that should be considered an error in a trusted push.
cnt := 0
handleTarget := func(aux *json.RawMessage) {
cnt++
if cnt > 1 {
// handleTarget should only be called one. This will be treated as an error.
return
}
var pushResult distribution.PushResult
err := json.Unmarshal(*aux, &pushResult)
if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil {
h, err := hex.DecodeString(pushResult.Digest.Hex())
if err != nil {
target = nil
return
}
target.Name = registry.ParseReference(pushResult.Tag).String()
target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h}
target.Length = int64(pushResult.Size)
}
}
var tag string
switch x := ref.(type) {
case reference.Canonical:
return errors.New("cannot push a digest reference")
case reference.NamedTagged:
tag = x.Tag()
}
// 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
}
fmt.Fprintln(cli.Out(), "No tag specified, skipping trust metadata push")
return nil
}
if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil {
return err
}
if cnt > 1 {
return fmt.Errorf("internal error: only one call to handleTarget expected")
}
if target == nil {
fmt.Fprintln(cli.Out(), "No targets found, please provide a specific tag in order to sign it")
return nil
}
fmt.Fprintln(cli.Out(), "Signing and pushing trust metadata")
repo, err := GetNotaryRepository(cli, repoInfo, authConfig, "push", "pull")
if err != nil {
fmt.Fprintf(cli.Out(), "Error establishing connection to notary repository: %s\n", err)
return err
}
// get the latest repository metadata so we can figure out which roles to sign
err = repo.Update(false)
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
keys := repo.CryptoService.ListKeys(data.CanonicalRootRole)
var rootKeyID string
// always select the first root key
if len(keys) > 0 {
sort.Strings(keys)
rootKeyID = keys[0]
} else {
rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey)
if err != nil {
return err
}
rootKeyID = rootPublicKey.ID()
}
// Initialize the notary repository with a remotely managed snapshot key
if err := repo.Initialize(rootKeyID, data.CanonicalSnapshotRole); err != nil {
return notaryError(repoInfo.FullName(), err)
}
fmt.Fprintf(cli.Out(), "Finished initializing %q\n", repoInfo.FullName())
err = repo.AddTarget(target, data.CanonicalTargetsRole)
case nil:
// already initialized and we have successfully downloaded the latest metadata
err = addTargetToAllSignableRoles(repo, target)
default:
return notaryError(repoInfo.FullName(), err)
}
if err == nil {
err = repo.Publish()
}
if err != nil {
fmt.Fprintf(cli.Out(), "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error())
return notaryError(repoInfo.FullName(), err)
}
fmt.Fprintf(cli.Out(), "Successfully signed %q:%s\n", repoInfo.FullName(), tag)
return nil
}
// Attempt to add the image target to all the top level delegation roles we can
// (based on whether we have the signing key and whether the role's path allows
// us to).
// If there are no delegation roles, we add to the targets role.
func addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error {
var signableRoles []string
// translate the full key names, which includes the GUN, into just the key IDs
allCanonicalKeyIDs := make(map[string]struct{})
for fullKeyID := range repo.CryptoService.ListAllKeys() {
allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{}
}
allDelegationRoles, err := repo.GetDelegationRoles()
if err != nil {
return err
}
// if there are no delegation roles, then just try to sign it into the targets role
if len(allDelegationRoles) == 0 {
return repo.AddTarget(target, data.CanonicalTargetsRole)
}
// there are delegation roles, find every delegation role we have a key for, and
// attempt to sign into into all those roles.
for _, delegationRole := range allDelegationRoles {
// We do not support signing any delegation role that isn't a direct child of the targets role.
// Also don't bother checking the keys if we can't add the target
// to this role due to path restrictions
if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) {
continue
}
for _, canonicalKeyID := range delegationRole.KeyIDs {
if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok {
signableRoles = append(signableRoles, delegationRole.Name)
break
}
}
}
if len(signableRoles) == 0 {
return fmt.Errorf("no valid signing keys for delegation roles")
}
return repo.AddTarget(target, signableRoles...)
}
// imagePushPrivileged push the image
func imagePushPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) {
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return nil, err
}
options := types.ImagePushOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
}
return cli.Client().ImagePush(ctx, ref, options)
}
// 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 {
var refs []target
notaryRepo, err := 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() == "" {
// List all targets
targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole)
if err != nil {
return notaryError(repoInfo.FullName(), err)
}
for _, tgt := range targets {
t, err := convertTarget(tgt.Target)
if err != nil {
fmt.Fprintf(cli.Out(), "Skipping target for %q\n", repoInfo.Name())
continue
}
// 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 {
continue
}
refs = append(refs, t)
}
if len(refs) == 0 {
return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName()))
}
} else {
t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole)
if err != nil {
return 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()))
}
logrus.Debugf("retrieving target for %s role\n", t.Role)
r, err := convertTarget(t.Target)
if err != nil {
return err
}
refs = append(refs, r)
}
for i, r := range refs {
displayTag := r.reference.String()
if displayTag != "" {
displayTag = ":" + displayTag
}
fmt.Fprintf(cli.Out(), "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest)
ref, err := reference.WithDigest(repoInfo, r.digest)
if err != nil {
return err
}
if err := imagePullPrivileged(ctx, cli, authConfig, ref.String(), requestPrivilege, false); err != nil {
return err
}
// If reference is not trusted, tag by trusted reference
if !r.reference.HasDigest() {
tagged, err := reference.WithTag(repoInfo, r.reference.String())
if err != nil {
return err
}
trustedRef, err := reference.WithDigest(repoInfo, r.digest)
if err != nil {
return err
}
if err := TagTrusted(ctx, cli, trustedRef, tagged); err != nil {
return err
}
}
}
return nil
}
// imagePullPrivileged pulls the image and displays it to the output
func imagePullPrivileged(ctx context.Context, cli *command.DockerCli, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error {
encodedAuth, err := command.EncodeAuthToBase64(authConfig)
if err != nil {
return err
}
options := types.ImagePullOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
All: all,
}
responseBody, err := cli.Client().ImagePull(ctx, ref, options)
if err != nil {
return err
}
defer responseBody.Close()
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 := auth.NewSimpleChallengeManager()
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) passphrase.Retriever {
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)
if err != nil {
return nil, err
}
// Resolve the Auth config relevant for this server
authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index)
notaryRepo, err := 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)
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()))
}
r, err := convertTarget(t.Target)
if err != nil {
return nil, err
}
return reference.WithDigest(ref, r.digest)
}
func convertTarget(t client.Target) (target, error) {
h, ok := t.Hashes["sha256"]
if !ok {
return target{}, errors.New("no valid hash, expecting sha256")
}
return target{
reference: registry.ParseReference(t.Name),
digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
size: t.Length,
}, nil
}
// TagTrusted tags a trusted ref
func TagTrusted(ctx context.Context, cli *command.DockerCli, trustedRef reference.Canonical, ref reference.NamedTagged) error {
fmt.Fprintf(cli.Out(), "Tagging %s as %s\n", trustedRef.String(), ref.String())
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 *net.OpError:
return fmt.Errorf("Error: error contacting notary server: %v", err)
case store.ErrMetaNotFound:
return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err)
case 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

@ -1,4 +1,4 @@
package command
package image
import (
"os"

View File

@ -1,48 +1,15 @@
package command
import (
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"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/transport"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/cliconfig"
"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/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/signed"
"github.com/docker/notary/tuf/store"
"github.com/spf13/pflag"
)
var (
releasesRole = path.Join(data.CanonicalTargetsRole, "releases")
untrusted bool
// TODO: make this not global
untrusted bool
)
// AddTrustedFlags adds content trust flags to the current command flagset
@ -70,529 +37,3 @@ func setupTrustedFlag(verify bool) (bool, string) {
func IsTrusted() bool {
return !untrusted
}
type target struct {
reference registry.Reference
digest digest.Digest
size int64
}
func (cli *DockerCli) 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 (cli *DockerCli) 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.
func (cli *DockerCli) getNotaryRepository(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 := cli.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(clientUserAgent(), 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 := auth.NewSimpleChallengeManager()
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(
cli.trustDirectory(), repoInfo.FullName(), server, tr, cli.getPassphraseRetriever(),
trustpinning.TrustPinConfig{})
}
func convertTarget(t client.Target) (target, error) {
h, ok := t.Hashes["sha256"]
if !ok {
return target{}, errors.New("no valid hash, expecting sha256")
}
return target{
reference: registry.ParseReference(t.Name),
digest: digest.NewDigestFromHex("sha256", hex.EncodeToString(h)),
size: t.Length,
}, nil
}
func (cli *DockerCli) getPassphraseRetriever() passphrase.Retriever {
aliasMap := map[string]string{
"root": "root",
"snapshot": "repository",
"targets": "repository",
"default": "repository",
}
baseRetriever := passphrase.PromptRetrieverWithInOut(cli.in, cli.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 (cli *DockerCli) TrustedReference(ctx context.Context, ref reference.NamedTagged) (reference.Canonical, error) {
repoInfo, err := registry.ParseRepositoryInfo(ref)
if err != nil {
return nil, err
}
// Resolve the Auth config relevant for this server
authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index)
notaryRepo, err := cli.getNotaryRepository(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)
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()))
}
r, err := convertTarget(t.Target)
if err != nil {
return nil, err
}
return reference.WithDigest(ref, r.digest)
}
// TagTrusted tags a trusted ref
func (cli *DockerCli) TagTrusted(ctx context.Context, trustedRef reference.Canonical, ref reference.NamedTagged) error {
fmt.Fprintf(cli.out, "Tagging %s as %s\n", trustedRef.String(), ref.String())
return cli.client.ImageTag(ctx, trustedRef.String(), ref.String())
}
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 *net.OpError:
return fmt.Errorf("Error: error contacting notary server: %v", err)
case store.ErrMetaNotFound:
return fmt.Errorf("Error: trust data missing for remote repository %s or remote repository not found: %v", repoName, err)
case 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
}
// TrustedPull handles content trust pulling of an image
func (cli *DockerCli) TrustedPull(ctx context.Context, repoInfo *registry.RepositoryInfo, ref registry.Reference, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
var refs []target
notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull")
if err != nil {
fmt.Fprintf(cli.out, "Error establishing connection to trust repository: %s\n", err)
return err
}
if ref.String() == "" {
// List all targets
targets, err := notaryRepo.ListTargets(releasesRole, data.CanonicalTargetsRole)
if err != nil {
return notaryError(repoInfo.FullName(), err)
}
for _, tgt := range targets {
t, err := convertTarget(tgt.Target)
if err != nil {
fmt.Fprintf(cli.out, "Skipping target for %q\n", repoInfo.Name())
continue
}
// 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 {
continue
}
refs = append(refs, t)
}
if len(refs) == 0 {
return notaryError(repoInfo.FullName(), fmt.Errorf("No trusted tags for %s", repoInfo.FullName()))
}
} else {
t, err := notaryRepo.GetTargetByName(ref.String(), releasesRole, data.CanonicalTargetsRole)
if err != nil {
return 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()))
}
logrus.Debugf("retrieving target for %s role\n", t.Role)
r, err := convertTarget(t.Target)
if err != nil {
return err
}
refs = append(refs, r)
}
for i, r := range refs {
displayTag := r.reference.String()
if displayTag != "" {
displayTag = ":" + displayTag
}
fmt.Fprintf(cli.out, "Pull (%d of %d): %s%s@%s\n", i+1, len(refs), repoInfo.Name(), displayTag, r.digest)
ref, err := reference.WithDigest(repoInfo, r.digest)
if err != nil {
return err
}
if err := cli.ImagePullPrivileged(ctx, authConfig, ref.String(), requestPrivilege, false); err != nil {
return err
}
// If reference is not trusted, tag by trusted reference
if !r.reference.HasDigest() {
tagged, err := reference.WithTag(repoInfo, r.reference.String())
if err != nil {
return err
}
trustedRef, err := reference.WithDigest(repoInfo, r.digest)
if err != nil {
return err
}
if err := cli.TagTrusted(ctx, trustedRef, tagged); err != nil {
return err
}
}
}
return nil
}
// TrustedPush handles content trust pushing of an image
func (cli *DockerCli) TrustedPush(ctx context.Context, repoInfo *registry.RepositoryInfo, ref reference.Named, authConfig types.AuthConfig, requestPrivilege types.RequestPrivilegeFunc) error {
responseBody, err := cli.ImagePushPrivileged(ctx, authConfig, ref.String(), requestPrivilege)
if err != nil {
return err
}
defer responseBody.Close()
// If it is a trusted push we would like to find the target entry which match the
// tag provided in the function and then do an AddTarget later.
target := &client.Target{}
// Count the times of calling for handleTarget,
// if it is called more that once, that should be considered an error in a trusted push.
cnt := 0
handleTarget := func(aux *json.RawMessage) {
cnt++
if cnt > 1 {
// handleTarget should only be called one. This will be treated as an error.
return
}
var pushResult distribution.PushResult
err := json.Unmarshal(*aux, &pushResult)
if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil {
h, err := hex.DecodeString(pushResult.Digest.Hex())
if err != nil {
target = nil
return
}
target.Name = registry.ParseReference(pushResult.Tag).String()
target.Hashes = data.Hashes{string(pushResult.Digest.Algorithm()): h}
target.Length = int64(pushResult.Size)
}
}
var tag string
switch x := ref.(type) {
case reference.Canonical:
return errors.New("cannot push a digest reference")
case reference.NamedTagged:
tag = x.Tag()
}
// 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
}
fmt.Fprintln(cli.out, "No tag specified, skipping trust metadata push")
return nil
}
if err = jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), handleTarget); err != nil {
return err
}
if cnt > 1 {
return fmt.Errorf("internal error: only one call to handleTarget expected")
}
if target == nil {
fmt.Fprintln(cli.out, "No targets found, please provide a specific tag in order to sign it")
return nil
}
fmt.Fprintln(cli.out, "Signing and pushing trust metadata")
repo, err := cli.getNotaryRepository(repoInfo, authConfig, "push", "pull")
if err != nil {
fmt.Fprintf(cli.out, "Error establishing connection to notary repository: %s\n", err)
return err
}
// get the latest repository metadata so we can figure out which roles to sign
err = repo.Update(false)
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
keys := repo.CryptoService.ListKeys(data.CanonicalRootRole)
var rootKeyID string
// always select the first root key
if len(keys) > 0 {
sort.Strings(keys)
rootKeyID = keys[0]
} else {
rootPublicKey, err := repo.CryptoService.Create(data.CanonicalRootRole, "", data.ECDSAKey)
if err != nil {
return err
}
rootKeyID = rootPublicKey.ID()
}
// Initialize the notary repository with a remotely managed snapshot key
if err := repo.Initialize(rootKeyID, data.CanonicalSnapshotRole); err != nil {
return notaryError(repoInfo.FullName(), err)
}
fmt.Fprintf(cli.out, "Finished initializing %q\n", repoInfo.FullName())
err = repo.AddTarget(target, data.CanonicalTargetsRole)
case nil:
// already initialized and we have successfully downloaded the latest metadata
err = cli.addTargetToAllSignableRoles(repo, target)
default:
return notaryError(repoInfo.FullName(), err)
}
if err == nil {
err = repo.Publish()
}
if err != nil {
fmt.Fprintf(cli.out, "Failed to sign %q:%s - %s\n", repoInfo.FullName(), tag, err.Error())
return notaryError(repoInfo.FullName(), err)
}
fmt.Fprintf(cli.out, "Successfully signed %q:%s\n", repoInfo.FullName(), tag)
return nil
}
// Attempt to add the image target to all the top level delegation roles we can
// (based on whether we have the signing key and whether the role's path allows
// us to).
// If there are no delegation roles, we add to the targets role.
func (cli *DockerCli) addTargetToAllSignableRoles(repo *client.NotaryRepository, target *client.Target) error {
var signableRoles []string
// translate the full key names, which includes the GUN, into just the key IDs
allCanonicalKeyIDs := make(map[string]struct{})
for fullKeyID := range repo.CryptoService.ListAllKeys() {
allCanonicalKeyIDs[path.Base(fullKeyID)] = struct{}{}
}
allDelegationRoles, err := repo.GetDelegationRoles()
if err != nil {
return err
}
// if there are no delegation roles, then just try to sign it into the targets role
if len(allDelegationRoles) == 0 {
return repo.AddTarget(target, data.CanonicalTargetsRole)
}
// there are delegation roles, find every delegation role we have a key for, and
// attempt to sign into into all those roles.
for _, delegationRole := range allDelegationRoles {
// We do not support signing any delegation role that isn't a direct child of the targets role.
// Also don't bother checking the keys if we can't add the target
// to this role due to path restrictions
if path.Dir(delegationRole.Name) != data.CanonicalTargetsRole || !delegationRole.CheckPaths(target.Name) {
continue
}
for _, canonicalKeyID := range delegationRole.KeyIDs {
if _, ok := allCanonicalKeyIDs[canonicalKeyID]; ok {
signableRoles = append(signableRoles, delegationRole.Name)
break
}
}
}
if len(signableRoles) == 0 {
return fmt.Errorf("no valid signing keys for delegation roles")
}
return repo.AddTarget(target, signableRoles...)
}
// ImagePullPrivileged pulls the image and displays it to the output
func (cli *DockerCli) ImagePullPrivileged(ctx context.Context, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error {
encodedAuth, err := EncodeAuthToBase64(authConfig)
if err != nil {
return err
}
options := types.ImagePullOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
All: all,
}
responseBody, err := cli.client.ImagePull(ctx, ref, options)
if err != nil {
return err
}
defer responseBody.Close()
return jsonmessage.DisplayJSONMessagesToStream(responseBody, cli.Out(), nil)
}
// ImagePushPrivileged push the image
func (cli *DockerCli) ImagePushPrivileged(ctx context.Context, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) {
encodedAuth, err := EncodeAuthToBase64(authConfig)
if err != nil {
return nil, err
}
options := types.ImagePushOptions{
RegistryAuth: encodedAuth,
PrivilegeFunc: requestPrivilege,
}
return cli.client.ImagePush(ctx, ref, options)
}