1
0
Fork 0
mirror of https://github.com/moby/moby.git synced 2022-11-09 12:21:53 -05:00

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>
This commit is contained in:
Aaron Lehmann 2015-12-21 15:02:44 -08:00
parent 1c979f7587
commit 65370be888
13 changed files with 88 additions and 82 deletions

View file

@ -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

View file

@ -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 {

View file

@ -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)
} }

View file

@ -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)

View file

@ -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)
} }

View file

@ -49,25 +49,26 @@ 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)
}
func (cli *DockerCli) imagePushPrivileged(authConfig types.AuthConfig, imageID, tag string, outputStream io.Writer, requestPrivilege client.RequestPrivilegeFunc) error {
encodedAuth, err := encodeAuthToBase64(authConfig)
if err != nil { if err != nil {
return err 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, requestPrivilege client.RequestPrivilegeFunc) (io.ReadCloser, error) {
encodedAuth, err := encodeAuthToBase64(authConfig)
if err != nil {
return nil, err
}
options := types.ImagePushOptions{ options := types.ImagePushOptions{
ImageID: imageID, ImageID: imageID,
Tag: tag, Tag: tag,
RegistryAuth: encodedAuth, RegistryAuth: encodedAuth,
} }
responseBody, err := cli.client.ImagePush(options, requestPrivilege) return cli.client.ImagePush(options, requestPrivilege)
if err != nil {
return err
}
defer responseBody.Close()
return jsonmessage.DisplayJSONMessagesStream(responseBody, outputStream, cli.outFd, cli.isTerminalOut)
} }

View file

@ -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/distribution"
"github.com/docker/docker/pkg/ioutils" "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) { func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error {
r, w := io.Pipe() responseBody, err := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, requestPrivilege)
out := io.MultiWriter(in, w) if err != nil {
targetChan := make(chan []target) return err
}
defer responseBody.Close()
go func() {
targets := []target{} targets := []target{}
scanner := bufio.NewScanner(r) handleTarget := func(aux *json.RawMessage) {
scanner.Split(ansiescape.ScanANSILines) var pushResult distribution.PushResult
for scanner.Scan() { err := json.Unmarshal(*aux, &pushResult)
line := scanner.Bytes() if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil {
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{ targets = append(targets, target{
reference: registry.ParseReference(string(matches[1])), reference: registry.ParseReference(pushResult.Tag),
digest: dgst, digest: pushResult.Digest,
size: s, size: int64(pushResult.Size),
}) })
} }
} }
targetChan <- targets
}()
return ioutils.NewWriteCloserWrapper(out, w.Close), targetChan err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, handleTarget)
} if err != nil {
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 {
return err return err
} }
// Check error from request
if reqError != nil {
return reqError
}
// Get target results
targets := <-targetChan
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")

View file

@ -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})
} }
} }

View file

@ -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
} }

View file

@ -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] {

View file

@ -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})
}

View file

@ -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 {

View file

@ -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)