diff --git a/src/cmd/linuxkit/pkglib/docker.go b/src/cmd/linuxkit/pkglib/docker.go index 045359623..7d9d981eb 100644 --- a/src/cmd/linuxkit/pkglib/docker.go +++ b/src/cmd/linuxkit/pkglib/docker.go @@ -5,16 +5,32 @@ package pkglib //go:generate ./gen import ( + "bytes" + "encoding/base64" "fmt" "io" "os" "os/exec" + "path" + "strings" + "github.com/docker/cli/cli/config" log "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" ) -const dctEnableEnv = "DOCKER_CONTENT_TRUST=1" +const ( + dctEnableEnv = "DOCKER_CONTENT_TRUST=1" + registry = "https://index.docker.io/v1/" + notaryServer = "https://notary.docker.io" + notaryDelegationPassphraseEnvVar = "NOTARY_DELEGATION_PASSPHRASE" + notaryAuthEnvVar = "NOTARY_AUTH" + dctEnvVar = "DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE" +) + +var platforms = []string{ + "linux/amd64", "linux/arm64", "linux/s390x", +} type dockerRunner struct { dct bool @@ -133,18 +149,13 @@ func (dr dockerRunner) pushWithManifest(img, suffix string) error { return err } - var dctArg string + var trust bool if dr.dct { - dctArg = "1" + trust = true } fmt.Printf("Pushing %s to manifest %s\n", img+suffix, img) - cmd := exec.Command("/bin/sh", "-c", manifestPushScript, "manifest-push-script", img, dctArg) - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - log.Debugf("Executing: %v", cmd.Args) - - return cmd.Run() + return manifestPush(img, trust) } func (dr dockerRunner) tag(ref, tag string) error { @@ -166,3 +177,99 @@ func (dr dockerRunner) save(tgt string, refs ...string) error { args := append([]string{"image", "save", "-o", tgt}, refs...) return dr.command(args...) } + +func manifestPush(img string, trust bool) error { + imgParts := strings.Split(img, ":") + if len(imgParts) < 2 { + return fmt.Errorf("image not composed of : '%s'", img) + } + repo := imgParts[0] + tag := imgParts[1] + + cfgFile := config.LoadDefaultConfigFile(os.Stderr) + auth, err := cfgFile.GetAuthConfig(registry) + if err != nil { + return fmt.Errorf("unable to get auth for %s: %v", registry, err) + } + + args := []string{ + "push", + "from-args", + "--ignore-missing", + "--platforms", + strings.Join(platforms, ","), + "--template", + fmt.Sprintf("%s-ARCH", img), + "--target", + img, + } + manTool := "manifest-tool" + // we do this separately to avoid printing username and password to debug output + log.Debugf("Executing (will add username/password): %v", append([]string{manTool}, args...)) + args = append([]string{ + "--username", + auth.Username, + "--password", + auth.Password, + }, args...) + cmd := exec.Command(manTool, args...) + + var stdout bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to execute manifest-tool: %v", err) + } + + if !trust { + fmt.Printf("trust disabled, not signing %s\n", img) + return nil + } + + // get the image hash and the length from the manifest tool output + manToolOut := string(stdout.Bytes()) + manToolOutParts := strings.Fields(manToolOut) + if len(manToolOutParts) < 3 { + return fmt.Errorf("manifest-tool output was less then required 3 parts '%s'", manToolOut) + } + hashParts := strings.Split(manToolOutParts[1], ":") + if len(hashParts) < 2 { + return fmt.Errorf("manifest-tool output hash was not in format : '%s'", manToolOutParts[1]) + } + hash := hashParts[1] + length := manToolOutParts[2] + + notaryAuth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", auth.Username, auth.Password))) + // run the notary command to sign + args = []string{ + "-s", + notaryServer, + "-d", + path.Join(os.Getenv("HOME"), ".docker/trust"), + "addhash", + "-p", + fmt.Sprintf("docker.io/%s", repo), + tag, + length, + "--sha256", + hash, + "-r", + "targets/releases", + } + cmd = exec.Command("notary", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", notaryDelegationPassphraseEnvVar, os.Getenv(dctEnvVar)), fmt.Sprintf("%s=%s", notaryAuthEnvVar, notaryAuth)) + log.Debugf("Executing: %v", cmd.Args) + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to execute notary-tool: %v", err) + } + + // report output + fmt.Printf("New signed multi-arch image: %s:%s\n", repo, tag) + + return nil +} diff --git a/src/cmd/linuxkit/pkglib/gen b/src/cmd/linuxkit/pkglib/gen deleted file mode 100755 index c9ecc5a5b..000000000 --- a/src/cmd/linuxkit/pkglib/gen +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -set -e -( - echo package pkglib - echo - echo const manifestPushScript = \` - # TODO(ijc) once everything is ported this script can move to this source directory - cat ../../../../scripts/push-manifest.sh - echo \` -) > manifest_push_script.go.new -mv manifest_push_script.go.new manifest_push_script.go diff --git a/src/cmd/linuxkit/pkglib/manifest_push_script.go b/src/cmd/linuxkit/pkglib/manifest_push_script.go deleted file mode 100644 index 7befcfeea..000000000 --- a/src/cmd/linuxkit/pkglib/manifest_push_script.go +++ /dev/null @@ -1,89 +0,0 @@ -package pkglib - -const manifestPushScript = ` -#! /bin/sh - -set -e - -# This script pushes a multiarch manifest for packages and signs it. -# -# The TARGET must be of the form /: and this is what -# the manifest is pushed to. It assumes that there is are images of -# the form /:- already on hub. -# -# If TRUST is not set, the manifest will not be signed. -# -# For signing, DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE must be set. - -# This should all be replaced with 'docker manifest' once it lands. - -TARGET=$1 -TRUST=$2 - -REPO=$(echo "$TARGET" | cut -d':' -f1) -TAG=$(echo "$TARGET" | cut -d':' -f2) - -# Work out credentials. On macOS they are needed for manifest-tool and -# we need them for notary on all platforms. -case $(uname -s) in - Darwin) - # Prior to 2018-03-27 D4M used a .bin suffix on the keychain utility binary name. Support the old name for a while - if [ -f /Applications/Docker.app/Contents/Resources/bin/docker-credential-osxkeychain.bin ]; then - CREDHELPER="/Applications/Docker.app/Contents/Resources/bin/docker-credential-osxkeychain.bin" - else - CREDHELPER="/Applications/Docker.app/Contents/Resources/bin/docker-credential-osxkeychain" - fi - ;; - Linux) - CREDSTORE=$(cat ~/.docker/config.json | jq -r '.credsStore // empty') - if [ -n "$CREDSTORE" ] ; then - CREDHELPER="docker-credential-$CREDSTORE" - else - CRED=$(cat ~/.docker/config.json | jq -r '.auths."https://index.docker.io/v1/".auth' | base64 -d -) - USER=$(echo $CRED | cut -d ':' -f 1) - PASS=$(echo $CRED | cut -d ':' -f 2-) - # manifest-tool can use docker credentials directly - MT_ARGS= - fi - ;; - *) - echo "Unsupported platform" - exit 1 - ;; -esac -if [ -n "$CREDHELPER" ] ; then - CRED=$(echo "https://index.docker.io/v1/" | "$CREDHELPER" get) - USER=$(echo "$CRED" | jq -r '.Username') - PASS=$(echo "$CRED" | jq -r '.Secret') - MT_ARGS="--username $USER --password $PASS" -fi - -# Push manifest list -OUT=$(manifest-tool $MT_ARGS push from-args \ - --ignore-missing \ - --platforms linux/amd64,linux/arm64,linux/s390x \ - --template "$TARGET"-ARCH \ - --target "$TARGET") - -echo "$OUT" -if [ -z "$TRUST" ]; then - echo "Not signing $TARGET" - exit 0 -fi - -# Extract sha256 and length from the manifest-tool output -SHA256=$(echo "$OUT" | cut -d' ' -f2 | cut -d':' -f2) -LEN=$(echo "$OUT" | cut -d' ' -f3) - -# notary 0.6.0 accepts authentication as base64-encoded "username:password" -export NOTARY_AUTH=$(echo "$USER:$PASS" | base64) -export NOTARY_DELEGATION_PASSPHRASE="$DOCKER_CONTENT_TRUST_REPOSITORY_PASSPHRASE" - -notary -s https://notary.docker.io -d $HOME/.docker/trust addhash \ - -p docker.io/$REPO $TAG $LEN --sha256 $SHA256 \ - -r targets/releases - -echo -echo "New signed multi-arch image: $REPO:$TAG" -echo -`