Преглед на файлове

Send push information to trust code out-of-band

The trust code used to parse the console output of `docker push` to
extract the digest, tag, and size information and determine what to
sign. This is fragile and might give an attacker control over what gets
signed if the attacker can find a way to influence what gets printed as
part of the push output.

This commit sends the push metadata out-of-band. It introduces an `Aux`
field in JSONMessage that can carry application-specific data alongside
progress updates. Instead of parsing formatted output, the client looks
in this field to get the digest, size, and tag from the push.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
Aaron Lehmann преди 9 години
родител
ревизия
65370be888

+ 1 - 1
api/client/build.go

@@ -255,7 +255,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
 		return err
 		return err
 	}
 	}
 
 
-	err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut)
+	err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut, nil)
 	if err != nil {
 	if err != nil {
 		if jerr, ok := err.(*jsonmessage.JSONError); ok {
 		if jerr, ok := err.(*jsonmessage.JSONError); ok {
 			// If no error code is set, default to 1
 			// If no error code is set, default to 1

+ 1 - 1
api/client/create.go

@@ -57,7 +57,7 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error {
 	}
 	}
 	defer responseBody.Close()
 	defer responseBody.Close()
 
 
-	return jsonmessage.DisplayJSONMessagesStream(responseBody, out, cli.outFd, cli.isTerminalOut)
+	return jsonmessage.DisplayJSONMessagesStream(responseBody, out, cli.outFd, cli.isTerminalOut, nil)
 }
 }
 
 
 type cidFile struct {
 type cidFile struct {

+ 1 - 1
api/client/import.go

@@ -76,5 +76,5 @@ func (cli *DockerCli) CmdImport(args ...string) error {
 	}
 	}
 	defer responseBody.Close()
 	defer responseBody.Close()
 
 
-	return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut)
+	return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil)
 }
 }

+ 1 - 1
api/client/load.go

@@ -37,7 +37,7 @@ func (cli *DockerCli) CmdLoad(args ...string) error {
 	defer response.Body.Close()
 	defer response.Body.Close()
 
 
 	if response.JSON {
 	if response.JSON {
-		return jsonmessage.DisplayJSONMessagesStream(response.Body, cli.out, cli.outFd, cli.isTerminalOut)
+		return jsonmessage.DisplayJSONMessagesStream(response.Body, cli.out, cli.outFd, cli.isTerminalOut, nil)
 	}
 	}
 
 
 	_, err = io.Copy(cli.out, response.Body)
 	_, err = io.Copy(cli.out, response.Body)

+ 1 - 1
api/client/pull.go

@@ -83,5 +83,5 @@ func (cli *DockerCli) imagePullPrivileged(authConfig types.AuthConfig, imageID,
 	}
 	}
 	defer responseBody.Close()
 	defer responseBody.Close()
 
 
-	return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut)
+	return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil)
 }
 }

+ 11 - 10
api/client/push.go

@@ -49,13 +49,20 @@ func (cli *DockerCli) CmdPush(args ...string) error {
 		return cli.trustedPush(repoInfo, tag, authConfig, requestPrivilege)
 		return cli.trustedPush(repoInfo, tag, authConfig, requestPrivilege)
 	}
 	}
 
 
-	return cli.imagePushPrivileged(authConfig, ref.Name(), tag, cli.out, requestPrivilege)
+	responseBody, err := cli.imagePushPrivileged(authConfig, ref.Name(), tag, requestPrivilege)
+	if err != nil {
+		return err
+	}
+
+	defer responseBody.Close()
+
+	return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil)
 }
 }
 
 
-func (cli *DockerCli) imagePushPrivileged(authConfig types.AuthConfig, imageID, tag string, outputStream io.Writer, requestPrivilege client.RequestPrivilegeFunc) error {
+func (cli *DockerCli) imagePushPrivileged(authConfig types.AuthConfig, imageID, tag string, requestPrivilege client.RequestPrivilegeFunc) (io.ReadCloser, error) {
 	encodedAuth, err := encodeAuthToBase64(authConfig)
 	encodedAuth, err := encodeAuthToBase64(authConfig)
 	if err != nil {
 	if err != nil {
-		return err
+		return nil, err
 	}
 	}
 	options := types.ImagePushOptions{
 	options := types.ImagePushOptions{
 		ImageID:      imageID,
 		ImageID:      imageID,
@@ -63,11 +70,5 @@ func (cli *DockerCli) imagePushPrivileged(authConfig types.AuthConfig, imageID,
 		RegistryAuth: encodedAuth,
 		RegistryAuth: encodedAuth,
 	}
 	}
 
 
-	responseBody, err := cli.client.ImagePush(options, requestPrivilege)
-	if err != nil {
-		return err
-	}
-	defer responseBody.Close()
-
-	return jsonmessage.DisplayJSONMessagesStream(responseBody, outputStream, cli.outFd, cli.isTerminalOut)
+	return cli.client.ImagePush(options, requestPrivilege)
 }
 }

+ 22 - 56
api/client/trust.go

@@ -1,19 +1,16 @@
 package client
 package client
 
 
 import (
 import (
-	"bufio"
 	"encoding/hex"
 	"encoding/hex"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
-	"io"
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"os"
 	"os"
 	"path"
 	"path"
 	"path/filepath"
 	"path/filepath"
-	"regexp"
 	"sort"
 	"sort"
 	"strconv"
 	"strconv"
 	"time"
 	"time"
@@ -23,8 +20,8 @@ import (
 	"github.com/docker/distribution/registry/client/auth"
 	"github.com/docker/distribution/registry/client/auth"
 	"github.com/docker/distribution/registry/client/transport"
 	"github.com/docker/distribution/registry/client/transport"
 	"github.com/docker/docker/cliconfig"
 	"github.com/docker/docker/cliconfig"
-	"github.com/docker/docker/pkg/ansiescape"
-	"github.com/docker/docker/pkg/ioutils"
+	"github.com/docker/docker/distribution"
+	"github.com/docker/docker/pkg/jsonmessage"
 	flag "github.com/docker/docker/pkg/mflag"
 	flag "github.com/docker/docker/pkg/mflag"
 	"github.com/docker/docker/reference"
 	"github.com/docker/docker/reference"
 	"github.com/docker/docker/registry"
 	"github.com/docker/docker/registry"
@@ -64,8 +61,6 @@ func isTrusted() bool {
 	return !untrusted
 	return !untrusted
 }
 }
 
 
-var targetRegexp = regexp.MustCompile(`([\S]+): digest: ([\S]+) size: ([\d]+)`)
-
 type target struct {
 type target struct {
 	reference registry.Reference
 	reference registry.Reference
 	digest    digest.Digest
 	digest    digest.Digest
@@ -366,60 +361,31 @@ func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registr
 	return nil
 	return nil
 }
 }
 
 
-func targetStream(in io.Writer) (io.WriteCloser, <-chan []target) {
-	r, w := io.Pipe()
-	out := io.MultiWriter(in, w)
-	targetChan := make(chan []target)
-
-	go func() {
-		targets := []target{}
-		scanner := bufio.NewScanner(r)
-		scanner.Split(ansiescape.ScanANSILines)
-		for scanner.Scan() {
-			line := scanner.Bytes()
-			if matches := targetRegexp.FindSubmatch(line); len(matches) == 4 {
-				dgst, err := digest.ParseDigest(string(matches[2]))
-				if err != nil {
-					// Line does match what is expected, continue looking for valid lines
-					logrus.Debugf("Bad digest value %q in matched line, ignoring\n", string(matches[2]))
-					continue
-				}
-				s, err := strconv.ParseInt(string(matches[3]), 10, 64)
-				if err != nil {
-					// Line does match what is expected, continue looking for valid lines
-					logrus.Debugf("Bad size value %q in matched line, ignoring\n", string(matches[3]))
-					continue
-				}
-
-				targets = append(targets, target{
-					reference: registry.ParseReference(string(matches[1])),
-					digest:    dgst,
-					size:      s,
-				})
-			}
-		}
-		targetChan <- targets
-	}()
-
-	return ioutils.NewWriteCloserWrapper(out, w.Close), targetChan
-}
-
 func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error {
 func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error {
-	streamOut, targetChan := targetStream(cli.out)
-
-	reqError := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, streamOut, requestPrivilege)
-
-	// Close stream channel to finish target parsing
-	if err := streamOut.Close(); err != nil {
+	responseBody, err := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, requestPrivilege)
+	if err != nil {
 		return err
 		return err
 	}
 	}
-	// Check error from request
-	if reqError != nil {
-		return reqError
+
+	defer responseBody.Close()
+
+	targets := []target{}
+	handleTarget := func(aux *json.RawMessage) {
+		var pushResult distribution.PushResult
+		err := json.Unmarshal(*aux, &pushResult)
+		if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil {
+			targets = append(targets, target{
+				reference: registry.ParseReference(pushResult.Tag),
+				digest:    pushResult.Digest,
+				size:      int64(pushResult.Size),
+			})
+		}
 	}
 	}
 
 
-	// Get target results
-	targets := <-targetChan
+	err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, handleTarget)
+	if err != nil {
+		return err
+	}
 
 
 	if tag == "" {
 	if tag == "" {
 		fmt.Fprintf(cli.out, "No tag specified, skipping trust metadata push\n")
 		fmt.Fprintf(cli.out, "No tag specified, skipping trust metadata push\n")

+ 12 - 2
distribution/push_v2.go

@@ -26,6 +26,15 @@ import (
 	"golang.org/x/net/context"
 	"golang.org/x/net/context"
 )
 )
 
 
+// PushResult contains the tag, manifest digest, and manifest size from the
+// push. It's used to signal this information to the trust code in the client
+// so it can sign the manifest if necessary.
+type PushResult struct {
+	Tag    string
+	Digest digest.Digest
+	Size   int
+}
+
 type v2Pusher struct {
 type v2Pusher struct {
 	blobSumService *metadata.BlobSumService
 	blobSumService *metadata.BlobSumService
 	ref            reference.Named
 	ref            reference.Named
@@ -174,9 +183,10 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, association reference.Associat
 	}
 	}
 	if manifestDigest != "" {
 	if manifestDigest != "" {
 		if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
 		if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
-			// NOTE: do not change this format without first changing the trust client
-			// code. This information is used to determine what was pushed and should be signed.
 			progress.Messagef(p.config.ProgressOutput, "", "%s: digest: %s size: %d", tagged.Tag(), manifestDigest, manifestSize)
 			progress.Messagef(p.config.ProgressOutput, "", "%s: digest: %s size: %d", tagged.Tag(), manifestDigest, manifestSize)
+			// Signal digest to the trust client so it can sign the
+			// push, if appropriate.
+			progress.Aux(p.config.ProgressOutput, PushResult{Tag: tagged.Tag(), Digest: manifestDigest, Size: manifestSize})
 		}
 		}
 	}
 	}
 
 

+ 10 - 1
pkg/jsonmessage/jsonmessage.go

@@ -102,6 +102,8 @@ type JSONMessage struct {
 	TimeNano        int64         `json:"timeNano,omitempty"`
 	TimeNano        int64         `json:"timeNano,omitempty"`
 	Error           *JSONError    `json:"errorDetail,omitempty"`
 	Error           *JSONError    `json:"errorDetail,omitempty"`
 	ErrorMessage    string        `json:"error,omitempty"` //deprecated
 	ErrorMessage    string        `json:"error,omitempty"` //deprecated
+	// Aux contains out-of-band data, such as digests for push signing.
+	Aux *json.RawMessage `json:"aux,omitempty"`
 }
 }
 
 
 // Display displays the JSONMessage to `out`. `isTerminal` describes if `out`
 // Display displays the JSONMessage to `out`. `isTerminal` describes if `out`
@@ -148,7 +150,7 @@ func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
 // DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal`
 // DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal`
 // describes if `out` is a terminal. If this is the case, it will print `\n` at the end of
 // describes if `out` is a terminal. If this is the case, it will print `\n` at the end of
 // each line and move the cursor while displaying.
 // each line and move the cursor while displaying.
-func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool) error {
+func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error {
 	var (
 	var (
 		dec = json.NewDecoder(in)
 		dec = json.NewDecoder(in)
 		ids = make(map[string]int)
 		ids = make(map[string]int)
@@ -163,6 +165,13 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr,
 			return err
 			return err
 		}
 		}
 
 
+		if jm.Aux != nil {
+			if auxCallback != nil {
+				auxCallback(jm.Aux)
+			}
+			continue
+		}
+
 		if jm.Progress != nil {
 		if jm.Progress != nil {
 			jm.Progress.terminalFd = terminalFd
 			jm.Progress.terminalFd = terminalFd
 		}
 		}

+ 3 - 3
pkg/jsonmessage/jsonmessage_test.go

@@ -168,7 +168,7 @@ func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) {
 	reader := strings.NewReader("This is not a 'valid' JSON []")
 	reader := strings.NewReader("This is not a 'valid' JSON []")
 	inFd, _ = term.GetFdInfo(reader)
 	inFd, _ = term.GetFdInfo(reader)
 
 
-	if err := DisplayJSONMessagesStream(reader, data, inFd, false); err == nil && err.Error()[:17] != "invalid character" {
+	if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err == nil && err.Error()[:17] != "invalid character" {
 		t.Fatalf("Should have thrown an error (invalid character in ..), got [%v]", err)
 		t.Fatalf("Should have thrown an error (invalid character in ..), got [%v]", err)
 	}
 	}
 }
 }
@@ -210,7 +210,7 @@ func TestDisplayJSONMessagesStream(t *testing.T) {
 		inFd, _ = term.GetFdInfo(reader)
 		inFd, _ = term.GetFdInfo(reader)
 
 
 		// Without terminal
 		// Without terminal
-		if err := DisplayJSONMessagesStream(reader, data, inFd, false); err != nil {
+		if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
 		}
 		}
 		if data.String() != expectedMessages[0] {
 		if data.String() != expectedMessages[0] {
@@ -220,7 +220,7 @@ func TestDisplayJSONMessagesStream(t *testing.T) {
 		// With terminal
 		// With terminal
 		data = bytes.NewBuffer([]byte{})
 		data = bytes.NewBuffer([]byte{})
 		reader = strings.NewReader(jsonMessage)
 		reader = strings.NewReader(jsonMessage)
-		if err := DisplayJSONMessagesStream(reader, data, inFd, true); err != nil {
+		if err := DisplayJSONMessagesStream(reader, data, inFd, true, nil); err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
 		}
 		}
 		if data.String() != expectedMessages[1] {
 		if data.String() != expectedMessages[1] {

+ 10 - 0
pkg/progress/progress.go

@@ -16,6 +16,10 @@ type Progress struct {
 	Current int64
 	Current int64
 	Total   int64
 	Total   int64
 
 
+	// Aux contains extra information not presented to the user, such as
+	// digests for push signing.
+	Aux interface{}
+
 	LastUpdate bool
 	LastUpdate bool
 }
 }
 
 
@@ -61,3 +65,9 @@ func Message(out Output, id, message string) {
 func Messagef(out Output, id, format string, a ...interface{}) {
 func Messagef(out Output, id, format string, a ...interface{}) {
 	Message(out, id, fmt.Sprintf(format, a...))
 	Message(out, id, fmt.Sprintf(format, a...))
 }
 }
+
+// Aux sends auxiliary information over a progress interface, which will not be
+// formatted for the UI. This is used for things such as push signing.
+func Aux(out Output, a interface{}) {
+	out.WriteProgress(Progress{Aux: a})
+}

+ 12 - 2
pkg/streamformatter/streamformatter.go

@@ -70,16 +70,26 @@ func (sf *StreamFormatter) FormatError(err error) []byte {
 }
 }
 
 
 // FormatProgress formats the progress information for a specified action.
 // FormatProgress formats the progress information for a specified action.
-func (sf *StreamFormatter) FormatProgress(id, action string, progress *jsonmessage.JSONProgress) []byte {
+func (sf *StreamFormatter) FormatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte {
 	if progress == nil {
 	if progress == nil {
 		progress = &jsonmessage.JSONProgress{}
 		progress = &jsonmessage.JSONProgress{}
 	}
 	}
 	if sf.json {
 	if sf.json {
+		var auxJSON *json.RawMessage
+		if aux != nil {
+			auxJSONBytes, err := json.Marshal(aux)
+			if err != nil {
+				return nil
+			}
+			auxJSON = new(json.RawMessage)
+			*auxJSON = auxJSONBytes
+		}
 		b, err := json.Marshal(&jsonmessage.JSONMessage{
 		b, err := json.Marshal(&jsonmessage.JSONMessage{
 			Status:          action,
 			Status:          action,
 			ProgressMessage: progress.String(),
 			ProgressMessage: progress.String(),
 			Progress:        progress,
 			Progress:        progress,
 			ID:              id,
 			ID:              id,
+			Aux:             auxJSON,
 		})
 		})
 		if err != nil {
 		if err != nil {
 			return nil
 			return nil
@@ -116,7 +126,7 @@ func (out *progressOutput) WriteProgress(prog progress.Progress) error {
 		formatted = out.sf.FormatStatus(prog.ID, prog.Message)
 		formatted = out.sf.FormatStatus(prog.ID, prog.Message)
 	} else {
 	} else {
 		jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total}
 		jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total}
-		formatted = out.sf.FormatProgress(prog.ID, prog.Action, &jsonProgress)
+		formatted = out.sf.FormatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux)
 	}
 	}
 	_, err := out.out.Write(formatted)
 	_, err := out.out.Write(formatted)
 	if err != nil {
 	if err != nil {

+ 1 - 1
pkg/streamformatter/streamformatter_test.go

@@ -73,7 +73,7 @@ func TestJSONFormatProgress(t *testing.T) {
 		Total:   30,
 		Total:   30,
 		Start:   1,
 		Start:   1,
 	}
 	}
-	res := sf.FormatProgress("id", "action", progress)
+	res := sf.FormatProgress("id", "action", progress, nil)
 	msg := &jsonmessage.JSONMessage{}
 	msg := &jsonmessage.JSONMessage{}
 	if err := json.Unmarshal(res, msg); err != nil {
 	if err := json.Unmarshal(res, msg); err != nil {
 		t.Fatal(err)
 		t.Fatal(err)