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>
| ... | ... |
@@ -255,7 +255,7 @@ func (cli *DockerCli) CmdBuild(args ...string) error {
|
| 255 | 255 |
return err |
| 256 | 256 |
} |
| 257 | 257 |
|
| 258 |
- err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut) |
|
| 258 |
+ err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, cli.outFd, cli.isTerminalOut, nil) |
|
| 259 | 259 |
if err != nil {
|
| 260 | 260 |
if jerr, ok := err.(*jsonmessage.JSONError); ok {
|
| 261 | 261 |
// If no error code is set, default to 1 |
| ... | ... |
@@ -57,7 +57,7 @@ func (cli *DockerCli) pullImageCustomOut(image string, out io.Writer) error {
|
| 57 | 57 |
} |
| 58 | 58 |
defer responseBody.Close() |
| 59 | 59 |
|
| 60 |
- return jsonmessage.DisplayJSONMessagesStream(responseBody, out, cli.outFd, cli.isTerminalOut) |
|
| 60 |
+ return jsonmessage.DisplayJSONMessagesStream(responseBody, out, cli.outFd, cli.isTerminalOut, nil) |
|
| 61 | 61 |
} |
| 62 | 62 |
|
| 63 | 63 |
type cidFile struct {
|
| ... | ... |
@@ -76,5 +76,5 @@ func (cli *DockerCli) CmdImport(args ...string) error {
|
| 76 | 76 |
} |
| 77 | 77 |
defer responseBody.Close() |
| 78 | 78 |
|
| 79 |
- return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) |
|
| 79 |
+ return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil) |
|
| 80 | 80 |
} |
| ... | ... |
@@ -37,7 +37,7 @@ func (cli *DockerCli) CmdLoad(args ...string) error {
|
| 37 | 37 |
defer response.Body.Close() |
| 38 | 38 |
|
| 39 | 39 |
if response.JSON {
|
| 40 |
- return jsonmessage.DisplayJSONMessagesStream(response.Body, cli.out, cli.outFd, cli.isTerminalOut) |
|
| 40 |
+ return jsonmessage.DisplayJSONMessagesStream(response.Body, cli.out, cli.outFd, cli.isTerminalOut, nil) |
|
| 41 | 41 |
} |
| 42 | 42 |
|
| 43 | 43 |
_, err = io.Copy(cli.out, response.Body) |
| ... | ... |
@@ -83,5 +83,5 @@ func (cli *DockerCli) imagePullPrivileged(authConfig types.AuthConfig, imageID, |
| 83 | 83 |
} |
| 84 | 84 |
defer responseBody.Close() |
| 85 | 85 |
|
| 86 |
- return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut) |
|
| 86 |
+ return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil) |
|
| 87 | 87 |
} |
| ... | ... |
@@ -49,13 +49,20 @@ func (cli *DockerCli) CmdPush(args ...string) error {
|
| 49 | 49 |
return cli.trustedPush(repoInfo, tag, authConfig, requestPrivilege) |
| 50 | 50 |
} |
| 51 | 51 |
|
| 52 |
- return cli.imagePushPrivileged(authConfig, ref.Name(), tag, cli.out, requestPrivilege) |
|
| 52 |
+ responseBody, err := cli.imagePushPrivileged(authConfig, ref.Name(), tag, requestPrivilege) |
|
| 53 |
+ if err != nil {
|
|
| 54 |
+ return err |
|
| 55 |
+ } |
|
| 56 |
+ |
|
| 57 |
+ defer responseBody.Close() |
|
| 58 |
+ |
|
| 59 |
+ return jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, nil) |
|
| 53 | 60 |
} |
| 54 | 61 |
|
| 55 |
-func (cli *DockerCli) imagePushPrivileged(authConfig types.AuthConfig, imageID, tag string, outputStream io.Writer, requestPrivilege client.RequestPrivilegeFunc) error {
|
|
| 62 |
+func (cli *DockerCli) imagePushPrivileged(authConfig types.AuthConfig, imageID, tag string, requestPrivilege client.RequestPrivilegeFunc) (io.ReadCloser, error) {
|
|
| 56 | 63 |
encodedAuth, err := encodeAuthToBase64(authConfig) |
| 57 | 64 |
if err != nil {
|
| 58 |
- return err |
|
| 65 |
+ return nil, err |
|
| 59 | 66 |
} |
| 60 | 67 |
options := types.ImagePushOptions{
|
| 61 | 68 |
ImageID: imageID, |
| ... | ... |
@@ -63,11 +70,5 @@ func (cli *DockerCli) imagePushPrivileged(authConfig types.AuthConfig, imageID, |
| 63 | 63 |
RegistryAuth: encodedAuth, |
| 64 | 64 |
} |
| 65 | 65 |
|
| 66 |
- responseBody, err := cli.client.ImagePush(options, requestPrivilege) |
|
| 67 |
- if err != nil {
|
|
| 68 |
- return err |
|
| 69 |
- } |
|
| 70 |
- defer responseBody.Close() |
|
| 71 |
- |
|
| 72 |
- return jsonmessage.DisplayJSONMessagesStream(responseBody, outputStream, cli.outFd, cli.isTerminalOut) |
|
| 66 |
+ return cli.client.ImagePush(options, requestPrivilege) |
|
| 73 | 67 |
} |
| ... | ... |
@@ -1,19 +1,16 @@ |
| 1 | 1 |
package client |
| 2 | 2 |
|
| 3 | 3 |
import ( |
| 4 |
- "bufio" |
|
| 5 | 4 |
"encoding/hex" |
| 6 | 5 |
"encoding/json" |
| 7 | 6 |
"errors" |
| 8 | 7 |
"fmt" |
| 9 |
- "io" |
|
| 10 | 8 |
"net" |
| 11 | 9 |
"net/http" |
| 12 | 10 |
"net/url" |
| 13 | 11 |
"os" |
| 14 | 12 |
"path" |
| 15 | 13 |
"path/filepath" |
| 16 |
- "regexp" |
|
| 17 | 14 |
"sort" |
| 18 | 15 |
"strconv" |
| 19 | 16 |
"time" |
| ... | ... |
@@ -23,8 +20,8 @@ import ( |
| 23 | 23 |
"github.com/docker/distribution/registry/client/auth" |
| 24 | 24 |
"github.com/docker/distribution/registry/client/transport" |
| 25 | 25 |
"github.com/docker/docker/cliconfig" |
| 26 |
- "github.com/docker/docker/pkg/ansiescape" |
|
| 27 |
- "github.com/docker/docker/pkg/ioutils" |
|
| 26 |
+ "github.com/docker/docker/distribution" |
|
| 27 |
+ "github.com/docker/docker/pkg/jsonmessage" |
|
| 28 | 28 |
flag "github.com/docker/docker/pkg/mflag" |
| 29 | 29 |
"github.com/docker/docker/reference" |
| 30 | 30 |
"github.com/docker/docker/registry" |
| ... | ... |
@@ -64,8 +61,6 @@ func isTrusted() bool {
|
| 64 | 64 |
return !untrusted |
| 65 | 65 |
} |
| 66 | 66 |
|
| 67 |
-var targetRegexp = regexp.MustCompile(`([\S]+): digest: ([\S]+) size: ([\d]+)`) |
|
| 68 |
- |
|
| 69 | 67 |
type target struct {
|
| 70 | 68 |
reference registry.Reference |
| 71 | 69 |
digest digest.Digest |
| ... | ... |
@@ -366,60 +361,31 @@ func (cli *DockerCli) trustedPull(repoInfo *registry.RepositoryInfo, ref registr |
| 366 | 366 |
return nil |
| 367 | 367 |
} |
| 368 | 368 |
|
| 369 |
-func targetStream(in io.Writer) (io.WriteCloser, <-chan []target) {
|
|
| 370 |
- r, w := io.Pipe() |
|
| 371 |
- out := io.MultiWriter(in, w) |
|
| 372 |
- targetChan := make(chan []target) |
|
| 373 |
- |
|
| 374 |
- go func() {
|
|
| 375 |
- targets := []target{}
|
|
| 376 |
- scanner := bufio.NewScanner(r) |
|
| 377 |
- scanner.Split(ansiescape.ScanANSILines) |
|
| 378 |
- for scanner.Scan() {
|
|
| 379 |
- line := scanner.Bytes() |
|
| 380 |
- if matches := targetRegexp.FindSubmatch(line); len(matches) == 4 {
|
|
| 381 |
- dgst, err := digest.ParseDigest(string(matches[2])) |
|
| 382 |
- if err != nil {
|
|
| 383 |
- // Line does match what is expected, continue looking for valid lines |
|
| 384 |
- logrus.Debugf("Bad digest value %q in matched line, ignoring\n", string(matches[2]))
|
|
| 385 |
- continue |
|
| 386 |
- } |
|
| 387 |
- s, err := strconv.ParseInt(string(matches[3]), 10, 64) |
|
| 388 |
- if err != nil {
|
|
| 389 |
- // Line does match what is expected, continue looking for valid lines |
|
| 390 |
- logrus.Debugf("Bad size value %q in matched line, ignoring\n", string(matches[3]))
|
|
| 391 |
- continue |
|
| 392 |
- } |
|
| 393 |
- |
|
| 394 |
- targets = append(targets, target{
|
|
| 395 |
- reference: registry.ParseReference(string(matches[1])), |
|
| 396 |
- digest: dgst, |
|
| 397 |
- size: s, |
|
| 398 |
- }) |
|
| 399 |
- } |
|
| 400 |
- } |
|
| 401 |
- targetChan <- targets |
|
| 402 |
- }() |
|
| 403 |
- |
|
| 404 |
- return ioutils.NewWriteCloserWrapper(out, w.Close), targetChan |
|
| 405 |
-} |
|
| 406 |
- |
|
| 407 | 369 |
func (cli *DockerCli) trustedPush(repoInfo *registry.RepositoryInfo, tag string, authConfig types.AuthConfig, requestPrivilege apiclient.RequestPrivilegeFunc) error {
|
| 408 |
- streamOut, targetChan := targetStream(cli.out) |
|
| 409 |
- |
|
| 410 |
- reqError := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, streamOut, requestPrivilege) |
|
| 411 |
- |
|
| 412 |
- // Close stream channel to finish target parsing |
|
| 413 |
- if err := streamOut.Close(); err != nil {
|
|
| 370 |
+ responseBody, err := cli.imagePushPrivileged(authConfig, repoInfo.Name(), tag, requestPrivilege) |
|
| 371 |
+ if err != nil {
|
|
| 414 | 372 |
return err |
| 415 | 373 |
} |
| 416 |
- // Check error from request |
|
| 417 |
- if reqError != nil {
|
|
| 418 |
- return reqError |
|
| 374 |
+ |
|
| 375 |
+ defer responseBody.Close() |
|
| 376 |
+ |
|
| 377 |
+ targets := []target{}
|
|
| 378 |
+ handleTarget := func(aux *json.RawMessage) {
|
|
| 379 |
+ var pushResult distribution.PushResult |
|
| 380 |
+ err := json.Unmarshal(*aux, &pushResult) |
|
| 381 |
+ if err == nil && pushResult.Tag != "" && pushResult.Digest.Validate() == nil {
|
|
| 382 |
+ targets = append(targets, target{
|
|
| 383 |
+ reference: registry.ParseReference(pushResult.Tag), |
|
| 384 |
+ digest: pushResult.Digest, |
|
| 385 |
+ size: int64(pushResult.Size), |
|
| 386 |
+ }) |
|
| 387 |
+ } |
|
| 419 | 388 |
} |
| 420 | 389 |
|
| 421 |
- // Get target results |
|
| 422 |
- targets := <-targetChan |
|
| 390 |
+ err = jsonmessage.DisplayJSONMessagesStream(responseBody, cli.out, cli.outFd, cli.isTerminalOut, handleTarget) |
|
| 391 |
+ if err != nil {
|
|
| 392 |
+ return err |
|
| 393 |
+ } |
|
| 423 | 394 |
|
| 424 | 395 |
if tag == "" {
|
| 425 | 396 |
fmt.Fprintf(cli.out, "No tag specified, skipping trust metadata push\n") |
| ... | ... |
@@ -26,6 +26,15 @@ import ( |
| 26 | 26 |
"golang.org/x/net/context" |
| 27 | 27 |
) |
| 28 | 28 |
|
| 29 |
+// PushResult contains the tag, manifest digest, and manifest size from the |
|
| 30 |
+// push. It's used to signal this information to the trust code in the client |
|
| 31 |
+// so it can sign the manifest if necessary. |
|
| 32 |
+type PushResult struct {
|
|
| 33 |
+ Tag string |
|
| 34 |
+ Digest digest.Digest |
|
| 35 |
+ Size int |
|
| 36 |
+} |
|
| 37 |
+ |
|
| 29 | 38 |
type v2Pusher struct {
|
| 30 | 39 |
blobSumService *metadata.BlobSumService |
| 31 | 40 |
ref reference.Named |
| ... | ... |
@@ -174,9 +183,10 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, association reference.Associat |
| 174 | 174 |
} |
| 175 | 175 |
if manifestDigest != "" {
|
| 176 | 176 |
if tagged, isTagged := ref.(reference.NamedTagged); isTagged {
|
| 177 |
- // NOTE: do not change this format without first changing the trust client |
|
| 178 |
- // code. This information is used to determine what was pushed and should be signed. |
|
| 179 | 177 |
progress.Messagef(p.config.ProgressOutput, "", "%s: digest: %s size: %d", tagged.Tag(), manifestDigest, manifestSize) |
| 178 |
+ // Signal digest to the trust client so it can sign the |
|
| 179 |
+ // push, if appropriate. |
|
| 180 |
+ progress.Aux(p.config.ProgressOutput, PushResult{Tag: tagged.Tag(), Digest: manifestDigest, Size: manifestSize})
|
|
| 180 | 181 |
} |
| 181 | 182 |
} |
| 182 | 183 |
|
| ... | ... |
@@ -102,6 +102,8 @@ type JSONMessage struct {
|
| 102 | 102 |
TimeNano int64 `json:"timeNano,omitempty"` |
| 103 | 103 |
Error *JSONError `json:"errorDetail,omitempty"` |
| 104 | 104 |
ErrorMessage string `json:"error,omitempty"` //deprecated |
| 105 |
+ // Aux contains out-of-band data, such as digests for push signing. |
|
| 106 |
+ Aux *json.RawMessage `json:"aux,omitempty"` |
|
| 105 | 107 |
} |
| 106 | 108 |
|
| 107 | 109 |
// Display displays the JSONMessage to `out`. `isTerminal` describes if `out` |
| ... | ... |
@@ -148,7 +150,7 @@ func (jm *JSONMessage) Display(out io.Writer, isTerminal bool) error {
|
| 148 | 148 |
// DisplayJSONMessagesStream displays a json message stream from `in` to `out`, `isTerminal` |
| 149 | 149 |
// describes if `out` is a terminal. If this is the case, it will print `\n` at the end of |
| 150 | 150 |
// each line and move the cursor while displaying. |
| 151 |
-func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool) error {
|
|
| 151 |
+func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, isTerminal bool, auxCallback func(*json.RawMessage)) error {
|
|
| 152 | 152 |
var ( |
| 153 | 153 |
dec = json.NewDecoder(in) |
| 154 | 154 |
ids = make(map[string]int) |
| ... | ... |
@@ -163,6 +165,13 @@ func DisplayJSONMessagesStream(in io.Reader, out io.Writer, terminalFd uintptr, |
| 163 | 163 |
return err |
| 164 | 164 |
} |
| 165 | 165 |
|
| 166 |
+ if jm.Aux != nil {
|
|
| 167 |
+ if auxCallback != nil {
|
|
| 168 |
+ auxCallback(jm.Aux) |
|
| 169 |
+ } |
|
| 170 |
+ continue |
|
| 171 |
+ } |
|
| 172 |
+ |
|
| 166 | 173 |
if jm.Progress != nil {
|
| 167 | 174 |
jm.Progress.terminalFd = terminalFd |
| 168 | 175 |
} |
| ... | ... |
@@ -168,7 +168,7 @@ func TestDisplayJSONMessagesStreamInvalidJSON(t *testing.T) {
|
| 168 | 168 |
reader := strings.NewReader("This is not a 'valid' JSON []")
|
| 169 | 169 |
inFd, _ = term.GetFdInfo(reader) |
| 170 | 170 |
|
| 171 |
- if err := DisplayJSONMessagesStream(reader, data, inFd, false); err == nil && err.Error()[:17] != "invalid character" {
|
|
| 171 |
+ if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err == nil && err.Error()[:17] != "invalid character" {
|
|
| 172 | 172 |
t.Fatalf("Should have thrown an error (invalid character in ..), got [%v]", err)
|
| 173 | 173 |
} |
| 174 | 174 |
} |
| ... | ... |
@@ -210,7 +210,7 @@ func TestDisplayJSONMessagesStream(t *testing.T) {
|
| 210 | 210 |
inFd, _ = term.GetFdInfo(reader) |
| 211 | 211 |
|
| 212 | 212 |
// Without terminal |
| 213 |
- if err := DisplayJSONMessagesStream(reader, data, inFd, false); err != nil {
|
|
| 213 |
+ if err := DisplayJSONMessagesStream(reader, data, inFd, false, nil); err != nil {
|
|
| 214 | 214 |
t.Fatal(err) |
| 215 | 215 |
} |
| 216 | 216 |
if data.String() != expectedMessages[0] {
|
| ... | ... |
@@ -220,7 +220,7 @@ func TestDisplayJSONMessagesStream(t *testing.T) {
|
| 220 | 220 |
// With terminal |
| 221 | 221 |
data = bytes.NewBuffer([]byte{})
|
| 222 | 222 |
reader = strings.NewReader(jsonMessage) |
| 223 |
- if err := DisplayJSONMessagesStream(reader, data, inFd, true); err != nil {
|
|
| 223 |
+ if err := DisplayJSONMessagesStream(reader, data, inFd, true, nil); err != nil {
|
|
| 224 | 224 |
t.Fatal(err) |
| 225 | 225 |
} |
| 226 | 226 |
if data.String() != expectedMessages[1] {
|
| ... | ... |
@@ -16,6 +16,10 @@ type Progress struct {
|
| 16 | 16 |
Current int64 |
| 17 | 17 |
Total int64 |
| 18 | 18 |
|
| 19 |
+ // Aux contains extra information not presented to the user, such as |
|
| 20 |
+ // digests for push signing. |
|
| 21 |
+ Aux interface{}
|
|
| 22 |
+ |
|
| 19 | 23 |
LastUpdate bool |
| 20 | 24 |
} |
| 21 | 25 |
|
| ... | ... |
@@ -61,3 +65,9 @@ func Message(out Output, id, message string) {
|
| 61 | 61 |
func Messagef(out Output, id, format string, a ...interface{}) {
|
| 62 | 62 |
Message(out, id, fmt.Sprintf(format, a...)) |
| 63 | 63 |
} |
| 64 |
+ |
|
| 65 |
+// Aux sends auxiliary information over a progress interface, which will not be |
|
| 66 |
+// formatted for the UI. This is used for things such as push signing. |
|
| 67 |
+func Aux(out Output, a interface{}) {
|
|
| 68 |
+ out.WriteProgress(Progress{Aux: a})
|
|
| 69 |
+} |
| ... | ... |
@@ -70,16 +70,26 @@ func (sf *StreamFormatter) FormatError(err error) []byte {
|
| 70 | 70 |
} |
| 71 | 71 |
|
| 72 | 72 |
// FormatProgress formats the progress information for a specified action. |
| 73 |
-func (sf *StreamFormatter) FormatProgress(id, action string, progress *jsonmessage.JSONProgress) []byte {
|
|
| 73 |
+func (sf *StreamFormatter) FormatProgress(id, action string, progress *jsonmessage.JSONProgress, aux interface{}) []byte {
|
|
| 74 | 74 |
if progress == nil {
|
| 75 | 75 |
progress = &jsonmessage.JSONProgress{}
|
| 76 | 76 |
} |
| 77 | 77 |
if sf.json {
|
| 78 |
+ var auxJSON *json.RawMessage |
|
| 79 |
+ if aux != nil {
|
|
| 80 |
+ auxJSONBytes, err := json.Marshal(aux) |
|
| 81 |
+ if err != nil {
|
|
| 82 |
+ return nil |
|
| 83 |
+ } |
|
| 84 |
+ auxJSON = new(json.RawMessage) |
|
| 85 |
+ *auxJSON = auxJSONBytes |
|
| 86 |
+ } |
|
| 78 | 87 |
b, err := json.Marshal(&jsonmessage.JSONMessage{
|
| 79 | 88 |
Status: action, |
| 80 | 89 |
ProgressMessage: progress.String(), |
| 81 | 90 |
Progress: progress, |
| 82 | 91 |
ID: id, |
| 92 |
+ Aux: auxJSON, |
|
| 83 | 93 |
}) |
| 84 | 94 |
if err != nil {
|
| 85 | 95 |
return nil |
| ... | ... |
@@ -116,7 +126,7 @@ func (out *progressOutput) WriteProgress(prog progress.Progress) error {
|
| 116 | 116 |
formatted = out.sf.FormatStatus(prog.ID, prog.Message) |
| 117 | 117 |
} else {
|
| 118 | 118 |
jsonProgress := jsonmessage.JSONProgress{Current: prog.Current, Total: prog.Total}
|
| 119 |
- formatted = out.sf.FormatProgress(prog.ID, prog.Action, &jsonProgress) |
|
| 119 |
+ formatted = out.sf.FormatProgress(prog.ID, prog.Action, &jsonProgress, prog.Aux) |
|
| 120 | 120 |
} |
| 121 | 121 |
_, err := out.out.Write(formatted) |
| 122 | 122 |
if err != nil {
|
| ... | ... |
@@ -73,7 +73,7 @@ func TestJSONFormatProgress(t *testing.T) {
|
| 73 | 73 |
Total: 30, |
| 74 | 74 |
Start: 1, |
| 75 | 75 |
} |
| 76 |
- res := sf.FormatProgress("id", "action", progress)
|
|
| 76 |
+ res := sf.FormatProgress("id", "action", progress, nil)
|
|
| 77 | 77 |
msg := &jsonmessage.JSONMessage{}
|
| 78 | 78 |
if err := json.Unmarshal(res, msg); err != nil {
|
| 79 | 79 |
t.Fatal(err) |