From 1ab10377076c7cd77d4ee210dc101f04486e1fbe Mon Sep 17 00:00:00 2001 From: Tycho Andersen Date: Thu, 10 Aug 2017 10:14:20 -0600 Subject: [PATCH 1/2] add a `pass` credential helper backend Signed-off-by: Tycho Andersen --- .travis.yml | 1 + Makefile | 6 +- ci/before_script_linux.sh | 18 ++++ pass/cmd/main_linux.go | 10 ++ pass/pass_linux.go | 208 ++++++++++++++++++++++++++++++++++++++ pass/pass_linux_test.go | 71 +++++++++++++ 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 pass/cmd/main_linux.go create mode 100644 pass/pass_linux.go create mode 100644 pass/pass_linux_test.go diff --git a/.travis.yml b/.travis.yml index 6011a4d..bf1ac60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,7 @@ apt: packages: - libsecret-1-dev + - pass before_script: - "export DISPLAY=:99.0" - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh ci/before_script_linux.sh; fi diff --git a/Makefile b/Makefile index df5b72f..1f3549e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all deps osxkeychain secretservice test validate wincred +.PHONY: all deps osxkeychain secretservice test validate wincred pass TRAVIS_OS_NAME ?= linux VERSION := $(shell grep 'const Version' credentials/version.go | awk -F'"' '{ print $$2 }') @@ -30,6 +30,10 @@ secretservice: mkdir bin go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go +pass: + mkdir -p bin + go build -o bin/docker-credential-pass pass/cmd/main_linux.go + wincred: mkdir bin go build -o bin/docker-credential-wincred.exe wincred/cmd/main_windows.go diff --git a/ci/before_script_linux.sh b/ci/before_script_linux.sh index eb3ac19..9fc0ea2 100644 --- a/ci/before_script_linux.sh +++ b/ci/before_script_linux.sh @@ -2,3 +2,21 @@ set -ex sh -e /etc/init.d/xvfb start sleep 3 # give xvfb some time to start + +# init key for pass +gpg --batch --gen-key <<-EOF +%echo Generating a standard key +Key-Type: DSA +Key-Length: 1024 +Subkey-Type: ELG-E +Subkey-Length: 1024 +Name-Real: Meshuggah Rocks +Name-Email: meshuggah@example.com +Expire-Date: 0 +# Do a commit here, so that we can later print "done" :-) +%commit +%echo done +EOF + +key=$(gpg --no-auto-check-trustdb --list-secret-keys | grep ^sec | cut -d/ -f2 | cut -d" " -f1) +pass init $key diff --git a/pass/cmd/main_linux.go b/pass/cmd/main_linux.go new file mode 100644 index 0000000..4fe7c1e --- /dev/null +++ b/pass/cmd/main_linux.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/docker/docker-credential-helpers/credentials" + "github.com/docker/docker-credential-helpers/pass" +) + +func main() { + credentials.Serve(pass.Pass{}) +} diff --git a/pass/pass_linux.go b/pass/pass_linux.go new file mode 100644 index 0000000..8a1fa81 --- /dev/null +++ b/pass/pass_linux.go @@ -0,0 +1,208 @@ +// A `pass` based credential helper. Passwords are stored as arguments to pass +// of the form: "$PASS_FOLDER/base64-url(serverURL)/username". We base64-url +// encode the serverURL, because under the hood pass uses files and folders, so +// /s will get translated into additional folders. +package pass + +import ( + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "strings" + + "github.com/docker/docker-credential-helpers/credentials" +) + +const PASS_FOLDER = "docker-credential-helpers" + +var ( + passInitialized bool +) + +func init() { + passInitialized = exec.Command("pass").Run() == nil +} + +func runPass(stdinContent string, args ...string) (string, error) { + cmd := exec.Command("pass", args...) + + stdin, err := cmd.StdinPipe() + if err != nil { + return "", err + } + defer stdin.Close() + + stderr, err := cmd.StderrPipe() + if err != nil { + return "", err + } + defer stderr.Close() + + stdout, err := cmd.StdoutPipe() + if err != nil { + return "", err + } + defer stdout.Close() + + err = cmd.Start() + if err != nil { + return "", err + } + + _, err = stdin.Write([]byte(stdinContent)) + if err != nil { + return "", err + } + stdin.Close() + + errContent, err := ioutil.ReadAll(stderr) + if err != nil { + return "", fmt.Errorf("error reading stderr: %s", err) + } + + result, err := ioutil.ReadAll(stdout) + if err != nil { + return "", fmt.Errorf("Error reading stdout: %s", err) + } + + cmdErr := cmd.Wait() + if cmdErr != nil { + return "", fmt.Errorf("%s: %s", cmdErr, errContent) + } + + return string(result), nil +} + +// Pass handles secrets using Linux secret-service as a store. +type Pass struct{} + +// Add adds new credentials to the keychain. +func (h Pass) Add(creds *credentials.Credentials) error { + if !passInitialized { + return errors.New("pass store is uninitialized") + } + + if creds == nil { + return errors.New("missing credentials") + } + + encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL)) + + _, err := runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username)) + return err +} + +// Delete removes credentials from the store. +func (h Pass) Delete(serverURL string) error { + if !passInitialized { + return errors.New("pass store is uninitialized") + } + + if serverURL == "" { + return errors.New("missing server url") + } + + encoded := base64.URLEncoding.EncodeToString([]byte(serverURL)) + _, err := runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded)) + return err +} + +// listPassDir lists all the contents of a directory in the password store. +// Pass uses fancy unicode to emit stuff to stdout, so rather than try +// and parse this, let's just look at the directory structure instead. +func listPassDir(args ...string) ([]os.FileInfo, error) { + passDir := os.ExpandEnv("$HOME/.password-store") + for _, e := range os.Environ() { + parts := strings.SplitN(e, "=", 2) + if len(parts) < 2 { + continue + } + + if parts[0] != "PASSWORD_STORE_DIR" { + continue + } + + passDir = parts[1] + break + } + + p := path.Join(append([]string{passDir, PASS_FOLDER}, args...)...) + contents, err := ioutil.ReadDir(p) + if err != nil { + if os.IsNotExist(err) { + return []os.FileInfo{}, nil + } + + return nil, err + } + + return contents, nil +} + +// Get returns the username and secret to use for a given registry server URL. +func (h Pass) Get(serverURL string) (string, string, error) { + if !passInitialized { + return "", "", errors.New("pass store is uninitialized") + } + + if serverURL == "" { + return "", "", errors.New("missing server url") + } + + encoded := base64.URLEncoding.EncodeToString([]byte(serverURL)) + + usernames, err := listPassDir(encoded) + if err != nil { + return "", "", err + } + + if len(usernames) < 1 { + return "", "", fmt.Errorf("no usernames for %s", serverURL) + } + + actual := strings.TrimSuffix(usernames[0].Name(), ".gpg") + secret, err := runPass("", "show", path.Join(PASS_FOLDER, encoded, actual)) + return actual, secret, err +} + +// List returns the stored URLs and corresponding usernames for a given credentials label +func (h Pass) List() (map[string]string, error) { + if !passInitialized { + return nil, errors.New("pass store is uninitialized") + } + + servers, err := listPassDir() + if err != nil { + return nil, err + } + + resp := map[string]string{} + + for _, server := range servers { + if !server.IsDir() { + continue + } + + serverURL, err := base64.URLEncoding.DecodeString(server.Name()) + if err != nil { + return nil, err + } + + usernames, err := listPassDir(server.Name()) + if err != nil { + return nil, err + } + + if len(usernames) < 1 { + return nil, fmt.Errorf("no usernames for %s", serverURL) + } + + resp[string(serverURL)] = strings.TrimSuffix(usernames[0].Name(), ".gpg") + } + + return resp, nil +} diff --git a/pass/pass_linux_test.go b/pass/pass_linux_test.go new file mode 100644 index 0000000..1e6f0f8 --- /dev/null +++ b/pass/pass_linux_test.go @@ -0,0 +1,71 @@ +package pass + +import ( + "strings" + "testing" + + "github.com/docker/docker-credential-helpers/credentials" +) + +func TestPassHelper(t *testing.T) { + helper := Pass{} + + creds := &credentials.Credentials{ + ServerURL: "https://foobar.docker.io:2376/v1", + Username: "nothing", + Secret: "isthebestmeshuggahalbum", + } + + helper.Add(creds) + + creds.ServerURL = "https://foobar.docker.io:9999/v2" + helper.Add(creds) + + credsList, err := helper.List() + if err != nil { + t.Fatal(err) + } + + for server, username := range credsList { + if !(strings.Contains(server, "2376") || + strings.Contains(server, "9999")) { + t.Fatalf("invalid url: %s", creds.ServerURL) + } + + if username != "nothing" { + t.Fatalf("invalid username: %v", username) + } + + u, s, err := helper.Get(server) + if err != nil { + t.Fatal(err) + } + + if u != username { + t.Fatalf("invalid username %s", u) + } + + if s != "isthebestmeshuggahalbum" { + t.Fatalf("invalid secret: %s", s) + } + + err = helper.Delete(server) + if err != nil { + t.Fatal(err) + } + + _, _, err = helper.Get(server) + if err == nil { + t.Fatalf("%s shuldn't exist any more", server) + } + } + + credsList, err = helper.List() + if err != nil { + t.Fatal(err) + } + + if len(credsList) != 0 { + t.Fatal("didn't delete all creds?") + } +} From 86c94d3e3068bfb66f1108d8f95e9d6aa1ab3985 Mon Sep 17 00:00:00 2001 From: Tycho Andersen Date: Wed, 9 Aug 2017 15:39:37 -0600 Subject: [PATCH 2/2] add a deb package for pass/secret service backends Note that this single source package produces two binary packages: one for -pass, and one for -secretservice, so that users can install whichever password backend (and thus deps) that they want. Signed-off-by: Tycho Andersen --- Makefile | 14 +++++++++- deb/Dockerfile | 19 ++++++++++++++ deb/build-deb | 26 +++++++++++++++++++ deb/debian/compat | 1 + deb/debian/control | 25 ++++++++++++++++++ deb/debian/docker-credential-pass.install | 1 + .../docker-credential-secretservice.install | 1 + deb/debian/rules | 17 ++++++++++++ 8 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 deb/Dockerfile create mode 100755 deb/build-deb create mode 100644 deb/debian/compat create mode 100644 deb/debian/control create mode 100644 deb/debian/docker-credential-pass.install create mode 100644 deb/debian/docker-credential-secretservice.install create mode 100755 deb/debian/rules diff --git a/Makefile b/Makefile index 1f3549e..b4715db 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: all deps osxkeychain secretservice test validate wincred pass +.PHONY: all deps osxkeychain secretservice test validate wincred pass deb TRAVIS_OS_NAME ?= linux VERSION := $(shell grep 'const Version' credentials/version.go | awk -F'"' '{ print $$2 }') @@ -68,3 +68,15 @@ fmt: gofmt -s -l `ls **/*.go | grep -v vendor` validate: vet lint fmt + + +BUILDIMG:=docker-credential-secretservice-$(VERSION) +deb: + mkdir -p release + docker build -f deb/Dockerfile \ + --build-arg VERSION=$(VERSION) \ + --build-arg DISTRO=xenial \ + --tag $(BUILDIMG) \ + . + docker run --rm --net=none $(BUILDIMG) tar cf - /release | tar xf - + docker rmi $(BUILDIMG) diff --git a/deb/Dockerfile b/deb/Dockerfile new file mode 100644 index 0000000..1e97b96 --- /dev/null +++ b/deb/Dockerfile @@ -0,0 +1,19 @@ +FROM ubuntu:xenial + +ARG VERSION +ARG DISTRO + +RUN apt-get update && apt-get install -yy debhelper dh-make golang-go libsecret-1-dev +RUN mkdir -p /build + +WORKDIR /build +ENV GOPATH /build + +COPY Makefile . +COPY credentials credentials +COPY secretservice secretservice +COPY pass pass +COPY deb/debian ./debian +COPY deb/build-deb . + +RUN /build/build-deb ${VERSION} ${DISTRO} diff --git a/deb/build-deb b/deb/build-deb new file mode 100755 index 0000000..dbb9172 --- /dev/null +++ b/deb/build-deb @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -x +set -e + +version=$1 +distro=$2 + +maintainer=$(awk -F ': ' '$1 == "Maintainer" { print $2; exit }' debian/control) + +cat > "debian/changelog" <<-EOF +docker-credential-helpers ($version) $DISTRO; urgency=low + + * New upstream version + + -- $maintainer $(date --rfc-2822) +EOF + +mkdir -p src/github.com/docker/docker-credential-helpers +ln -s /build/credentials /build/src/github.com/docker/docker-credential-helpers/credentials +ln -s /build/secretservice /build/src/github.com/docker/docker-credential-helpers/secretservice +ln -s /build/pass /build/src/github.com/docker/docker-credential-helpers/pass + +dpkg-buildpackage -us -uc + +mkdir /release +mv /docker-credential-* /release diff --git a/deb/debian/compat b/deb/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/deb/debian/compat @@ -0,0 +1 @@ +9 diff --git a/deb/debian/control b/deb/debian/control new file mode 100644 index 0000000..33f2041 --- /dev/null +++ b/deb/debian/control @@ -0,0 +1,25 @@ +Source: docker-credential-helpers +Section: admin +Priority: optional +Maintainer: Docker +Homepage: https://dockerproject.org +Standards-Version: 3.9.6 +Vcs-Browser: https://github.com/docker/docker-credential-helpers +Vcs-Git: git://github.com/docker/docker-credential-helpers.git +Build-Depends: debhelper + , dh-make + , libsecret-1-dev + +Package: docker-credential-secretservice +Architecture: any +Depends: libsecret-1-0 + , ${misc:Depends} +Description: docker-credential-secretservice is a credential helper backend + which uses libsecret to keep Docker credentials safe. + +Package: docker-credential-pass +Architecture: any +Depends: pass + , ${misc:Depends} +Description: docker-credential-secretservice is a credential helper backend + which uses the pass utility to keep Docker credentials safe. diff --git a/deb/debian/docker-credential-pass.install b/deb/debian/docker-credential-pass.install new file mode 100644 index 0000000..fb17479 --- /dev/null +++ b/deb/debian/docker-credential-pass.install @@ -0,0 +1 @@ +debian/tmp/usr/bin/docker-credential-pass diff --git a/deb/debian/docker-credential-secretservice.install b/deb/debian/docker-credential-secretservice.install new file mode 100644 index 0000000..4a17630 --- /dev/null +++ b/deb/debian/docker-credential-secretservice.install @@ -0,0 +1 @@ +debian/tmp/usr/bin/docker-credential-secretservice diff --git a/deb/debian/rules b/deb/debian/rules new file mode 100755 index 0000000..e3421e9 --- /dev/null +++ b/deb/debian/rules @@ -0,0 +1,17 @@ +#!/usr/bin/make -f + +DESTDIR := $(CURDIR)/debian/tmp + +override_dh_auto_build: + make secretservice pass + +override_dh_auto_install: + install -D bin/docker-credential-secretservice $(DESTDIR)/usr/bin/docker-credential-secretservice + install -D bin/docker-credential-pass $(DESTDIR)/usr/bin/docker-credential-pass + +%: + dh $@ + +override_dh_auto_test: + # no tests +