Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Add linux kernel keyring based credential helper (carry) #235

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/fixtures/generate.sh
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
#!/usr/bin/env sh
set -ex

gpg --batch --gen-key <<-EOF
# shellcheck disable=SC2155
export GPG_TTY=$(tty)
gpg --batch --gen-key --no-tty <<-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: [email protected]
Passphrase: with stupid passphrase
Expire-Date: 0
# Do a commit here, so that we can later print "done" :-)
%commit
%echo done
EOF

# doesn't work; still asks for passphrase interactively
# gpg --output private.pgp --armor --export-secret-key [email protected]

# doesn't work; still asks for passphrase interactively
# gpg --passphrase 'with stupid passphrase' --output private.pgp --armor --export-secret-key [email protected]

# doesn't work; still asks for passphrase interactively
# gpg --batch --passphrase 'with stupid passphrase' --no-tty --output private.pgp --armor --export-secret-key [email protected]
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ RUN xx-apt-get install -y binutils gcc libc6-dev libgcc-10-dev libsecret-1-dev p

FROM base AS test
ARG DEBIAN_FRONTEND
RUN xx-apt-get install -y dbus-x11 gnome-keyring gpg-agent gpgconf libsecret-1-dev pass
RUN xx-apt-get install -y dbus-x11 gnome-keyring gpg-agent gpgconf keyutils libsecret-1-dev pass
RUN --mount=type=bind,target=. \
--mount=type=cache,target=/root/.cache \
--mount=type=cache,target=/go/pkg/mod <<EOT
Expand Down Expand Up @@ -106,7 +106,8 @@ RUN --mount=type=bind,target=. \
--mount=type=bind,source=/tmp/.revision,target=/tmp/.revision,from=version <<EOT
set -ex
xx-go --wrap
make build-pass build-secretservice PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
make build-keyctl build-pass build-secretservice PACKAGE=$PACKAGE VERSION=$(cat /tmp/.version) REVISION=$(cat /tmp/.revision) DESTDIR=/out
xx-verify /out/docker-credential-keyctl
xx-verify /out/docker-credential-pass
xx-verify /out/docker-credential-secretservice
EOT
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ build-%: # build, can be one of build-osxkeychain build-pass build-secretservice
go build -trimpath -ldflags="$(GO_LDFLAGS) -X ${GO_PKG}/credentials.Name=docker-credential-$*" -o "$(DESTDIR)/docker-credential-$*" ./$*/cmd/

# aliases for build-* targets
.PHONY: osxkeychain secretservice pass wincred
.PHONY: osxkeychain secretservice pass wincred keyctl
osxkeychain: build-osxkeychain
secretservice: build-secretservice
pass: build-pass
wincred: build-wincred
keyctl: build-keyctl

.PHONY: cross
cross: # cross build all supported credential helpers
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,15 @@ You can see examples of each function in the [client](https://godoc.org/github.c
2. secretservice: Provides a helper to use the D-Bus secret service as credentials store.
3. wincred: Provides a helper to use Windows credentials manager as store.
4. pass: Provides a helper to use `pass` as credentials store.
5. keyctl: Provides a kernel keyring based helper as credential store. It is a purely non-file based credential store.

#### Note

`pass` needs to be configured for `docker-credential-pass` to work properly.
It must be initialized with a `gpg2` key ID. Make sure your GPG key exists is in `gpg2` keyring as `pass` uses `gpg2` instead of the regular `gpg`.

`keyctl` does not need any configuration except that kernel should be compiled with CONFIG_KEYS enabled, which is default in most distro kernels.

## Development

A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are four valid values:
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ module github.com/docker/docker-credential-helpers

go 1.19

require github.com/danieljoos/wincred v1.2.1
require (
github.com/danieljoos/wincred v1.2.1
github.com/jsipprell/keyctl v1.0.3
)

require golang.org/x/sys v0.15.0 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
github.com/danieljoos/wincred v1.2.1 h1:dl9cBrupW8+r5250DYkYxocLeZ1Y4vB1kxgtjxw8GQs=
github.com/danieljoos/wincred v1.2.1/go.mod h1:uGaFL9fDn3OLTvzCGulzE+SzjEe5NGlh5FdCcyfPwps=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/jsipprell/keyctl v1.0.3 h1:o72tppb3ZhP5B/v9FGUtMqJWx+S1Gs0elQ7AZmiNhsM=
github.com/jsipprell/keyctl v1.0.3/go.mod h1:64s6WpBtruURX3w8W/vhWj1/uh+nOm7vUXSJlK5+KMs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
Expand Down
10 changes: 10 additions & 0 deletions keyctl/cmd/main_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/keyctl"
)

func main() {
credentials.Serve(keyctl.Keyctl{})
}
256 changes: 256 additions & 0 deletions keyctl/keyctl_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// Package keyctl implements a `keyctl` based credential helper. Passwords are stored
// in linux kernel keyring.
package keyctl

import (
"bytes"
"encoding/base64"
"fmt"
"os"
"os/exec"
"strconv"
"strings"

"github.com/docker/docker-credential-helpers/credentials"
"github.com/jsipprell/keyctl"
)

// Keyctl based credential helper looks for a default keyring inside
// session keyring. It does all operations inside the default keyring

const defaultKeyringName string = "keyctlCredsStore"
const persistent int = 1

// Keyctl handles secrets using Linux Kernel keyring mechanism
type Keyctl struct{}

// createDefaultPersistentKeyring creates the default persistent keyring. If the
// keyring for the user already exists, then it returns the id of the existing
// keyring.
func (k Keyctl) createDefaultPersistentKeyring() (string, error) {
var errout, out bytes.Buffer
uid := os.Getuid()
cmd := exec.Command("keyctl", "get_persistent", "@u", strconv.Itoa(uid))
cmd.Stderr = &errout
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("cannot run keyctl command (%s) to create persistent keyring: %s: %w", cmd.String(), errout.String(), err)
}
persistentKeyringID := out.String()
if err != nil {
return "", fmt.Errorf("cannot create or read persistent keyring: %w", err)
}
return persistentKeyringID, nil
}

func (k Keyctl) getDefaultCredsStoreFromPersistent() (keyctl.NamedKeyring, error) {
var out, errout bytes.Buffer
persistentKeyringID, err := k.createDefaultPersistentKeyring()
if err != nil {
return nil, fmt.Errorf("default persistent keyring cannot be created: %w", err)
}

defaultSessionKeyring, err := keyctl.SessionKeyring()
if err != nil {
return nil, fmt.Errorf("errors getting session keyring: %w", err)
}

defaultKeyring, err := keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
// create keyring if it does not exist
if err != nil || defaultKeyring == nil {
cmd := exec.Command("keyctl", "newring", defaultKeyringName, strings.TrimSuffix(persistentKeyringID, "\n"))
cmd.Stdout = &out
cmd.Stderr = &errout
err := cmd.Run()
if err != nil {
return nil, fmt.Errorf("cannot run keyctl command to create credstore keyring (%s): %s %s: %w", cmd.String(), errout.String(), out.String(), err)
}
}
// Search for it again and return the default keyring
defaultKeyring, err = keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
if err != nil {
return nil, fmt.Errorf("failed to lookup default session keyring: %w", err)
}

return defaultKeyring, nil
}

// getDefaultCredsStore is a helper function to get the default credsStore keyring
func (k Keyctl) getDefaultCredsStore() (keyctl.NamedKeyring, error) {
if persistent == 1 { // TODO(thaJeztah) persistent is a const, and always 1, what's this check for?
cs, err := k.getDefaultCredsStoreFromPersistent()
if err != nil {
return nil, err
}
if cs == nil {
return nil, fmt.Errorf("nil credstore")
}
return cs, err
}
defaultSessionKeyring, err := keyctl.SessionKeyring()
if err != nil {
return nil, fmt.Errorf("error getting session keyring: %w", err)
}

defaultKeyring, err := keyctl.OpenKeyring(defaultSessionKeyring, defaultKeyringName)
if err != nil || defaultKeyring == nil {
if defaultKeyring == nil {
defaultKeyring, err = keyctl.CreateKeyring(defaultSessionKeyring, defaultKeyringName)
if err != nil {
return nil, fmt.Errorf("failed to create default credsStore keyring: %w", err)
}
}
}

if defaultKeyring == nil {
return nil, fmt.Errorf("nil credstore")
}

return defaultKeyring, nil
}

// Add adds new credentials to the keychain.
func (k Keyctl) Add(creds *credentials.Credentials) error {
defaultKeyring, err := k.getDefaultCredsStore()
if err != nil {
return fmt.Errorf("failed to create credsStore entry for %s: %w", creds.ServerURL, err)
}

// create a child keyring under default for given url
encoded := base64.URLEncoding.EncodeToString([]byte(strings.TrimSuffix(creds.ServerURL, "\n")))
urlKeyring, err := keyctl.CreateKeyring(defaultKeyring, encoded)
if err != nil {
return fmt.Errorf("failed to create keyring for %s: %w", creds.ServerURL, err)
}

_, err = urlKeyring.Add(creds.Username, []byte(creds.Secret))
if err != nil {
return fmt.Errorf("failed to add creds to keryring for %s: %w", creds.ServerURL, err)
}
return err
}

// searchHelper function searches for an url inside the default keyring.
func (k Keyctl) searchHelper(serverURL string) (keyctl.NamedKeyring, string, error) {
defaultKeyring, err := k.getDefaultCredsStore()
if err != nil {
return nil, "", fmt.Errorf("searchHelper failed: cannot read defaultCredsStore: %w", err)
}

encoded := base64.URLEncoding.EncodeToString([]byte(strings.TrimSuffix(serverURL, "\n")))
urlKeyring, err := keyctl.OpenKeyring(defaultKeyring, encoded)
if err != nil {
return nil, "", fmt.Errorf("error in reading credsStore for url %s", serverURL)
}
if urlKeyring == nil {
return nil, "", fmt.Errorf("credsStore entry for suplied url %s not found", serverURL)
}

refs, err := keyctl.ListKeyring(urlKeyring)
if err != nil {
return nil, "", fmt.Errorf("key for server url not found")
}
if len(refs) < 1 {
return nil, "", fmt.Errorf("no keys in keyring %s", urlKeyring.Name())
}

obj := refs[0]
id, err := obj.Get()
if err != nil {
return nil, "", fmt.Errorf("key for server url not found")
}

info, err := id.Info()
if err != nil {
return nil, "", fmt.Errorf("cannot read info for url key")
}

return urlKeyring, info.Name, err
}

// Get returns the username and secret to use for a given registry server URL.
func (k Keyctl) Get(serverURL string) (string, string, error) {
if serverURL == "" {
return "", "", fmt.Errorf("missing server url")
}

serverURL = strings.TrimSuffix(serverURL, "\n")
urlKeyring, searchData, err := k.searchHelper(serverURL)
if err != nil {
return "", "", fmt.Errorf("url (%s) not found by searchHelper: %w", serverURL, err)
}
key, err := urlKeyring.Search(searchData)
if err != nil {
return "", "", fmt.Errorf("url (%s) not found in %+v: %w", serverURL, urlKeyring, err)
}
secret, err := key.Get()
if err != nil {
return "", "", fmt.Errorf("failed to read credentials for url (%s): %s: %w", serverURL, searchData, err)
}

return searchData, string(secret), nil
}

// Delete removes credentials from the store.
func (k Keyctl) Delete(serverURL string) error {
serverURL = strings.TrimSuffix(serverURL, "\n")
urlKeyring, searchData, err := k.searchHelper(serverURL)
if err != nil {
return fmt.Errorf("cannot find server url (%s): %w", serverURL, err)
}

key, err := urlKeyring.Search(searchData)
if err != nil {
return err
}

err = key.Unlink()
if err != nil {
return err
}

refs, err := keyctl.ListKeyring(urlKeyring)
if err != nil {
fmt.Printf("cannot list keyring %s", urlKeyring.Name())
}
if len(refs) == 0 {
_ = keyctl.UnlinkKeyring(urlKeyring)
} else {
return fmt.Errorf("canot remove keyring as its not empty %s", urlKeyring.Name())
}

return err
}

// List returns the stored URLs and corresponding usernames for a given credentials label
func (k Keyctl) List() (map[string]string, error) {
defaultKeyring, err := k.getDefaultCredsStore()
if err != nil {
return nil, fmt.Errorf("failed to list credentials: cannot read default credStore: %w", err)
}

resp := map[string]string{}

refs, err := keyctl.ListKeyring(defaultKeyring)
if err != nil {
return nil, err
}

for _, r := range refs {
id, _ := r.Get()
info, _ := id.Info()
url, _ := base64.URLEncoding.DecodeString(info.Name)

key, _ := keyctl.OpenKeyring(defaultKeyring, info.Name)
innerRefs, _ := keyctl.ListKeyring(key)

if len(innerRefs) < 1 {
continue
}
k, _ := innerRefs[0].Get()
i, _ := k.Info()
resp[string(url)] = i.Name
}
return resp, nil
}
Loading
Loading