Merge pull request #566 from riyazdf/docker-trust-2

docker trust: interact with signers and keys
master
Sebastiaan van Stijn 2017-10-30 17:03:58 +01:00 committed by GitHub
commit 0c4fa699eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 2179 additions and 23 deletions

View File

@ -316,7 +316,39 @@ func (l LoadedNotaryRepository) ListRoles() ([]client.RoleWithSignatures, error)
Name: data.CanonicalTargetsRole,
}
return []client.RoleWithSignatures{{Role: rootRole}, {Role: targetsRole}}, nil
aliceRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"A"},
Threshold: 1,
},
Name: data.RoleName("targets/alice"),
}
bobRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"B"},
Threshold: 1,
},
Name: data.RoleName("targets/bob"),
}
releasesRole := data.Role{
RootRole: data.RootRole{
KeyIDs: []string{"A", "B"},
Threshold: 1,
},
Name: data.RoleName("targets/releases"),
}
// have releases only signed off by Alice last
releasesSig := []data.Signature{{KeyID: "A"}}
return []client.RoleWithSignatures{
{Role: rootRole},
{Role: targetsRole},
{Role: aliceRole},
{Role: bobRole},
{Role: releasesRole, Signatures: releasesSig},
}, nil
}
func (l LoadedNotaryRepository) ListTargets(roles ...data.RoleName) ([]*client.TargetWithRole, error) {

View File

@ -18,6 +18,8 @@ func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
newViewCommand(dockerCli),
newRevokeCommand(dockerCli),
newSignCommand(dockerCli),
newTrustKeyCommand(dockerCli),
newTrustSignerCommand(dockerCli),
)
return cmd
}

View File

@ -9,13 +9,15 @@ import (
)
const releasedRoleName = "Repo Admin"
const releasesRoleTUFName = "targets/releases"
// check if a role name is "released": either targets/releases or targets TUF roles
// isReleasedTarget checks if a role name is "released":
// either targets/releases or targets TUF roles
func isReleasedTarget(role data.RoleName) bool {
return role == data.CanonicalTargetsRole || role == trust.ReleasesRole
}
// convert TUF role name to a human-understandable signer name
// notaryRoleToSigner converts TUF role name to a human-understandable signer name
func notaryRoleToSigner(tufRole data.RoleName) string {
// don't show a signer for "targets" or "targets/releases"
if isReleasedTarget(data.RoleName(tufRole.String())) {
@ -24,6 +26,7 @@ func notaryRoleToSigner(tufRole data.RoleName) string {
return strings.TrimPrefix(tufRole.String(), "targets/")
}
// clearChangelist clears the notary staging changelist.
func clearChangeList(notaryRepo client.Repository) error {
cl, err := notaryRepo.GetChangelist()
if err != nil {
@ -31,3 +34,14 @@ func clearChangeList(notaryRepo client.Repository) error {
}
return cl.Clear("")
}
// getOrGenerateRootKeyAndInitRepo initializes the notary repository
// with a remotely managed snapshot key. The initialization will use
// an existing root key if one is found, else a new one will be generated.
func getOrGenerateRootKeyAndInitRepo(notaryRepo client.Repository) error {
rootKey, err := getOrGenerateNotaryKey(notaryRepo, data.CanonicalRootRole)
if err != nil {
return err
}
return notaryRepo.Initialize([]string{rootKey.ID()}, data.CanonicalSnapshotRole)
}

View File

@ -0,0 +1,25 @@
package trust
import (
"io/ioutil"
"os"
"testing"
"github.com/docker/notary/client"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustpinning"
"github.com/stretchr/testify/assert"
)
func TestGetOrGenerateNotaryKeyAndInitRepo(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "notary-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
notaryRepo, err := client.NewFileCachedRepository(tmpDir, "gun", "https://localhost", nil, passphrase.ConstantRetriever(passwd), trustpinning.TrustPinConfig{})
assert.NoError(t, err)
err = getOrGenerateRootKeyAndInitRepo(notaryRepo)
assert.EqualError(t, err, "client is offline")
}

22
cli/command/trust/key.go Normal file
View File

@ -0,0 +1,22 @@
package trust
import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// newTrustKeyCommand returns a cobra command for `trust key` subcommands
func newTrustKeyCommand(dockerCli command.Streams) *cobra.Command {
cmd := &cobra.Command{
Use: "key",
Short: "Manage keys for signing Docker images (experimental)",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
newKeyGenerateCommand(dockerCli),
newKeyLoadCommand(dockerCli),
)
return cmd
}

View File

@ -0,0 +1,134 @@
package trust
import (
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/notary"
"github.com/docker/notary/trustmanager"
"github.com/docker/notary/tuf/data"
tufutils "github.com/docker/notary/tuf/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type keyGenerateOptions struct {
name string
directory string
}
func newKeyGenerateCommand(dockerCli command.Streams) *cobra.Command {
options := keyGenerateOptions{}
cmd := &cobra.Command{
Use: "generate NAME",
Short: "Generate and load a signing key-pair",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
options.name = args[0]
return setupPassphraseAndGenerateKeys(dockerCli, options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.directory, "dir", "", "Directory to generate key in, defaults to current directory")
return cmd
}
// key names can use lowercase alphanumeric + _ + - characters
var validKeyName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
// validate that all of the key names are unique and are alphanumeric + _ + -
// and that we do not already have public key files in the target dir on disk
func validateKeyArgs(keyName string, targetDir string) error {
if !validKeyName(keyName) {
return fmt.Errorf("key name \"%s\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character", keyName)
}
pubKeyFileName := keyName + ".pub"
if _, err := os.Stat(targetDir); err != nil {
return fmt.Errorf("public key path does not exist: \"%s\"", targetDir)
}
targetPath := filepath.Join(targetDir, pubKeyFileName)
if _, err := os.Stat(targetPath); err == nil {
return fmt.Errorf("public key file already exists: \"%s\"", targetPath)
}
return nil
}
func setupPassphraseAndGenerateKeys(streams command.Streams, opts keyGenerateOptions) error {
targetDir := opts.directory
if targetDir == "" {
cwd, err := os.Getwd()
if err != nil {
return err
}
targetDir = cwd
}
return validateAndGenerateKey(streams, opts.name, targetDir)
}
func validateAndGenerateKey(streams command.Streams, keyName string, workingDir string) error {
freshPassRetGetter := func() notary.PassRetriever { return trust.GetPassphraseRetriever(streams.In(), streams.Out()) }
if err := validateKeyArgs(keyName, workingDir); err != nil {
return err
}
fmt.Fprintf(streams.Out(), "Generating key for %s...\n", keyName)
// Automatically load the private key to local storage for use
privKeyFileStore, err := trustmanager.NewKeyFileStore(trust.GetTrustDirectory(), freshPassRetGetter())
if err != nil {
return err
}
pubPEM, err := generateKeyAndOutputPubPEM(keyName, privKeyFileStore)
if err != nil {
fmt.Fprintf(streams.Out(), err.Error())
return errors.Wrapf(err, "failed to generate key for %s", keyName)
}
// Output the public key to a file in the CWD or specified dir
writtenPubFile, err := writePubKeyPEMToDir(pubPEM, keyName, workingDir)
if err != nil {
return err
}
fmt.Fprintf(streams.Out(), "Successfully generated and loaded private key. Corresponding public key available: %s\n", writtenPubFile)
return nil
}
func generateKeyAndOutputPubPEM(keyName string, privKeyStore trustmanager.KeyStore) (pem.Block, error) {
privKey, err := tufutils.GenerateKey(data.ECDSAKey)
if err != nil {
return pem.Block{}, err
}
privKeyStore.AddKey(trustmanager.KeyInfo{Role: data.RoleName(keyName)}, privKey)
if err != nil {
return pem.Block{}, err
}
pubKey := data.PublicKeyFromPrivate(privKey)
return pem.Block{
Type: "PUBLIC KEY",
Headers: map[string]string{
"role": keyName,
},
Bytes: pubKey.Public(),
}, nil
}
func writePubKeyPEMToDir(pubPEM pem.Block, keyName, workingDir string) (string, error) {
// Output the public key to a file in the CWD or specified dir
pubFileName := strings.Join([]string{keyName, "pub"}, ".")
pubFilePath := filepath.Join(workingDir, pubFileName)
if err := ioutil.WriteFile(pubFilePath, pem.EncodeToMemory(&pubPEM), notary.PrivNoExecPerms); err != nil {
return "", errors.Wrapf(err, "failed to write public key to %s", pubFilePath)
}
return pubFilePath, nil
}

View File

@ -0,0 +1,138 @@
package trust
import (
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/notary"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/trustmanager"
tufutils "github.com/docker/notary/tuf/utils"
"github.com/stretchr/testify/assert"
)
func TestTrustKeyGenerateErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
}{
{
name: "not-enough-args",
expectedError: "requires exactly 1 argument",
},
{
name: "too-many-args",
args: []string{"key-1", "key-2"},
expectedError: "requires exactly 1 argument",
},
}
tmpDir, err := ioutil.TempDir("", "docker-key-generate-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
config.SetDir(tmpDir)
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{})
cmd := newKeyGenerateCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestGenerateKeySuccess(t *testing.T) {
pubKeyCWD, err := ioutil.TempDir("", "pub-keys-")
assert.NoError(t, err)
defer os.RemoveAll(pubKeyCWD)
privKeyStorageDir, err := ioutil.TempDir("", "priv-keys-")
assert.NoError(t, err)
defer os.RemoveAll(privKeyStorageDir)
passwd := "password"
cannedPasswordRetriever := passphrase.ConstantRetriever(passwd)
// generate a single key
keyName := "alice"
privKeyFileStore, err := trustmanager.NewKeyFileStore(privKeyStorageDir, cannedPasswordRetriever)
assert.NoError(t, err)
pubKeyPEM, err := generateKeyAndOutputPubPEM(keyName, privKeyFileStore)
assert.NoError(t, err)
assert.Equal(t, keyName, pubKeyPEM.Headers["role"])
// the default GUN is empty
assert.Equal(t, "", pubKeyPEM.Headers["gun"])
// assert public key header
assert.Equal(t, "PUBLIC KEY", pubKeyPEM.Type)
// check that an appropriate ~/<trust_dir>/private/<key_id>.key file exists
expectedPrivKeyDir := filepath.Join(privKeyStorageDir, notary.PrivDir)
_, err = os.Stat(expectedPrivKeyDir)
assert.NoError(t, err)
keyFiles, err := ioutil.ReadDir(expectedPrivKeyDir)
assert.NoError(t, err)
assert.Len(t, keyFiles, 1)
privKeyFilePath := filepath.Join(expectedPrivKeyDir, keyFiles[0].Name())
// verify the key content
privFrom, _ := os.OpenFile(privKeyFilePath, os.O_RDONLY, notary.PrivExecPerms)
defer privFrom.Close()
fromBytes, _ := ioutil.ReadAll(privFrom)
privKeyPEM, _ := pem.Decode(fromBytes)
assert.Equal(t, keyName, privKeyPEM.Headers["role"])
// the default GUN is empty
assert.Equal(t, "", privKeyPEM.Headers["gun"])
// assert encrypted header
assert.Equal(t, "ENCRYPTED PRIVATE KEY", privKeyPEM.Type)
// check that the passphrase matches
_, err = tufutils.ParsePKCS8ToTufKey(privKeyPEM.Bytes, []byte(passwd))
assert.NoError(t, err)
// check that the public key exists at the correct path if we use the helper:
returnedPath, err := writePubKeyPEMToDir(pubKeyPEM, keyName, pubKeyCWD)
assert.NoError(t, err)
expectedPubKeyPath := filepath.Join(pubKeyCWD, keyName+".pub")
assert.Equal(t, returnedPath, expectedPubKeyPath)
_, err = os.Stat(expectedPubKeyPath)
assert.NoError(t, err)
// check that the public key is the only file output in CWD
cwdKeyFiles, err := ioutil.ReadDir(pubKeyCWD)
assert.NoError(t, err)
assert.Len(t, cwdKeyFiles, 1)
}
func TestValidateKeyArgs(t *testing.T) {
pubKeyCWD, err := ioutil.TempDir("", "pub-keys-")
assert.NoError(t, err)
defer os.RemoveAll(pubKeyCWD)
err = validateKeyArgs("a", pubKeyCWD)
assert.NoError(t, err)
err = validateKeyArgs("a/b", pubKeyCWD)
assert.Error(t, err)
assert.Equal(t, err.Error(), "key name \"a/b\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character")
err = validateKeyArgs("-", pubKeyCWD)
assert.Error(t, err)
assert.Equal(t, err.Error(), "key name \"-\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character")
assert.NoError(t, ioutil.WriteFile(filepath.Join(pubKeyCWD, "a.pub"), []byte("abc"), notary.PrivExecPerms))
err = validateKeyArgs("a", pubKeyCWD)
assert.Error(t, err)
assert.Equal(t, err.Error(), fmt.Sprintf("public key file already exists: \"%s/a.pub\"", pubKeyCWD))
err = validateKeyArgs("a", "/random/dir/")
assert.Error(t, err)
assert.Equal(t, err.Error(), "public key path does not exist: \"/random/dir/\"")
}

View File

@ -0,0 +1,115 @@
package trust
import (
"bytes"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/trust"
"github.com/docker/notary"
"github.com/docker/notary/storage"
"github.com/docker/notary/trustmanager"
tufutils "github.com/docker/notary/tuf/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
const (
nonOwnerReadWriteMask = 0077
)
type keyLoadOptions struct {
keyName string
}
func newKeyLoadCommand(dockerCli command.Streams) *cobra.Command {
var options keyLoadOptions
cmd := &cobra.Command{
Use: "load [OPTIONS] KEYFILE",
Short: "Load a private key file for signing",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return loadPrivKey(dockerCli, args[0], options)
},
}
flags := cmd.Flags()
flags.StringVar(&options.keyName, "name", "signer", "Name for the loaded key")
return cmd
}
func loadPrivKey(streams command.Streams, keyPath string, options keyLoadOptions) error {
// validate the key name if provided
if options.keyName != "" && !validKeyName(options.keyName) {
return fmt.Errorf("key name \"%s\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character", options.keyName)
}
trustDir := trust.GetTrustDirectory()
keyFileStore, err := storage.NewPrivateKeyFileStorage(trustDir, notary.KeyExtension)
if err != nil {
return err
}
privKeyImporters := []trustmanager.Importer{keyFileStore}
fmt.Fprintf(streams.Out(), "Loading key from \"%s\"...\n", keyPath)
// Always use a fresh passphrase retriever for each import
passRet := trust.GetPassphraseRetriever(streams.In(), streams.Out())
keyBytes, err := getPrivKeyBytesFromPath(keyPath)
if err != nil {
return errors.Wrapf(err, "refusing to load key from %s", keyPath)
}
if err := loadPrivKeyBytesToStore(keyBytes, privKeyImporters, keyPath, options.keyName, passRet); err != nil {
return errors.Wrapf(err, "error importing key from %s", keyPath)
}
fmt.Fprintf(streams.Out(), "Successfully imported key from %s\n", keyPath)
return nil
}
func getPrivKeyBytesFromPath(keyPath string) ([]byte, error) {
fileInfo, err := os.Stat(keyPath)
if err != nil {
return nil, err
}
if fileInfo.Mode()&nonOwnerReadWriteMask != 0 {
return nil, fmt.Errorf("private key file %s must not be readable or writable by others", keyPath)
}
from, err := os.OpenFile(keyPath, os.O_RDONLY, notary.PrivExecPerms)
if err != nil {
return nil, err
}
defer from.Close()
return ioutil.ReadAll(from)
}
func loadPrivKeyBytesToStore(privKeyBytes []byte, privKeyImporters []trustmanager.Importer, keyPath, keyName string, passRet notary.PassRetriever) error {
var err error
if _, _, err = tufutils.ExtractPrivateKeyAttributes(privKeyBytes); err != nil {
return fmt.Errorf("provided file %s is not a supported private key - to add a signer's public key use docker trust signer add", keyPath)
}
if privKeyBytes, err = decodePrivKeyIfNecessary(privKeyBytes, passRet); err != nil {
return errors.Wrapf(err, "cannot load key from provided file %s", keyPath)
}
// Make a reader, rewind the file pointer
return trustmanager.ImportKeys(bytes.NewReader(privKeyBytes), privKeyImporters, keyName, "", passRet)
}
func decodePrivKeyIfNecessary(privPemBytes []byte, passRet notary.PassRetriever) ([]byte, error) {
pemBlock, _ := pem.Decode(privPemBytes)
_, containsDEKInfo := pemBlock.Headers["DEK-Info"]
if containsDEKInfo || pemBlock.Type == "ENCRYPTED PRIVATE KEY" {
// if we do not have enough information to properly import, try to decrypt the key
if _, ok := pemBlock.Headers["path"]; !ok {
privKey, _, err := trustmanager.GetPasswdDecryptBytes(passRet, privPemBytes, "", "encrypted")
if err != nil {
return []byte{}, fmt.Errorf("could not decrypt key")
}
privPemBytes = privKey.Private()
}
}
return privPemBytes, nil
}

View File

@ -0,0 +1,244 @@
package trust
import (
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/notary"
"github.com/docker/notary/passphrase"
"github.com/docker/notary/storage"
"github.com/docker/notary/trustmanager"
tufutils "github.com/docker/notary/tuf/utils"
"github.com/stretchr/testify/assert"
)
func TestTrustKeyLoadErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
expectedOutput string
}{
{
name: "not-enough-args",
expectedError: "exactly 1 argument",
expectedOutput: "",
},
{
name: "too-many-args",
args: []string{"iamnotakey", "alsonotakey"},
expectedError: "exactly 1 argument",
expectedOutput: "",
},
{
name: "not-a-key",
args: []string{"iamnotakey"},
expectedError: "refusing to load key from iamnotakey: stat iamnotakey: no such file or directory",
expectedOutput: "Loading key from \"iamnotakey\"...\n",
},
{
name: "bad-key-name",
args: []string{"iamnotakey", "--name", "KEYNAME"},
expectedError: "key name \"KEYNAME\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character",
expectedOutput: "",
},
}
tmpDir, err := ioutil.TempDir("", "docker-key-load-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
config.SetDir(tmpDir)
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{})
cmd := newKeyLoadCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
assert.Contains(t, cli.OutBuffer().String(), tc.expectedOutput)
}
}
var rsaPrivKeyFixture = []byte(`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAs7yVMzCw8CBZPoN+QLdx3ZzbVaHnouHIKu+ynX60IZ3stpbb
6rowu78OWON252JcYJqe++2GmdIgbBhg+mZDwhX0ZibMVztJaZFsYL+Ch/2J9KqD
A5NtE1s/XdhYoX5hsv7W4ok9jLFXRYIMj+T4exJRlR4f4GP9p0fcqPWd9/enPnlJ
JFTmu0DXJTZUMVS1UrXUy5t/DPXdrwyl8pM7VCqO3bqK7jqE6mWawdTkEeiku1fJ
ydP0285uiYTbj1Q38VVhPwXzMuLbkaUgRJhCI4BcjfQIjtJLbWpS+VdhUEvtgMVx
XJMKxCVGG69qjXyj9TjI7pxanb/bWglhovJN9wIDAQABAoIBAQCSnMsLxbUfOxPx
RWuwOLN+NZxIvtfnastQEtSdWiRvo5Xa3zYmw5hLHa8DXRC57+cwug/jqr54LQpb
gotg1hiBck05In7ezTK2FXTVeoJskal91bUnLpP0DSOkVnz9xszFKNF6Wr7FTEfH
IC1FF16Fbcz0mW0hKg9X6+uYOzqPcKpQRwli5LAwhT18Alf9h4/3NCeKotiJyr2J
xvcEH1eY2m2c/jQZurBkys7qBC3+i8LJEOW8MBQt7mxajwfbU91wtP2YoqMcoYiS
zsPbYp7Ui2t4G9Yn+OJw+uj4RGP1Bo4nSyRxWDtg+8Zug/JYU6/s+8kVRpiGffd3
T1GvoxUhAoGBAOnPDWG/g1xlJf65Rh71CxMs638zhYbIloU2K4Rqr05DHe7GryTS
9hLVrwhHddK+KwfVbR8HFMPo1DC/NVbuKt8StTAadAu3HsC088gWd28nOiGAWuvH
Bo3x/DYQGYwGFfoo4rzCOgMj6DJjXmcWEXNv3NDMoXoYpkxa0g6zZDyHAoGBAMTL
t7EUneJT+Mm7wyL1I5bmaT/HFwqoUQB2ccBPVD8p1el62NgLdfhOa8iNlBVhMrlh
2aTjrMlSPcjr9sCgKrLcenSWw+2qFsf4+SmV01ntB9kWes2phXpnB0ynXIcbeG05
+BLxbqDTVV0Iqh4r/dGeplyV2WyL3mTpkT3hRq8RAoGAZ93degEUICWnHWO9LN97
Dge0joua0+ekRoVsC6VBP6k9UOfewqMdQfy/hxQH2Zk1kINVuKTyqp1yNj2bOoUP
co3jA/2cc9/jv4QjkE26vRxWDK/ytC90T/aiLno0fyns9XbYUzaNgvuemVPfijgZ
hIi7Nd7SFWWB6wWlr3YuH10CgYEAwh7JVa2mh8iZEjVaKTNyJbmmfDjgq6yYKkKr
ti0KRzv3O9Xn7ERx27tPaobtWaGFLYQt8g57NCMhuv23aw8Sz1fYmwTUw60Rx7P5
42FdF8lOAn/AJvpfJfxXIO+9v7ADPIr//3+TxqRwAdM4K4btWkaKh61wyTe26gfT
MxzyYmECgYAnlU5zsGyiZqwoXVktkhtZrE7Qu0SoztzFb8KpvFNmMTPF1kAAYmJY
GIhbizeGJ3h4cUdozKmt8ZWIt6uFDEYCqEA7XF4RH75dW25x86mpIPO7iRl9eisY
IsLeMYqTIwXAwGx6Ka9v5LOL1kzcHQ2iVj6+QX+yoptSft1dYa9jOA==
-----END RSA PRIVATE KEY-----`)
const rsaPrivKeyID = "ee69e8e07a14756ad5ff0aca2336b37f86b0ac1710d1f3e94440081e080aecd7"
var ecPrivKeyFixture = []byte(`-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINfxKtDH3ug7ZIQPDyeAzujCdhw36D+bf9ToPE1A7YEyoAoGCCqGSM49
AwEHoUQDQgAEUIH9AYtrcDFzZrFJBdJZkn21d+4cH3nzy2O6Q/ct4BjOBKa+WCdR
tPo78bA+C/7t81ADQO8Jqaj59W50rwoqDQ==
-----END EC PRIVATE KEY-----`)
const ecPrivKeyID = "46157cb0becf9c72c3219e11d4692424fef9bf4460812ccc8a71a3dfcafc7e60"
var testKeys = map[string][]byte{
ecPrivKeyID: ecPrivKeyFixture,
rsaPrivKeyID: rsaPrivKeyFixture,
}
func TestLoadKeyFromPath(t *testing.T) {
for keyID, keyBytes := range testKeys {
t.Run(fmt.Sprintf("load-key-id-%s-from-path", keyID), func(t *testing.T) {
testLoadKeyFromPath(t, keyID, keyBytes)
})
}
}
func testLoadKeyFromPath(t *testing.T, privKeyID string, privKeyFixture []byte) {
privKeyDir, err := ioutil.TempDir("", "key-load-test-")
assert.NoError(t, err)
defer os.RemoveAll(privKeyDir)
privKeyFilepath := filepath.Join(privKeyDir, "privkey.pem")
assert.NoError(t, ioutil.WriteFile(privKeyFilepath, privKeyFixture, notary.PrivNoExecPerms))
keyStorageDir, err := ioutil.TempDir("", "loaded-keys-")
assert.NoError(t, err)
defer os.RemoveAll(keyStorageDir)
passwd := "password"
cannedPasswordRetriever := passphrase.ConstantRetriever(passwd)
keyFileStore, err := storage.NewPrivateKeyFileStorage(keyStorageDir, notary.KeyExtension)
assert.NoError(t, err)
privKeyImporters := []trustmanager.Importer{keyFileStore}
// get the privKeyBytes
privKeyBytes, err := getPrivKeyBytesFromPath(privKeyFilepath)
assert.NoError(t, err)
// import the key to our keyStorageDir
assert.NoError(t, loadPrivKeyBytesToStore(privKeyBytes, privKeyImporters, privKeyFilepath, "signer-name", cannedPasswordRetriever))
// check that the appropriate ~/<trust_dir>/private/<key_id>.key file exists
expectedImportKeyPath := filepath.Join(keyStorageDir, notary.PrivDir, privKeyID+"."+notary.KeyExtension)
_, err = os.Stat(expectedImportKeyPath)
assert.NoError(t, err)
// verify the key content
from, _ := os.OpenFile(expectedImportKeyPath, os.O_RDONLY, notary.PrivExecPerms)
defer from.Close()
fromBytes, _ := ioutil.ReadAll(from)
keyPEM, _ := pem.Decode(fromBytes)
assert.Equal(t, "signer-name", keyPEM.Headers["role"])
// the default GUN is empty
assert.Equal(t, "", keyPEM.Headers["gun"])
// assert encrypted header
assert.Equal(t, "ENCRYPTED PRIVATE KEY", keyPEM.Type)
decryptedKey, err := tufutils.ParsePKCS8ToTufKey(keyPEM.Bytes, []byte(passwd))
assert.NoError(t, err)
fixturePEM, _ := pem.Decode(privKeyFixture)
assert.Equal(t, fixturePEM.Bytes, decryptedKey.Private())
}
func TestLoadKeyTooPermissive(t *testing.T) {
for keyID, keyBytes := range testKeys {
t.Run(fmt.Sprintf("load-key-id-%s-too-permissive", keyID), func(t *testing.T) {
testLoadKeyTooPermissive(t, keyBytes)
})
}
}
func testLoadKeyTooPermissive(t *testing.T, privKeyFixture []byte) {
privKeyDir, err := ioutil.TempDir("", "key-load-test-")
assert.NoError(t, err)
defer os.RemoveAll(privKeyDir)
privKeyFilepath := filepath.Join(privKeyDir, "privkey477.pem")
assert.NoError(t, ioutil.WriteFile(privKeyFilepath, privKeyFixture, 0477))
keyStorageDir, err := ioutil.TempDir("", "loaded-keys-")
assert.NoError(t, err)
defer os.RemoveAll(keyStorageDir)
// import the key to our keyStorageDir
_, err = getPrivKeyBytesFromPath(privKeyFilepath)
assert.Error(t, err)
assert.Contains(t, fmt.Sprintf("private key file %s must not be readable or writable by others", privKeyFilepath), err.Error())
privKeyFilepath = filepath.Join(privKeyDir, "privkey667.pem")
assert.NoError(t, ioutil.WriteFile(privKeyFilepath, privKeyFixture, 0677))
_, err = getPrivKeyBytesFromPath(privKeyFilepath)
assert.Error(t, err)
assert.Contains(t, fmt.Sprintf("private key file %s must not be readable or writable by others", privKeyFilepath), err.Error())
privKeyFilepath = filepath.Join(privKeyDir, "privkey777.pem")
assert.NoError(t, ioutil.WriteFile(privKeyFilepath, privKeyFixture, 0777))
_, err = getPrivKeyBytesFromPath(privKeyFilepath)
assert.Error(t, err)
assert.Contains(t, fmt.Sprintf("private key file %s must not be readable or writable by others", privKeyFilepath), err.Error())
privKeyFilepath = filepath.Join(privKeyDir, "privkey400.pem")
assert.NoError(t, ioutil.WriteFile(privKeyFilepath, privKeyFixture, 0400))
_, err = getPrivKeyBytesFromPath(privKeyFilepath)
assert.NoError(t, err)
privKeyFilepath = filepath.Join(privKeyDir, "privkey600.pem")
assert.NoError(t, ioutil.WriteFile(privKeyFilepath, privKeyFixture, 0600))
_, err = getPrivKeyBytesFromPath(privKeyFilepath)
assert.NoError(t, err)
}
var pubKeyFixture = []byte(`-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEUIH9AYtrcDFzZrFJBdJZkn21d+4c
H3nzy2O6Q/ct4BjOBKa+WCdRtPo78bA+C/7t81ADQO8Jqaj59W50rwoqDQ==
-----END PUBLIC KEY-----`)
func TestLoadPubKeyFailure(t *testing.T) {
pubKeyDir, err := ioutil.TempDir("", "key-load-test-pubkey-")
assert.NoError(t, err)
defer os.RemoveAll(pubKeyDir)
pubKeyFilepath := filepath.Join(pubKeyDir, "pubkey.pem")
assert.NoError(t, ioutil.WriteFile(pubKeyFilepath, pubKeyFixture, notary.PrivNoExecPerms))
keyStorageDir, err := ioutil.TempDir("", "loaded-keys-")
assert.NoError(t, err)
defer os.RemoveAll(keyStorageDir)
passwd := "password"
cannedPasswordRetriever := passphrase.ConstantRetriever(passwd)
keyFileStore, err := storage.NewPrivateKeyFileStorage(keyStorageDir, notary.KeyExtension)
assert.NoError(t, err)
privKeyImporters := []trustmanager.Importer{keyFileStore}
pubKeyBytes, err := getPrivKeyBytesFromPath(pubKeyFilepath)
assert.NoError(t, err)
// import the key to our keyStorageDir - it should fail
err = loadPrivKeyBytesToStore(pubKeyBytes, privKeyImporters, pubKeyFilepath, "signer-name", cannedPasswordRetriever)
assert.Error(t, err)
assert.Contains(t, fmt.Sprintf("provided file %s is not a supported private key - to add a signer's public key use docker trust signer add", pubKeyFilepath), err.Error())
}

View File

@ -183,7 +183,9 @@ func initNotaryRepoWithSigners(notaryRepo client.Repository, newSigner data.Role
if err != nil {
return err
}
addStagedSigner(notaryRepo, newSigner, []data.PublicKey{signerKey})
if err := addStagedSigner(notaryRepo, newSigner, []data.PublicKey{signerKey}); err != nil {
return errors.Wrapf(err, "could not add signer to repo: %s", strings.TrimPrefix(newSigner.String(), "targets/"))
}
return notaryRepo.Publish()
}
@ -216,12 +218,21 @@ func getOrGenerateNotaryKey(notaryRepo client.Repository, role data.RoleName) (d
}
// stages changes to add a signer with the specified name and key(s). Adds to targets/<name> and targets/releases
func addStagedSigner(notaryRepo client.Repository, newSigner data.RoleName, signerKeys []data.PublicKey) {
func addStagedSigner(notaryRepo client.Repository, newSigner data.RoleName, signerKeys []data.PublicKey) error {
// create targets/<username>
notaryRepo.AddDelegationRoleAndKeys(newSigner, signerKeys)
notaryRepo.AddDelegationPaths(newSigner, []string{""})
if err := notaryRepo.AddDelegationRoleAndKeys(newSigner, signerKeys); err != nil {
return err
}
if err := notaryRepo.AddDelegationPaths(newSigner, []string{""}); err != nil {
return err
}
// create targets/releases
notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys)
notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""})
if err := notaryRepo.AddDelegationRoleAndKeys(trust.ReleasesRole, signerKeys); err != nil {
return err
}
if err := notaryRepo.AddDelegationPaths(trust.ReleasesRole, []string{""}); err != nil {
return err
}
return nil
}

View File

@ -140,7 +140,8 @@ func TestAddStageSigners(t *testing.T) {
// stage targets/user
userRole := data.RoleName("targets/user")
userKey := data.NewPublicKey("algoA", []byte("a"))
addStagedSigner(notaryRepo, userRole, []data.PublicKey{userKey})
err = addStagedSigner(notaryRepo, userRole, []data.PublicKey{userKey})
assert.NoError(t, err)
// check the changelist for four total changes: two on targets/releases and two on targets/user
cl, err := notaryRepo.GetChangelist()
assert.NoError(t, err)

View File

@ -0,0 +1,22 @@
package trust
import (
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
// newTrustSignerCommand returns a cobra command for `trust signer` subcommands
func newTrustSignerCommand(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "signer",
Short: "Manage entities who can sign Docker images (experimental)",
Args: cli.NoArgs,
RunE: command.ShowHelp(dockerCli.Err()),
}
cmd.AddCommand(
newSignerAddCommand(dockerCli),
newSignerRemoveCommand(dockerCli),
)
return cmd
}

View File

@ -0,0 +1,141 @@
package trust
import (
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path"
"regexp"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/trust"
"github.com/docker/cli/opts"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
tufutils "github.com/docker/notary/tuf/utils"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type signerAddOptions struct {
keys opts.ListOpts
signer string
repos []string
}
func newSignerAddCommand(dockerCli command.Cli) *cobra.Command {
var options signerAddOptions
cmd := &cobra.Command{
Use: "add OPTIONS NAME REPOSITORY [REPOSITORY...] ",
Short: "Add a signer",
Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
options.signer = args[0]
options.repos = args[1:]
return addSigner(dockerCli, options)
},
}
flags := cmd.Flags()
options.keys = opts.NewListOpts(nil)
flags.Var(&options.keys, "key", "Path to the signer's public key file")
return cmd
}
var validSignerName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
func addSigner(cli command.Cli, options signerAddOptions) error {
signerName := options.signer
if !validSignerName(signerName) {
return fmt.Errorf("signer name \"%s\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character", signerName)
}
if signerName == "releases" {
return fmt.Errorf("releases is a reserved keyword, please use a different signer name")
}
if options.keys.Len() == 0 {
return fmt.Errorf("path to a public key must be provided using the `--key` flag")
}
signerPubKeys, err := ingestPublicKeys(options.keys.GetAll())
if err != nil {
return err
}
var errRepos []string
for _, repoName := range options.repos {
fmt.Fprintf(cli.Out(), "Adding signer \"%s\" to %s...\n", signerName, repoName)
if err := addSignerToRepo(cli, signerName, repoName, signerPubKeys); err != nil {
fmt.Fprintln(cli.Err(), err.Error()+"\n")
errRepos = append(errRepos, repoName)
} else {
fmt.Fprintf(cli.Out(), "Successfully added signer: %s to %s\n\n", signerName, repoName)
}
}
if len(errRepos) > 0 {
return fmt.Errorf("Failed to add signer to: %s", strings.Join(errRepos, ", "))
}
return nil
}
func addSignerToRepo(cli command.Cli, signerName string, repoName string, signerPubKeys []data.PublicKey) error {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), repoName)
if err != nil {
return err
}
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPushAndPull)
if err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
if _, err = notaryRepo.ListTargets(); err != nil {
switch err.(type) {
case client.ErrRepoNotInitialized, client.ErrRepositoryNotExist:
fmt.Fprintf(cli.Out(), "Initializing signed repository for %s...\n", repoName)
if err := getOrGenerateRootKeyAndInitRepo(notaryRepo); err != nil {
return trust.NotaryError(repoName, err)
}
fmt.Fprintf(cli.Out(), "Successfully initialized %q\n", repoName)
default:
return trust.NotaryError(repoName, err)
}
}
newSignerRoleName := data.RoleName(path.Join(data.CanonicalTargetsRole.String(), signerName))
if err := addStagedSigner(notaryRepo, newSignerRoleName, signerPubKeys); err != nil {
return errors.Wrapf(err, "could not add signer to repo: %s", strings.TrimPrefix(newSignerRoleName.String(), "targets/"))
}
return notaryRepo.Publish()
}
func ingestPublicKeys(pubKeyPaths []string) ([]data.PublicKey, error) {
pubKeys := []data.PublicKey{}
for _, pubKeyPath := range pubKeyPaths {
// Read public key bytes from PEM file, limit to 1 KiB
pubKeyFile, err := os.OpenFile(pubKeyPath, os.O_RDONLY, 0666)
if err != nil {
return nil, errors.Wrap(err, "unable to read public key from file")
}
defer pubKeyFile.Close()
// limit to
l := io.LimitReader(pubKeyFile, 1<<20)
pubKeyBytes, err := ioutil.ReadAll(l)
if err != nil {
return nil, errors.Wrap(err, "unable to read public key from file")
}
// Parse PEM bytes into type PublicKey
pubKey, err := tufutils.ParsePEMPublicKey(pubKeyBytes)
if err != nil {
return nil, errors.Wrapf(err, "could not parse public key from file: %s", pubKeyPath)
}
pubKeys = append(pubKeys, pubKey)
}
return pubKeys, nil
}

View File

@ -0,0 +1,137 @@
package trust
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/notary"
"github.com/stretchr/testify/assert"
)
func TestTrustSignerAddErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
}{
{
name: "not-enough-args",
expectedError: "requires at least 2 argument",
},
{
name: "no-key",
args: []string{"foo", "bar"},
expectedError: "path to a public key must be provided using the `--key` flag",
},
{
name: "reserved-releases-signer-add",
args: []string{"releases", "my-image", "--key", "/path/to/key"},
expectedError: "releases is a reserved keyword, please use a different signer name",
},
{
name: "disallowed-chars",
args: []string{"ali/ce", "my-image", "--key", "/path/to/key"},
expectedError: "signer name \"ali/ce\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character",
},
{
name: "no-upper-case",
args: []string{"Alice", "my-image", "--key", "/path/to/key"},
expectedError: "signer name \"Alice\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character",
},
{
name: "start-with-letter",
args: []string{"_alice", "my-image", "--key", "/path/to/key"},
expectedError: "signer name \"_alice\" must start with lowercase alphanumeric characters and can include \"-\" or \"_\" after the first character",
},
}
tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
config.SetDir(tmpDir)
for _, tc := range testCases {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd := newSignerAddCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
}
func TestSignerAddCommandNoTargetsKey(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
config.SetDir(tmpDir)
tmpfile, err := ioutil.TempFile("", "pemfile")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd := newSignerAddCommand(cli)
cmd.SetArgs([]string{"--key", tmpfile.Name(), "alice", "alpine", "linuxkit/alpine"})
cmd.SetOutput(ioutil.Discard)
assert.EqualError(t, cmd.Execute(), fmt.Sprintf("could not parse public key from file: %s: no valid public key found", tmpfile.Name()))
}
func TestSignerAddCommandBadKeyPath(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
config.SetDir(tmpDir)
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getEmptyTargetsNotaryRepository)
cmd := newSignerAddCommand(cli)
cmd.SetArgs([]string{"--key", "/path/to/key.pem", "alice", "alpine"})
cmd.SetOutput(ioutil.Discard)
assert.EqualError(t, cmd.Execute(), "unable to read public key from file: open /path/to/key.pem: no such file or directory")
}
func TestSignerAddCommandInvalidRepoName(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "docker-sign-test-")
assert.NoError(t, err)
defer os.RemoveAll(tmpDir)
config.SetDir(tmpDir)
pubKeyDir, err := ioutil.TempDir("", "key-load-test-pubkey-")
assert.NoError(t, err)
defer os.RemoveAll(pubKeyDir)
pubKeyFilepath := filepath.Join(pubKeyDir, "pubkey.pem")
assert.NoError(t, ioutil.WriteFile(pubKeyFilepath, pubKeyFixture, notary.PrivNoExecPerms))
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getUninitializedNotaryRepository)
cmd := newSignerAddCommand(cli)
imageName := "870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"
cmd.SetArgs([]string{"--key", pubKeyFilepath, "alice", imageName})
cmd.SetOutput(ioutil.Discard)
assert.EqualError(t, cmd.Execute(), "Failed to add signer to: 870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd")
expectedErr := fmt.Sprintf("invalid repository name (%s), cannot specify 64-byte hexadecimal strings\n\n", imageName)
assert.Equal(t, expectedErr, cli.ErrBuffer().String())
}
func TestIngestPublicKeys(t *testing.T) {
// Call with a bad path
_, err := ingestPublicKeys([]string{"foo", "bar"})
assert.EqualError(t, err, "unable to read public key from file: open foo: no such file or directory")
// Call with real file path
tmpfile, err := ioutil.TempFile("", "pemfile")
assert.NoError(t, err)
defer os.Remove(tmpfile.Name())
_, err = ingestPublicKeys([]string{tmpfile.Name()})
assert.EqualError(t, err, fmt.Sprintf("could not parse public key from file: %s: no valid public key found", tmpfile.Name()))
}

View File

@ -0,0 +1,138 @@
package trust
import (
"context"
"fmt"
"os"
"strings"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/image"
"github.com/docker/cli/cli/trust"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type signerRemoveOptions struct {
signer string
repos []string
forceYes bool
}
func newSignerRemoveCommand(dockerCli command.Cli) *cobra.Command {
options := signerRemoveOptions{}
cmd := &cobra.Command{
Use: "remove [OPTIONS] NAME REPOSITORY [REPOSITORY...]",
Short: "Remove a signer",
Args: cli.RequiresMinArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
options.signer = args[0]
options.repos = args[1:]
return removeSigner(dockerCli, options)
},
}
flags := cmd.Flags()
flags.BoolVarP(&options.forceYes, "force", "f", false, "Do not prompt for confirmation before removing the most recent signer")
return cmd
}
func removeSigner(cli command.Cli, options signerRemoveOptions) error {
var errRepos []string
for _, repo := range options.repos {
fmt.Fprintf(cli.Out(), "Removing signer \"%s\" from %s...\n", options.signer, repo)
if err := removeSingleSigner(cli, repo, options.signer, options.forceYes); err != nil {
fmt.Fprintln(cli.Err(), err.Error()+"\n")
errRepos = append(errRepos, repo)
} else {
fmt.Fprintf(cli.Out(), "Successfully removed %s from %s\n\n", options.signer, repo)
}
}
if len(errRepos) > 0 {
return fmt.Errorf("Error removing signer from: %s", strings.Join(errRepos, ", "))
}
return nil
}
func isLastSignerForReleases(roleWithSig data.Role, allRoles []client.RoleWithSignatures) (bool, error) {
var releasesRoleWithSigs client.RoleWithSignatures
for _, role := range allRoles {
if role.Name == releasesRoleTUFName {
releasesRoleWithSigs = role
break
}
}
counter := len(releasesRoleWithSigs.Signatures)
if counter == 0 {
return false, fmt.Errorf("All signed tags are currently revoked, use docker trust sign to fix")
}
for _, signature := range releasesRoleWithSigs.Signatures {
for _, key := range roleWithSig.KeyIDs {
if signature.KeyID == key {
counter--
}
}
}
return counter < releasesRoleWithSigs.Threshold, nil
}
func removeSingleSigner(cli command.Cli, repoName, signerName string, forceYes bool) error {
ctx := context.Background()
imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(cli), repoName)
if err != nil {
return err
}
signerDelegation := data.RoleName("targets/" + signerName)
if signerDelegation == releasesRoleTUFName {
return fmt.Errorf("releases is a reserved keyword and cannot be removed")
}
notaryRepo, err := cli.NotaryClient(imgRefAndAuth, trust.ActionsPushAndPull)
if err != nil {
return trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
}
delegationRoles, err := notaryRepo.GetDelegationRoles()
if err != nil {
return errors.Wrapf(err, "error retrieving signers for %s", repoName)
}
var role data.Role
for _, delRole := range delegationRoles {
if delRole.Name == signerDelegation {
role = delRole
break
}
}
if role.Name == "" {
return fmt.Errorf("No signer %s for repository %s", signerName, repoName)
}
allRoles, err := notaryRepo.ListRoles()
if err != nil {
return err
}
if ok, err := isLastSignerForReleases(role, allRoles); ok && !forceYes {
removeSigner := command.PromptForConfirmation(os.Stdin, cli.Out(), fmt.Sprintf("The signer \"%s\" signed the last released version of %s. "+
"Removing this signer will make %s unpullable. "+
"Are you sure you want to continue?",
signerName, repoName, repoName,
))
if !removeSigner {
fmt.Fprintf(cli.Out(), "\nAborting action.\n")
return nil
}
} else if err != nil {
return err
}
if err = notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
return err
}
if err = notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
return err
}
if err = notaryRepo.Publish(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,124 @@
package trust
import (
"io/ioutil"
"testing"
"github.com/docker/cli/internal/test"
"github.com/docker/cli/internal/test/testutil"
"github.com/docker/notary/client"
"github.com/docker/notary/tuf/data"
"github.com/stretchr/testify/assert"
)
func TestTrustSignerRemoveErrors(t *testing.T) {
testCases := []struct {
name string
args []string
expectedError string
}{
{
name: "not-enough-args-0",
expectedError: "requires at least 2 arguments",
},
{
name: "not-enough-args-1",
args: []string{"user"},
expectedError: "requires at least 2 arguments",
},
}
for _, tc := range testCases {
cmd := newSignerRemoveCommand(
test.NewFakeCli(&fakeClient{}))
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
testutil.ErrorContains(t, cmd.Execute(), tc.expectedError)
}
testCasesWithOutput := []struct {
name string
args []string
expectedError string
}{
{
name: "not-an-image",
args: []string{"user", "notanimage"},
expectedError: "error retrieving signers for notanimage",
},
{
name: "sha-reference",
args: []string{"user", "870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
expectedError: "invalid repository name",
},
{
name: "invalid-img-reference",
args: []string{"user", "ALPINE"},
expectedError: "invalid reference format",
},
}
for _, tc := range testCasesWithOutput {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getOfflineNotaryRepository)
cmd := newSignerRemoveCommand(cli)
cmd.SetArgs(tc.args)
cmd.SetOutput(ioutil.Discard)
cmd.Execute()
assert.Contains(t, cli.ErrBuffer().String(), tc.expectedError)
}
}
func TestRemoveSingleSigner(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedNotaryRepository)
err := removeSingleSigner(cli, "signed-repo", "test", true)
assert.EqualError(t, err, "No signer test for repository signed-repo")
err = removeSingleSigner(cli, "signed-repo", "releases", true)
assert.EqualError(t, err, "releases is a reserved keyword and cannot be removed")
}
func TestRemoveMultipleSigners(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedNotaryRepository)
err := removeSigner(cli, signerRemoveOptions{signer: "test", repos: []string{"signed-repo", "signed-repo"}, forceYes: true})
assert.EqualError(t, err, "Error removing signer from: signed-repo, signed-repo")
assert.Contains(t, cli.ErrBuffer().String(),
"No signer test for repository signed-repo")
assert.Contains(t, cli.OutBuffer().String(), "Removing signer \"test\" from signed-repo...\n")
}
func TestRemoveLastSignerWarning(t *testing.T) {
cli := test.NewFakeCli(&fakeClient{})
cli.SetNotaryClient(getLoadedNotaryRepository)
err := removeSigner(cli, signerRemoveOptions{signer: "alice", repos: []string{"signed-repo"}, forceYes: false})
assert.NoError(t, err)
assert.Contains(t, cli.OutBuffer().String(),
"The signer \"alice\" signed the last released version of signed-repo. "+
"Removing this signer will make signed-repo unpullable. "+
"Are you sure you want to continue? [y/N]")
}
func TestIsLastSignerForReleases(t *testing.T) {
role := data.Role{}
releaserole := client.RoleWithSignatures{}
releaserole.Name = releasesRoleTUFName
releaserole.Threshold = 1
allrole := []client.RoleWithSignatures{releaserole}
lastsigner, _ := isLastSignerForReleases(role, allrole)
assert.Equal(t, false, lastsigner)
role.KeyIDs = []string{"deadbeef"}
sig := data.Signature{}
sig.KeyID = "deadbeef"
releaserole.Signatures = []data.Signature{sig}
releaserole.Threshold = 1
allrole = []client.RoleWithSignatures{releaserole}
lastsigner, _ = isLastSignerForReleases(role, allrole)
assert.Equal(t, true, lastsigner)
sig.KeyID = "8badf00d"
releaserole.Signatures = []data.Signature{sig}
releaserole.Threshold = 1
allrole = []client.RoleWithSignatures{releaserole}
lastsigner, _ = isLastSignerForReleases(role, allrole)
assert.Equal(t, false, lastsigner)
}

View File

@ -43,7 +43,8 @@ var (
ActionsPushAndPull = []string{"pull", "push"}
)
func trustDirectory() string {
// GetTrustDirectory returns the base trust directory name
func GetTrustDirectory() string {
return filepath.Join(cliconfig.Dir(), "trust")
}
@ -172,15 +173,16 @@ func GetNotaryRepository(in io.Reader, out io.Writer, userAgent string, repoInfo
tr := transport.NewTransport(base, modifiers...)
return client.NewFileCachedRepository(
trustDirectory(),
GetTrustDirectory(),
data.GUN(repoInfo.Name.Name()),
server,
tr,
getPassphraseRetriever(in, out),
GetPassphraseRetriever(in, out),
trustpinning.TrustPinConfig{})
}
func getPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever {
// GetPassphraseRetriever returns a passphrase retriever that utilizes Content Trust env vars
func GetPassphraseRetriever(in io.Reader, out io.Writer) notary.PassRetriever {
aliasMap := map[string]string{
"root": "root",
"snapshot": "repository",

View File

@ -0,0 +1,69 @@
---
title: "key generate"
description: "The key generate command description and usage"
keywords: "Key, notary, trust"
---
<!-- This file is maintained within the docker/cli Github
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# trust key generate
```markdown
Usage: docker trust key generate NAME
Generate and load a signing key-pair
Options:
--dir string Directory to generate key in, defaults to current directory
--help Print usage
```
## Description
`docker trust key generate` generates a key-pair to be used with signing,
and loads the private key into the local docker trust keystore.
`docker trust key generate` is currently experimental.
## Examples
### Generate a key-pair
```bash
$ docker trust key generate alice
Generating key for alice...
Enter passphrase for new alice key with ID 17acf3c:
Repeat passphrase for new alice key with ID 17acf3c:
Successfully generated and loaded private key. Corresponding public key available: alice.pub
$ ls
alice.pub
```
The private signing key is encrypted by the passphrase and loaded into the docker trust keystore.
All passphrase requests to sign with the key will be referred to by the provided `NAME`.
The public key component `alice.pub` will be available in the current working directory, and can
be used directly by `docker trust signer add`.
Provide the `--dir` argument to specify a directory to generate the key in:
```bash
$ docker trust key generate alice --dir /foo
Generating key for alice...
Enter passphrase for new alice key with ID 17acf3c:
Repeat passphrase for new alice key with ID 17acf3c:
Successfully generated and loaded private key. Corresponding public key available: alice.pub
$ ls /foo
alice.pub
```

View File

@ -0,0 +1,59 @@
---
title: "key load"
description: "The key load command description and usage"
keywords: "Key, notary, trust"
---
<!-- This file is maintained within the docker/cli Github
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# trust key load
```markdown
Usage: docker trust key load [OPTIONS] KEYFILE
Load a private key file for signing
Options:
--help Print usage
--name string Name for the loaded key (default "signer")
```
## Description
`docker trust key load` adds private keys to the local docker trust keystore. To add a signer to a repository use `docker trust signer add`.
`docker trust key load` is currently experimental.
## Examples
### Load a single private key
For a private key `alice.pem` with permissions `-rw-------`
```bash
$ docker trust key load alice.pem
Loading key from "alice.pem"...
Enter passphrase for new signer key with ID f8097df:
Repeat passphrase for new signer key with ID f8097df:
Successfully imported key from alice.pem
```
to specify a name use the `--name` flag
```bash
$ docker trust key load --name alice-key alice.pem
Loading key from "alice.pem"...
Enter passphrase for new alice-key key with ID f8097df:
Repeat passphrase for new alice-key key with ID f8097df:
Successfully imported key from alice.pem
```

View File

@ -0,0 +1,213 @@
---
title: "signer add"
description: "The signer add command description and usage"
keywords: "signer, notary, trust"
---
<!-- This file is maintained within the docker/cli Github
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# trust signer add
```markdown
Usage: docker trust signer add [OPTIONS] NAME REPOSITORY [REPOSITORY...]
Add a signer
Options:
--help Print usage
-k, --key list Path to the signer's public key file
```
## Description
`docker trust signer add` adds signers to signed repositories.
`docker trust signer add` is currently experimental.
## Examples
### Add a signer to a repo
To add a new signer, `alice`, to this repository:
```bash
$ docker trust view example/trust-demo
No signatures for example/trust-demo
List of signers and their keys:
SIGNER KEYS
bob 5600f5ab76a2
Administrative keys for example/trust-demo:
Repository Key: 642692c14c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
Add `alice` with `docker trust signer add`:
```bash
$ docker trust signer add alice example/trust-demo --key alice.crt
Adding signer "alice" to example/trust-demo...
Enter passphrase for repository key with ID 642692c:
Successfully added signer: alice to example/trust-demo
```
`docker trust view` now lists `alice` as a valid signer:
```bash
$ docker trust view example/trust-demo
No signatures for example/trust-demo
List of signers and their keys:
SIGNER KEYS
alice 05e87edcaecb
bob 5600f5ab76a2
Administrative keys for example/trust-demo:
Repository Key: 642692c14c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
## Initialize a new repo and add a signer
When adding a signer on a repo for the first time, `docker trust signer add` sets up a new repo if it doesn't exist.
```bash
$ docker trust view example/trust-demo
No signatures or cannot access example/trust-demo
```
```bash
$ docker trust signer add alice example/trust-demo --key alice.crt
Initializing signed repository for example/trust-demo...
Enter passphrase for root key with ID 748121c:
Enter passphrase for new repository key with ID 95b9e55:
Repeat passphrase for new repository key with ID 95b9e55:
Successfully initialized "example/trust-demo"
Adding signer "alice" to example/trust-demo...
Successfully added signer: alice to example/trust-demo
```
```bash
$ docker trust view example/trust-demo
No signatures for example/trust-demo
SIGNED TAG DIGEST SIGNERS
List of signers and their keys:
SIGNER KEYS
alice 6d52b29d940f
Administrative keys for example/trust-demo:
Repository Key: 95b9e5565eac3ef5ec01406801bdfb70feb40c17808d2222427c18046eb63beb
Root Key: 748121c14bd1461f6c58cb3ef39087c8fdc7633bb11a98af844fd9a04e208103
```
## Add a signer to multiple repos
To add a signer, `alice`, to multiple repositories:
```bash
$ docker trust view example/trust-demo
SIGNED TAG DIGEST SIGNERS
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 bob
List of signers and their keys:
SIGNER KEYS
bob 5600f5ab76a2
Administrative keys for example/trust-demo:
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
```bash
$ docker trust view example/trust-demo2
SIGNED TAG DIGEST SIGNERS
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 bob
List of signers and their keys:
SIGNER KEYS
bob 5600f5ab76a2
Administrative keys for example/trust-demo2:
Repository Key: ece554f14c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4553d2ab20a8d9268
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
Add `alice` to both repositories with a single `docker trust signer add` command:
```bash
$ docker trust signer add alice example/trust-demo example/trust-demo2 --key alice.crt
Adding signer "alice" to example/trust-demo...
Enter passphrase for repository key with ID 95b9e55:
Successfully added signer: alice to example/trust-demo
Adding signer "alice" to example/trust-demo2...
Enter passphrase for repository key with ID ece554f:
Successfully added signer: alice to example/trust-demo2
```
`docker trust view` now lists `alice` as a valid signer of both `example/trust-demo` and `example/trust-demo2`:
```bash
$ docker trust view example/trust-demo
SIGNED TAG DIGEST SIGNERS
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 bob
List of signers and their keys:
SIGNER KEYS
alice 05e87edcaecb
bob 5600f5ab76a2
Administrative keys for example/trust-demo:
Repository Key: 95b9e5514c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
```bash
$ docker trust view example/trust-demo2
SIGNED TAG DIGEST SIGNERS
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 bob
List of signers and their keys:
SIGNER KEYS
alice 05e87edcaecb
bob 5600f5ab76a2
Administrative keys for example/trust-demo2:
Repository Key: ece554f14c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4553d2ab20a8d9268
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
`docker trust signer add` adds signers to repositories on a best effort basis, so it will continue to add the signer to subsequent repositories if one attempt fails:
```bash
$ docker trust signer add alice example/unauthorized example/authorized --key alice.crt
Adding signer "alice" to example/unauthorized...
you are not authorized to perform this operation: server returned 401.
Adding signer "alice" to example/authorized...
Enter passphrase for repository key with ID c6772a0:
Successfully added signer: alice to example/authorized
Failed to add signer to: example/unauthorized
```

View File

@ -0,0 +1,174 @@
---
title: "signer remove"
description: "The signer remove command description and usage"
keywords: "signer, notary, trust"
---
<!-- This file is maintained within the docker/cli Github
repository at https://github.com/docker/cli/. Make all
pull requests against that repo. If you see this file in
another repository, consider it read-only there, as it will
periodically be overwritten by the definitive file. Pull
requests which include edits to this file in other repositories
will be rejected.
-->
# trust signer remove
```markdown
Usage: docker trust signer remove [OPTIONS] NAME REPOSITORY [REPOSITORY...]
Remove a signer
Options:
-f, --force Do not prompt for confirmation before removing the most recent signer
--help Print usage
```
## Description
`docker trust signer remove` removes signers from signed repositories.
`docker trust signer remove` is currently experimental.
## Examples
### Remove a signer from a repo
To remove an existing signer, `alice`, from this repository:
```bash
$ docker trust view example/trust-demo
No signatures for example/trust-demo
List of signers and their keys:
SIGNER KEYS
alice 05e87edcaecb
bob 5600f5ab76a2
Administrative keys for example/trust-demo:
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
Remove `alice` with `docker trust signer remove`:
```bash
$ docker trust signer remove alice example/trust-demo
Removing signer "alice" from image example/trust-demo...
Enter passphrase for repository key with ID 642692c:
Successfully removed alice from example/trust-demo
```
`docker trust view` now does not list `alice` as a valid signer:
```bash
$ docker trust view example/trust-demo
No signatures for example/trust-demo
List of signers and their keys:
SIGNER KEYS
bob 5600f5ab76a2
Administrative keys for example/trust-demo:
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
### Remove a signer from multiple repos
To remove an existing signer, `alice`, from multiple repositories:
```bash
$ docker trust view example/trust-demo
SIGNED TAG DIGEST SIGNERS
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 alice, bob
List of signers and their keys:
SIGNER KEYS
alice 05e87edcaecb
bob 5600f5ab76a2
Administrative keys for example/trust-demo:
Repository Key: 95b9e5514c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
```bash
$ docker trust view example/trust-demo2
SIGNED TAG DIGEST SIGNERS
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 alice, bob
List of signers and their keys:
SIGNER KEYS
alice 05e87edcaecb
bob 5600f5ab76a2
Administrative keys for example/trust-demo2:
Repository Key: ece554f14c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4553d2ab20a8d9268
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
Remove `alice` from both images with a single `docker trust signer remove` command:
```bash
$ docker trust signer remove alice example/trust-demo example/trust-demo2
Removing signer "alice" from image example/trust-demo...
Enter passphrase for repository key with ID 95b9e55:
Successfully removed alice from example/trust-demo
Removing signer "alice" from image example/trust-demo2...
Enter passphrase for repository key with ID ece554f:
Successfully removed alice from example/trust-demo2
```
`docker trust view` no longer lists `alice` as a valid signer of either `example/trust-demo` or `example/trust-demo2`:
```bash
$ docker trust view example/trust-demo
SIGNED TAG DIGEST SIGNERS
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 bob
List of signers and their keys:
SIGNER KEYS
bob 5600f5ab76a2
Administrative keys for example/trust-demo:
Repository Key: ecc457614c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4555b3c6ab02f71e
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
```bash
$ docker trust view example/trust-demo2
SIGNED TAG DIGEST SIGNERS
v1 74d4bfa917d55d53c7df3d2ab20a8d926874d61c3da5ef6de15dd2654fc467c4 bob
List of signers and their keys:
SIGNER KEYS
bob 5600f5ab76a2
Administrative keys for example/trust-demo2:
Repository Key: ece554f14c9fc399da523a5f4e24fe306a0a6ee1cc79a10e4553d2ab20a8d9268
Root Key: 3cb2228f6561e58f46dbc4cda4fcaff9d5ef22e865a94636f82450d1d2234949
```
`docker trust signer remove` removes signers to repositories on a best effort basis, so it will continue to remove the signer from subsequent repositories if one attempt fails:
```bash
$ docker trust signer remove alice example/unauthorized example/authorized
Removing signer "alice" from image example/unauthorized...
No signer alice for image example/unauthorized
Removing signer "alice" from image example/authorized...
Enter passphrase for repository key with ID c6772a0:
Successfully removed alice from example/authorized
Error removing signer from: example/unauthorized
```

View File

@ -14,7 +14,7 @@ github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06
github.com/docker/go-connections 3ede32e2033de7505e6500d6c868c2b9ed9f169d
github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9
github.com/docker/go-units 9e638d38cf6977a37a8ea0078f3ee75a7cdb2dd1
github.com/docker/notary 8a1de3cfc3f1408e54d6364fc949214a4883a9f3
github.com/docker/notary 5d55a30c1bec010a8c6df4c09889acfb4e0a7942
github.com/docker/swarmkit 872861d2ae46958af7ead1d5fffb092c73afbaf0
github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
github.com/gogo/protobuf v0.4

View File

@ -1,3 +1,88 @@
/*
Package client implements everything required for interacting with a Notary repository.
Usage
Use this package by creating a new repository object and calling methods on it.
package main
import (
"encoding/hex"
"fmt"
"net/http"
"os"
"time"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
notary "github.com/docker/notary/client"
"github.com/docker/notary/trustpinning"
"github.com/docker/notary/tuf/data"
)
func main() {
rootDir := ".trust"
if err := os.MkdirAll(rootDir, 0700); err != nil {
panic(err)
}
server := "https://notary.docker.io"
image := "docker.io/library/alpine"
repo, err := notary.NewFileCachedNotaryRepository(
rootDir,
data.GUN(image),
server,
makeHubTransport(server, image),
nil,
trustpinning.TrustPinConfig{},
)
targets, err := repo.ListTargets()
if err != nil {
panic(err)
}
for _, tgt := range targets {
fmt.Printf("%s\t%s\n", tgt.Name, hex.EncodeToString(tgt.Hashes["sha256"]))
}
}
func makeHubTransport(server, image string) http.RoundTripper {
base := http.DefaultTransport
modifiers := []transport.RequestModifier{
transport.NewHeaderRequestModifier(http.Header{
"User-Agent": []string{"my-client"},
}),
}
authTransport := transport.NewTransport(base, modifiers...)
pingClient := &http.Client{
Transport: authTransport,
Timeout: 5 * time.Second,
}
req, err := http.NewRequest("GET", server+"/v2/", nil)
if err != nil {
panic(err)
}
challengeManager := challenge.NewSimpleManager()
resp, err := pingClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if err := challengeManager.AddResponse(resp); err != nil {
panic(err)
}
tokenHandler := auth.NewTokenHandler(base, nil, image, "pull")
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, auth.NewBasicHandler(nil)))
return transport.NewTransport(base, modifiers...)
}
*/
package client
import (

View File

@ -206,10 +206,7 @@ func (f *FilesystemStore) Set(name string, meta []byte) error {
os.RemoveAll(fp)
// Write the file to disk
if err = ioutil.WriteFile(fp, meta, notary.PrivNoExecPerms); err != nil {
return err
}
return nil
return ioutil.WriteFile(fp, meta, notary.PrivNoExecPerms)
}
// RemoveAll clears the existing filestore by removing its base directory

240
vendor/github.com/docker/notary/trustmanager/keys.go generated vendored Normal file
View File

@ -0,0 +1,240 @@
package trustmanager
import (
"encoding/pem"
"errors"
"io"
"io/ioutil"
"path/filepath"
"sort"
"strings"
"github.com/docker/notary"
tufdata "github.com/docker/notary/tuf/data"
"github.com/docker/notary/tuf/utils"
"github.com/sirupsen/logrus"
)
// Exporter is a simple interface for the two functions we need from the Storage interface
type Exporter interface {
Get(string) ([]byte, error)
ListFiles() []string
}
// Importer is a simple interface for the one function we need from the Storage interface
type Importer interface {
Set(string, []byte) error
}
// ExportKeysByGUN exports all keys filtered to a GUN
func ExportKeysByGUN(to io.Writer, s Exporter, gun string) error {
keys := s.ListFiles()
sort.Strings(keys) // ensure consistency. ListFiles has no order guarantee
for _, loc := range keys {
keyFile, err := s.Get(loc)
if err != nil {
logrus.Warn("Could not parse key file at ", loc)
continue
}
block, _ := pem.Decode(keyFile)
keyGun := block.Headers["gun"]
if keyGun == gun { // must be full GUN match
if err := ExportKeys(to, s, loc); err != nil {
return err
}
}
}
return nil
}
// ExportKeysByID exports all keys matching the given ID
func ExportKeysByID(to io.Writer, s Exporter, ids []string) error {
want := make(map[string]struct{})
for _, id := range ids {
want[id] = struct{}{}
}
keys := s.ListFiles()
for _, k := range keys {
id := filepath.Base(k)
if _, ok := want[id]; ok {
if err := ExportKeys(to, s, k); err != nil {
return err
}
}
}
return nil
}
// ExportKeys copies a key from the store to the io.Writer
func ExportKeys(to io.Writer, s Exporter, from string) error {
// get PEM block
k, err := s.Get(from)
if err != nil {
return err
}
// parse PEM blocks if there are more than one
for block, rest := pem.Decode(k); block != nil; block, rest = pem.Decode(rest) {
// add from path in a header for later import
block.Headers["path"] = from
// write serialized PEM
err = pem.Encode(to, block)
if err != nil {
return err
}
}
return nil
}
// ImportKeys expects an io.Reader containing one or more PEM blocks.
// It reads PEM blocks one at a time until pem.Decode returns a nil
// block.
// Each block is written to the subpath indicated in the "path" PEM
// header. If the file already exists, the file is truncated. Multiple
// adjacent PEMs with the same "path" header are appended together.
func ImportKeys(from io.Reader, to []Importer, fallbackRole string, fallbackGUN string, passRet notary.PassRetriever) error {
// importLogic.md contains a small flowchart I made to clear up my understand while writing the cases in this function
// it is very rough, but it may help while reading this piece of code
data, err := ioutil.ReadAll(from)
if err != nil {
return err
}
var (
writeTo string
toWrite []byte
)
for block, rest := pem.Decode(data); block != nil; block, rest = pem.Decode(rest) {
handleLegacyPath(block)
setFallbacks(block, fallbackGUN, fallbackRole)
loc, err := checkValidity(block)
if err != nil {
// already logged in checkValidity
continue
}
// the path header is not of any use once we've imported the key so strip it away
delete(block.Headers, "path")
// we are now all set for import but let's first encrypt the key
blockBytes := pem.EncodeToMemory(block)
// check if key is encrypted, note: if it is encrypted at this point, it will have had a path header
if privKey, err := utils.ParsePEMPrivateKey(blockBytes, ""); err == nil {
// Key is not encrypted- ask for a passphrase and encrypt this key
var chosenPassphrase string
for attempts := 0; ; attempts++ {
var giveup bool
chosenPassphrase, giveup, err = passRet(loc, block.Headers["role"], true, attempts)
if err == nil {
break
}
if giveup || attempts > 10 {
return errors.New("maximum number of passphrase attempts exceeded")
}
}
blockBytes, err = utils.ConvertPrivateKeyToPKCS8(privKey, tufdata.RoleName(block.Headers["role"]), tufdata.GUN(block.Headers["gun"]), chosenPassphrase)
if err != nil {
return errors.New("failed to encrypt key with given passphrase")
}
}
if loc != writeTo {
// next location is different from previous one. We've finished aggregating
// data for the previous file. If we have data, write the previous file,
// clear toWrite and set writeTo to the next path we're going to write
if toWrite != nil {
if err = importToStores(to, writeTo, toWrite); err != nil {
return err
}
}
// set up for aggregating next file's data
toWrite = nil
writeTo = loc
}
toWrite = append(toWrite, blockBytes...)
}
if toWrite != nil { // close out final iteration if there's data left
return importToStores(to, writeTo, toWrite)
}
return nil
}
func handleLegacyPath(block *pem.Block) {
// if there is a legacy path then we set the gun header from this path
// this is the case when a user attempts to import a key bundle generated by an older client
if rawPath := block.Headers["path"]; rawPath != "" && rawPath != filepath.Base(rawPath) {
// this is a legacy filepath and we should try to deduce the gun name from it
pathWOFileName := filepath.Dir(rawPath)
if strings.HasPrefix(pathWOFileName, notary.NonRootKeysSubdir) {
// remove the notary keystore-specific segment of the path, and any potential leading or trailing slashes
gunName := strings.Trim(strings.TrimPrefix(pathWOFileName, notary.NonRootKeysSubdir), "/")
if gunName != "" {
block.Headers["gun"] = gunName
}
}
block.Headers["path"] = filepath.Base(rawPath)
}
}
func setFallbacks(block *pem.Block, fallbackGUN, fallbackRole string) {
if block.Headers["gun"] == "" {
if fallbackGUN != "" {
block.Headers["gun"] = fallbackGUN
}
}
if block.Headers["role"] == "" {
if fallbackRole == "" {
block.Headers["role"] = notary.DefaultImportRole
} else {
block.Headers["role"] = fallbackRole
}
}
}
// checkValidity ensures the fields in the pem headers are valid and parses out the location.
// While importing a collection of keys, errors from this function should result in only the
// current pem block being skipped.
func checkValidity(block *pem.Block) (string, error) {
// A root key or a delegations key should not have a gun
// Note that a key that is not any of the canonical roles (except root) is a delegations key and should not have a gun
switch block.Headers["role"] {
case tufdata.CanonicalSnapshotRole.String(), tufdata.CanonicalTargetsRole.String(), tufdata.CanonicalTimestampRole.String():
// check if the key is missing a gun header or has an empty gun and error out since we don't know what gun it belongs to
if block.Headers["gun"] == "" {
logrus.Warnf("failed to import key (%s) to store: Cannot have canonical role key without a gun, don't know what gun it belongs to", block.Headers["path"])
return "", errors.New("invalid key pem block")
}
default:
delete(block.Headers, "gun")
}
loc, ok := block.Headers["path"]
// only if the path isn't specified do we get into this parsing path logic
if !ok || loc == "" {
// if the path isn't specified, we will try to infer the path rel to trust dir from the role (and then gun)
// parse key for the keyID which we will save it by.
// if the key is encrypted at this point, we will generate an error and continue since we don't know the ID to save it by
decodedKey, err := utils.ParsePEMPrivateKey(pem.EncodeToMemory(block), "")
if err != nil {
logrus.Warn("failed to import key to store: Invalid key generated, key may be encrypted and does not contain path header")
return "", errors.New("invalid key pem block")
}
loc = decodedKey.ID()
}
return loc, nil
}
func importToStores(to []Importer, path string, bytes []byte) error {
var err error
for _, i := range to {
if err = i.Set(path, bytes); err != nil {
logrus.Errorf("failed to import key to store: %s", err.Error())
continue
}
break
}
return err
}

View File

@ -12,9 +12,24 @@ import (
// TrustPinConfig represents the configuration under the trust_pinning section of the config file
// This struct represents the preferred way to bootstrap trust for this repository
// This is fully optional. If left at the default, uninitialized value Notary will use TOFU over
// HTTPS.
// You can use this to provide certificates or a CA to pin to as a root of trust for a GUN.
// These are used with the following precedence:
//
// 1. Certs
// 2. CA
// 3. TOFUS (TOFU over HTTPS)
//
// Only one trust pinning option will be used to validate a particular GUN.
type TrustPinConfig struct {
CA map[string]string
Certs map[string][]string
// CA maps a GUN prefix to file paths containing the root CA.
// This file can contain multiple root certificates, bundled in separate PEM blocks.
CA map[string]string
// Certs maps a GUN to a list of certificate IDs
Certs map[string][]string
// DisableTOFU, when true, disables "Trust On First Use" of new key data
// This is false by default, which means new key data will always be trusted the first time it is seen.
DisableTOFU bool
}

View File

@ -19,7 +19,9 @@ import (
"github.com/sirupsen/logrus"
)
// GUN type for specifying gun
// GUN is a Globally Unique Name. It is used to identify trust collections.
// An example usage of this is for container image repositories.
// For example: myregistry.io/myuser/myimage
type GUN string
func (g GUN) String() string {

View File

@ -24,7 +24,7 @@ github.com/prometheus/common 4fdc91a58c9d3696b982e8a680f4997403132d44
github.com/golang/protobuf c3cefd437628a0b7d31b34fe44b3a7a540e98527
github.com/spf13/cobra f368244301305f414206f889b1735a54cfc8bde8
github.com/spf13/viper be5ff3e4840cf692388bde7a057595a474ef379e
golang.org/x/crypto 5bcd134fee4dd1475da17714aac19c0aa0142e2f
golang.org/x/crypto 76eec36fa14229c4b25bb894c2d0e591527af429
golang.org/x/net 6a513affb38dc9788b449d59ffed099b8de18fa0
golang.org/x/sys 739734461d1c916b6c72a63d7efda2b27edb369f
google.golang.org/grpc 708a7f9f3283aa2d4f6132d287d78683babe55c8 # v1.0.5