Merge pull request #566 from riyazdf/docker-trust-2
docker trust: interact with signers and keysmaster
commit
0c4fa699eb
|
@ -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) {
|
||||
|
|
|
@ -18,6 +18,8 @@ func NewTrustCommand(dockerCli command.Cli) *cobra.Command {
|
|||
newViewCommand(dockerCli),
|
||||
newRevokeCommand(dockerCli),
|
||||
newSignCommand(dockerCli),
|
||||
newTrustKeyCommand(dockerCli),
|
||||
newTrustSignerCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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/\"")
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()))
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
||||
```
|
|
@ -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
|
||||
|
||||
```
|
|
@ -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
|
||||
```
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue